From 2d414fb7cfe0596bbbe0a673b974ab24e076916d Mon Sep 17 00:00:00 2001 From: Julian Mills Date: Fri, 22 Nov 2024 14:09:03 +0100 Subject: [PATCH 1/2] improve the public facing Surreal API --- src/surreal.ts | 61 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/surreal.ts b/src/surreal.ts index 5b4bf0d..d27630c 100644 --- a/src/surreal.ts +++ b/src/surreal.ts @@ -319,10 +319,11 @@ export class Surreal { } /** - * Start a live query and listen for the responses + * Start a live select query and invoke the callback with responses * @param table - The table that you want to receive live results for. * @param callback - Callback function that receives updates. * @param diff - If set to true, will return a set of patches instead of complete records + * @returns A unique subscription ID */ async live< Result extends Record | Patch = Record, @@ -341,8 +342,8 @@ export class Surreal { } /** - * Listen for live query responses by it's uuid - * @param queryUuid - The LQ uuid that you want to receive live results for. + * Subscribe to an existing live select query and invoke the callback with responses + * @param queryUuid - The unique ID of an existing live query you want to receive updates for. * @param callback - Callback function that receives updates. */ async subscribeLive< @@ -358,9 +359,9 @@ export class Surreal { } /** - * Listen for live query responses by it's uuid - * @param queryUuid - The LQ uuid that you want to receive live results for. - * @param callback - Callback function that receives updates. + * Unsubscribe a callback from a live select query + * @param queryUuid - The unique ID of an existing live query you want to ubsubscribe from. + * @param callback - The previously subscribed callback function. */ async unSubscribeLive< Result extends Record | Patch = Record, @@ -406,7 +407,7 @@ export class Surreal { async query( ...args: QueryParameters ): Promise> { - const raw = await this.query_raw(...args); + const raw = await this.queryRaw(...args); return raw.map(({ status, result }) => { if (status === "ERR") throw new ResponseError(result); return result; @@ -418,7 +419,7 @@ export class Surreal { * @param query - Specifies the SurrealQL statements. * @param bindings - Assigns variables which can be used in the query. */ - async query_raw( + async queryRaw( ...[q, b]: QueryParameters ): Promise>> { const params = @@ -438,8 +439,21 @@ export class Surreal { return res.result; } + /** + * Runs a set of SurrealQL statements against the database. + * @param query - Specifies the SurrealQL statements. + * @param bindings - Assigns variables which can be used in the query. + * @deprecated Use `queryRaw` instead + */ + async query_raw( + ...args: QueryParameters + ): Promise>> { + return this.queryRaw(...args); + } + /** * Selects all records in a table, or a specific record, from the database. + * If you intend on sorting, filtering, or performing other operations on the data, it is recommended to use the `query` method instead. * @param thing - The table name or a record ID to select. */ async select(thing: RecordId): Promise>; @@ -478,7 +492,7 @@ export class Surreal { /** * Inserts one or multiple records in the database. - * @param thing - The table name or the specific record ID to create. + * @param table - The table name to insert into. * @param data - The document(s) / record(s) to insert. */ async insert( @@ -507,14 +521,14 @@ export class Surreal { * @param thing - The table name or the specific record ID to create. * @param data - The document(s) / record(s) to insert. */ - async insert_relation( + async insertRelation( data?: U | U[], ): Promise[]>; - async insert_relation( + async insertRelation( table: Table | string, data?: U | U[], ): Promise[]>; - async insert_relation( + async insertRelation( arg1: Table | string | U | U[], arg2?: U | U[], ) { @@ -531,6 +545,28 @@ export class Surreal { return res.result; } + /** + * Inserts one or multiple records in the database. + * @param thing - The table name or the specific record ID to create. + * @param data - The document(s) / record(s) to insert. + * @deprecated Use `insertRelation` instead + */ + async insert_relation( + data?: U | U[], + ): Promise[]>; + async insert_relation( + table: Table | string, + data?: U | U[], + ): Promise[]>; + async insert_relation( + arg1: Table | string | U | U[], + arg2?: U | U[], + ) { + return arg1 instanceof Table || typeof arg1 === "string" + ? this.insertRelation(arg1, arg2) + : this.insertRelation(arg1); + } + /** * Updates all records in a table, or a specific record, in the database. * @@ -664,6 +700,7 @@ export class Surreal { /** * Obtain the version of the SurrealDB instance + * @example `surrealdb-2.1.0` */ async version(): Promise { await this.ready; From 5a13c58437e103847efd31be4f6da9e4da3687c5 Mon Sep 17 00:00:00 2001 From: Julian Mills Date: Fri, 22 Nov 2024 15:06:03 +0100 Subject: [PATCH 2/2] improve public API documentation --- src/data/cbor.ts | 10 ++++++ src/data/index.ts | 1 - src/data/types/decimal.ts | 3 ++ src/data/types/duration.ts | 3 ++ src/data/types/future.ts | 3 ++ src/data/types/geometry.ts | 24 ++++++++++++++ src/data/types/range.ts | 30 +++++++++--------- src/data/types/recordid.ts | 53 +++++++------------------------ src/data/types/table.ts | 3 ++ src/data/types/uuid.ts | 3 ++ src/data/value.ts | 3 ++ src/index.ts | 1 + src/util/emitter.ts | 3 ++ src/util/equals.ts | 3 +- src/util/escape.ts | 56 +++++++++++++++++++++++++++++++++ src/util/jsonify.ts | 6 +++- src/util/prepared-query.ts | 25 +++++++++++++++ src/util/string-prefixes.ts | 24 ++++++++++++++ src/util/tagged-template.ts | 8 +++++ src/util/to-surrealql-string.ts | 5 +++ src/util/version-check.ts | 6 ++++ 21 files changed, 214 insertions(+), 59 deletions(-) create mode 100644 src/util/escape.ts diff --git a/src/data/cbor.ts b/src/data/cbor.ts index 9ca285b..ebf449e 100644 --- a/src/data/cbor.ts +++ b/src/data/cbor.ts @@ -160,12 +160,22 @@ export const replacer = { Object.freeze(replacer); +/** + * Recursively encode any supported SurrealQL value into a binary CBOR representation. + * @param data - The input value + * @returns CBOR binary representation + */ export function encodeCbor(data: T): ArrayBuffer { return encode(data, { replacer: replacer.encode, }); } +/** + * Decode a CBOR encoded SurrealQL value into object representation. + * @param data - The encoded SurrealQL value + * @returns The parsed SurrealQL value + */ // biome-ignore lint/suspicious/noExplicitAny: Don't know what it will return export function decodeCbor(data: ArrayBufferLike): any { return decode(data, { diff --git a/src/data/index.ts b/src/data/index.ts index 6ee3458..19c8689 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -2,7 +2,6 @@ export { RecordId, StringRecordId, type RecordIdValue, - escape_ident, } from "./types/recordid.ts"; export { Range, diff --git a/src/data/types/decimal.ts b/src/data/types/decimal.ts index 20d143b..8fcea77 100644 --- a/src/data/types/decimal.ts +++ b/src/data/types/decimal.ts @@ -1,5 +1,8 @@ import { Value } from "../value"; +/** + * A SurrealQL decimal value. + */ export class Decimal extends Value { readonly decimal: string; diff --git a/src/data/types/duration.ts b/src/data/types/duration.ts index de292a5..d015081 100644 --- a/src/data/types/duration.ts +++ b/src/data/types/duration.ts @@ -32,6 +32,9 @@ const durationPartRegex = new RegExp( `^(\\d+)(${Array.from(units.keys()).join("|")})`, ); +/** + * A SurrealQL duration value. + */ export class Duration extends Value { readonly _milliseconds: number; diff --git a/src/data/types/future.ts b/src/data/types/future.ts index 1e4fb06..28e8a28 100644 --- a/src/data/types/future.ts +++ b/src/data/types/future.ts @@ -1,5 +1,8 @@ import { Value } from "../value"; +/** + * An uncomputed SurrealQL future value. + */ export class Future extends Value { constructor(readonly inner: string) { super(); diff --git a/src/data/types/geometry.ts b/src/data/types/geometry.ts index cd2e66b..d7741b0 100644 --- a/src/data/types/geometry.ts +++ b/src/data/types/geometry.ts @@ -1,6 +1,9 @@ import { Value } from "../value.ts"; import { Decimal } from "./decimal.ts"; +/** + * A SurrealQL geometry value. + */ export abstract class Geometry extends Value { abstract toJSON(): GeoJson; abstract is(geometry: Geometry): boolean; @@ -21,6 +24,9 @@ function f(num: number | Decimal) { return num; } +/** + * A SurrealQL point geometry value. + */ export class GeometryPoint extends Geometry { readonly point: [number, number]; @@ -56,6 +62,9 @@ export class GeometryPoint extends Geometry { } } +/** + * A SurrealQL line geometry value. + */ export class GeometryLine extends Geometry { readonly line: [GeometryPoint, GeometryPoint, ...GeometryPoint[]]; @@ -108,6 +117,9 @@ export class GeometryLine extends Geometry { } } +/** + * A SurrealQL polygon geometry value. + */ export class GeometryPolygon extends Geometry { readonly polygon: [GeometryLine, ...GeometryLine[]]; @@ -153,6 +165,9 @@ export class GeometryPolygon extends Geometry { } } +/** + * A SurrealQL multi-point geometry value. + */ export class GeometryMultiPoint extends Geometry { readonly points: [GeometryPoint, ...GeometryPoint[]]; @@ -193,6 +208,9 @@ export class GeometryMultiPoint extends Geometry { } } +/** + * A SurrealQL multi-line geometry value. + */ export class GeometryMultiLine extends Geometry { readonly lines: [GeometryLine, ...GeometryLine[]]; @@ -231,6 +249,9 @@ export class GeometryMultiLine extends Geometry { } } +/** + * A SurrealQL multi-polygon geometry value. + */ export class GeometryMultiPolygon extends Geometry { readonly polygons: [GeometryPolygon, ...GeometryPolygon[]]; @@ -275,6 +296,9 @@ export class GeometryMultiPolygon extends Geometry { } } +/** + * A SurrealQL geometry collection value. + */ export class GeometryCollection extends Geometry { readonly collection: [Geometry, ...Geometry[]]; diff --git a/src/data/types/range.ts b/src/data/types/range.ts index 8887aae..d88623e 100644 --- a/src/data/types/range.ts +++ b/src/data/types/range.ts @@ -1,16 +1,15 @@ import { Tagged } from "../../cbor"; import { SurrealDbError } from "../../errors"; import { equals } from "../../util/equals"; +import { escapeIdent } from "../../util/escape"; import { toSurrealqlString } from "../../util/to-surrealql-string"; import { TAG_BOUND_EXCLUDED, TAG_BOUND_INCLUDED } from "../cbor"; import { Value } from "../value"; -import { - type RecordIdValue, - escape_id_part, - escape_ident, - isValidIdPart, -} from "./recordid"; +import { type RecordIdValue, escapeIdPart, isValidIdPart } from "./recordid"; +/** + * A SurrealQL range value. + */ export class Range extends Value { constructor( readonly beg: Bound, @@ -34,8 +33,8 @@ export class Range extends Value { } toString(): string { - const beg = escape_range_bound(this.beg); - const end = escape_range_bound(this.end); + const beg = escapeRangeBound(this.beg); + const end = escapeRangeBound(this.end); return `${beg}${getRangeJoin(this.beg, this.end)}${end}`; } } @@ -49,6 +48,9 @@ export class BoundExcluded { constructor(readonly value: T) {} } +/** + * A SurrealQL record ID range value. + */ export class RecordIdRange extends Value { constructor( public readonly tb: Tb, @@ -78,9 +80,9 @@ export class RecordIdRange extends Value { } toString(): string { - const tb = escape_ident(this.tb); - const beg = escape_id_bound(this.beg); - const end = escape_id_bound(this.end); + const tb = escapeIdent(this.tb); + const beg = escapeIdBound(this.beg); + const end = escapeIdBound(this.end); return `${tb}:${beg}${getRangeJoin(this.beg, this.end)}${end}`; } } @@ -99,13 +101,13 @@ function isValidIdBound(bound: Bound): bound is Bound { : true; } -function escape_id_bound(bound: Bound): string { +function escapeIdBound(bound: Bound): string { return bound instanceof BoundIncluded || bound instanceof BoundExcluded - ? escape_id_part(bound.value) + ? escapeIdPart(bound.value) : ""; } -function escape_range_bound(bound: Bound): string { +function escapeRangeBound(bound: Bound): string { if (bound === undefined) return ""; const value = bound.value; diff --git a/src/data/types/recordid.ts b/src/data/types/recordid.ts index 39fdcb1..405e9c4 100644 --- a/src/data/types/recordid.ts +++ b/src/data/types/recordid.ts @@ -1,10 +1,10 @@ import { SurrealDbError } from "../../errors"; import { equals } from "../../util/equals"; +import { escapeIdent, escapeNumber } from "../../util/escape"; import { toSurrealqlString } from "../../util/to-surrealql-string"; import { Value } from "../value"; import { Uuid } from "./uuid"; -const MAX_i64 = 9223372036854775807n; export type RecordIdValue = | string | number @@ -13,6 +13,9 @@ export type RecordIdValue = | unknown[] | Record; +/** + * A SurrealQL record ID value. + */ export class RecordId extends Value { public readonly tb: Tb; public readonly id: RecordIdValue; @@ -38,12 +41,15 @@ export class RecordId extends Value { } toString(): string { - const tb = escape_ident(this.tb); - const id = escape_id_part(this.id); + const tb = escapeIdent(this.tb); + const id = escapeIdPart(this.id); return `${tb}:${id}`; } } +/** + * A SurrealQL string-represented record ID value. + */ export class StringRecordId extends Value { public readonly rid: string; @@ -77,41 +83,6 @@ export class StringRecordId extends Value { } } -export function escape_number(num: number | bigint): string { - return num <= MAX_i64 ? num.toString() : `⟨${num}⟩`; -} - -export function escape_ident(str: string): string { - // String which looks like a number should always be escaped, to prevent it from being parsed as a number - if (isOnlyNumbers(str)) { - return `⟨${str}⟩`; - } - - let code: number; - let i: number; - let len: number; - - for (i = 0, len = str.length; i < len; i++) { - code = str.charCodeAt(i); - if ( - !(code > 47 && code < 58) && // numeric (0-9) - !(code > 64 && code < 91) && // upper alpha (A-Z) - !(code > 96 && code < 123) && // lower alpha (a-z) - !(code === 95) // underscore (_) - ) { - return `⟨${str.replaceAll("⟩", "\\⟩")}⟩`; - } - } - - return str; -} - -export function isOnlyNumbers(str: string): boolean { - const stripped = str.replace("_", ""); - const parsed = Number.parseInt(stripped); - return !Number.isNaN(parsed) && parsed.toString() === stripped; -} - export function isValidIdPart(v: unknown): v is RecordIdValue { if (v instanceof Uuid) return true; @@ -127,12 +98,12 @@ export function isValidIdPart(v: unknown): v is RecordIdValue { } } -export function escape_id_part(id: RecordIdValue): string { +export function escapeIdPart(id: RecordIdValue): string { return id instanceof Uuid ? `u"${id}"` : typeof id === "string" - ? escape_ident(id) + ? escapeIdent(id) : typeof id === "bigint" || typeof id === "number" - ? escape_number(id) + ? escapeNumber(id) : toSurrealqlString(id); } diff --git a/src/data/types/table.ts b/src/data/types/table.ts index 094cb33..238a6fd 100644 --- a/src/data/types/table.ts +++ b/src/data/types/table.ts @@ -1,6 +1,9 @@ import { SurrealDbError } from "../../errors"; import { Value } from "../value"; +/** + * A SurrealQL table value. + */ export class Table extends Value { public readonly tb: Tb; diff --git a/src/data/types/uuid.ts b/src/data/types/uuid.ts index a01ef3a..a4c92f0 100644 --- a/src/data/types/uuid.ts +++ b/src/data/types/uuid.ts @@ -1,6 +1,9 @@ import { UUID, uuidv4obj, uuidv7obj } from "uuidv7"; import { Value } from "../value"; +/** + * A SurrealQL UUID value. + */ export class Uuid extends Value { private readonly inner: UUID; diff --git a/src/data/value.ts b/src/data/value.ts index 07042b9..6658c57 100644 --- a/src/data/value.ts +++ b/src/data/value.ts @@ -1,3 +1,6 @@ +/** + * A complex SurrealQL value type + */ export abstract class Value { /** * Compare equality with another value. diff --git a/src/index.ts b/src/index.ts index ed73d78..389b3a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export * from "./util/version-check.ts"; export * from "./util/get-incremental-id.ts"; export * from "./util/string-prefixes.ts"; export * from "./util/to-surrealql-string.ts"; +export * from "./util/escape.ts"; export { ConnectionStatus, AbstractEngine, diff --git a/src/util/emitter.ts b/src/util/emitter.ts index 12b8146..43b3fa5 100644 --- a/src/util/emitter.ts +++ b/src/util/emitter.ts @@ -3,6 +3,9 @@ export type Listener = ( ) => unknown; export type UnknownEvents = Record; +/** + * A class used to subscribe to and emit events + */ export class Emitter { private collectable: Partial<{ [K in keyof Events]: Events[K][]; diff --git a/src/util/equals.ts b/src/util/equals.ts index 4da5348..3523e9f 100644 --- a/src/util/equals.ts +++ b/src/util/equals.ts @@ -1,8 +1,7 @@ import { Value } from "../data/value"; /** - * Compares two values for deep equality, including arrays, objects, - * and SurrealQL value types. + * Recursively compare supported SurrealQL values for equality. * * @param x The first value to compare * @param y The second value to compare diff --git a/src/util/escape.ts b/src/util/escape.ts new file mode 100644 index 0000000..64df781 --- /dev/null +++ b/src/util/escape.ts @@ -0,0 +1,56 @@ +const MAX_i64 = 9223372036854775807n; + +/** + * Escape a given string to be used as a valid SurrealQL ident. + * @param str - The string to escape + * @returns Optionally escaped string + */ +export function escapeIdent(str: string): string { + // String which looks like a number should always be escaped, to prevent it from being parsed as a number + if (isOnlyNumbers(str)) { + return `⟨${str}⟩`; + } + + let code: number; + let i: number; + let len: number; + + for (i = 0, len = str.length; i < len; i++) { + code = str.charCodeAt(i); + if ( + !(code > 47 && code < 58) && // numeric (0-9) + !(code > 64 && code < 91) && // upper alpha (A-Z) + !(code > 96 && code < 123) && // lower alpha (a-z) + !(code === 95) // underscore (_) + ) { + return `⟨${str.replaceAll("⟩", "\\⟩")}⟩`; + } + } + + return str; +} + +/** + * Escape a given string to be used as a valid SurrealQL ident. + * @param str - The string to escape + * @returns Optionally escaped string + * @deprecated Use `escapeIdent` instead + */ +export function escape_ident(str: string): string { + return escapeIdent(str); +} + +/** + * Escape a number to be used as a valid SurrealQL ident. + * @param num - The number to escape + * @returns Optionally escaped number + */ +export function escapeNumber(num: number | bigint): string { + return num <= MAX_i64 ? num.toString() : `⟨${num}⟩`; +} + +function isOnlyNumbers(str: string): boolean { + const stripped = str.replace("_", ""); + const parsed = Number.parseInt(stripped); + return !Number.isNaN(parsed) && parsed.toString() === stripped; +} diff --git a/src/util/jsonify.ts b/src/util/jsonify.ts index 1fb63c8..70c6eab 100644 --- a/src/util/jsonify.ts +++ b/src/util/jsonify.ts @@ -37,7 +37,11 @@ export type Jsonify = T extends : T extends Table ? `${Tb}` : T; - +/** + * Recursively convert any supported SurrealQL value into a serializable JSON representation. + * @param input - The input value + * @returns JSON-safe representation + */ export function jsonify(input: T): Jsonify { if (typeof input === "object") { if (input === null) return null as Jsonify; diff --git a/src/util/prepared-query.ts b/src/util/prepared-query.ts index 57e3650..771b91d 100644 --- a/src/util/prepared-query.ts +++ b/src/util/prepared-query.ts @@ -12,6 +12,10 @@ import { replacer } from "../data/cbor"; let textEncoder: TextEncoder; export type ConvertMethod = (result: unknown[]) => T; + +/** + * A query and its bindings prepared for execution, which can be passed to the .query() method. + */ export class PreparedQuery { private _query: Uint8Array; private _bindings: Record; @@ -26,6 +30,9 @@ export class PreparedQuery { this.length = Object.keys(this._bindings).length; } + /** + * Retrieves the encoded query string. + */ get query(): Encoded { // Up to 9 bytes for the prefix const w = new Writer(this._query.byteLength + 9); @@ -34,14 +41,32 @@ export class PreparedQuery { return new Encoded(w.output(false)); } + /** + * Retrieves the encoded bindings. + */ get bindings(): Record { return this._bindings; } + /** + * Compile this query and its bindings into a single ArrayBuffer, optionally filling gaps. + * @param fills - The gap values to fill + */ build(fills?: Fill[]): ArrayBuffer { return encode([this.query, this.bindings], { fills }); } + /** + * A template literal tag function for appending additional query segments and bindings to the prepared query. + * @param query_raw - The additional query segments to append + * @param values - The additional interpolated values to append + * @example + * const query = surrealql`SELECT * FROM person`; + * + * if (filter) { + * query.append` WHERE name = ${filter}`; + * } + */ append( query_raw: string[] | TemplateStringsArray, ...values: unknown[] diff --git a/src/util/string-prefixes.ts b/src/util/string-prefixes.ts index 21bd4b6..5b1c909 100644 --- a/src/util/string-prefixes.ts +++ b/src/util/string-prefixes.ts @@ -1,5 +1,11 @@ import { StringRecordId, Uuid } from "../data"; +/** + * A template literal tag function for parsing a string type + * @param string - The string to parse + * @param values - The interpolated values + * @returns The parsed string + */ export function s( string: string[] | TemplateStringsArray, ...values: unknown[] @@ -10,6 +16,12 @@ export function s( ); } +/** + * A template literal tag function for parsing a string into a Date + * @param string - The string to parse + * @param values - The interpolated values + * @returns The parsed Date + */ export function d( string: string[] | TemplateStringsArray, ...values: unknown[] @@ -17,6 +29,12 @@ export function d( return new Date(s(string, values)); } +/** + * A template literal tag function for parsing a string into a StringRecordId + * @param string - The string to parse + * @param values - The interpolated values + * @returns The parsed StringRecordId + */ export function r( string: string[] | TemplateStringsArray, ...values: unknown[] @@ -24,6 +42,12 @@ export function r( return new StringRecordId(s(string, values)); } +/** + * A template literal tag function for parsing a string into a Uuid + * @param string - The string to parse + * @param values - The interpolated values + * @returns The parsed Uuid + */ export function u( string: string[] | TemplateStringsArray, ...values: unknown[] diff --git a/src/util/tagged-template.ts b/src/util/tagged-template.ts index adc68cb..d52d250 100644 --- a/src/util/tagged-template.ts +++ b/src/util/tagged-template.ts @@ -1,6 +1,14 @@ import { Gap } from "../cbor/gap.ts"; import { PreparedQuery } from "./prepared-query.ts"; +/** + * A template literal tag function for creating prepared queries from query strings. + * Interpolated values are automatically stored as bindings. + * @param query_raw - The raw query string + * @param values - The interpolated values + * @example const query = surrealql`SELECT * FROM ${id}`; + * @returns A PreparedQuery instance + */ export function surrealql( query_raw: string[] | TemplateStringsArray, ...values: unknown[] diff --git a/src/util/to-surrealql-string.ts b/src/util/to-surrealql-string.ts index d5e41ed..aa62a31 100644 --- a/src/util/to-surrealql-string.ts +++ b/src/util/to-surrealql-string.ts @@ -10,6 +10,11 @@ import { Uuid, } from "../data"; +/** + * Recursively print any supported SurrealQL value into a string representation. + * @param input - The input value + * @returns Stringified SurrealQL representation + */ export function toSurrealqlString(input: unknown): string { if (typeof input === "string") return `s${JSON.stringify(input)}`; if (input === null) return "NULL"; diff --git a/src/util/version-check.ts b/src/util/version-check.ts index f083787..4741ddf 100644 --- a/src/util/version-check.ts +++ b/src/util/version-check.ts @@ -33,6 +33,12 @@ export function isVersionSupported( ); } +/** + * Query the version of a remote SurrealDB instance + * @param url - The URL of the remote SurrealDB instance + * @param timeout - The timeout in milliseconds for the request + * @returns + */ export async function retrieveRemoteVersion( url: URL, timeout?: number,