diff --git a/src/transport.ts b/src/transport.ts index 90aa038..fdc1ec7 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -8,6 +8,47 @@ export type SerializedDataType = 'date'|'binary'|'reference'|'regexp'|'array'; export type SerializedDataMap = { [path: string]: SerializedDataType }; export type SerializedValue = { map?: SerializedDataType | SerializedDataMap, val: any }; +export type V2SerializedPrimitive = string|number|boolean; +export type V2SerializedDate = { '.type': 'date'; '.val': string } +export type V2SerializedBinary = { '.type': 'binary'; '.val': string }; +export type V2SerializedReference = { '.type': 'reference'; '.val': string }; +export type V2SerializedRegExp = { '.type': 'regexp'; '.val': { pattern: string; flags: string } }; +export type V2SerializedPartialArray = { '.type': 'array'; [index: string]: any }; +export type V2SerializedObject = { [key: string]: V2SerializedValue }; +export type V2SerializedArray = V2SerializedValue[]; +export type V2SerializedValue = V2SerializedPrimitive|V2SerializedDate|V2SerializedBinary|V2SerializedReference|V2SerializedRegExp|V2SerializedPartialArray|V2SerializedObject|V2SerializedArray; + +/** + * There are now 2 different serialization methods for transporting values. + * + * v1: + * The original version (v1) created an object with "map" and "val" properties. + * The "map" property was made optional in v1.14.1 so they won't be present for values needing no serializing + * + * v2: + * The new version replaces serialized values inline by objects containing ".type" and ".val" properties. + * This serializing method was introduced by `export` and `import` methods because they use streaming and + * are unable to prepare type mappings up-front. This format is smaller in transmission (in many cases), + * and easier to read and process. + * + * original: { "date": (some date) } + * v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z" } } + * v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" } } + * + * original: (some date) + * v1 serialized: { "map": "date", "val": "2022-04-22T07:49:23Z" } + * v2 serialized: { ".type": "date", ".val": "2022-04-22T07:49:23Z" } + * comment: top level value that need serializing is wrapped in an object with ".type" and ".val". v1 is smaller in this case + * + * original: 'some string' + * v1 serialized: { "map": {}, "val": "some string" } + * v2 serialized: "some string" + * comment: primitive types such as strings don't need serializing and are returned as is in v2 + * + * original: { "date": (some date), "text": "Some string" } + * v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z", "text": "Some string" } } + * v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" }, "text": "Some string" } + */ export const Transport = { deserialize(data: SerializedValue) { if (data.map === null || typeof data.map === 'undefined') { @@ -55,6 +96,22 @@ export const Transport = { return data.val; }, + detectSerializeVersion(data: any) { + if (typeof data !== 'object' || data === null) { + // This can only be v2, which allows primitive types to bypass serializing + return 2; + } + if ('map' in data && 'val' in data) { + return 1; + } + else if ('val' in data) { + // If it's v1, 'val' will be the only key in the object because serialize2 adds ".version": 2 to the object to prevent confusion. + if (Object.keys(data).length > 1) { return 2; } + return 1; + } + return 2; + }, + serialize(obj: any): SerializedValue { // Recursively find dates and binary data if (obj === null || typeof obj !== 'object' || obj instanceof Date || obj instanceof ArrayBuffer || obj instanceof PathReference) { @@ -99,9 +156,140 @@ export const Transport = { }; const mappings: SerializedDataMap = {}; process(obj, mappings, ''); - return { - map: Object.keys(mappings).length > 0 ? mappings : undefined, - val: obj - }; + + const serialized: SerializedValue = { val: obj }; + if (Object.keys(mappings).length > 0) { + serialized.map = mappings; + } + return serialized; + }, + + serialize2(obj: any): any { + // Recursively find data that needs serializing + const getSerializedValue = (val: any): V2SerializedValue => { + if (val instanceof Date) { + // serialize date to UTC string + return { + '.type': 'date', + '.val': val.toISOString() + }; + } + else if (val instanceof ArrayBuffer) { + // Serialize binary data with ascii85 + return { + '.type': 'binary', + '.val': ascii85.encode(val) + }; + } + else if (val instanceof PathReference) { + return { + '.type': 'reference', + '.val': val.path + }; + } + else if (val instanceof RegExp) { + // Queries using the 'matches' filter with a regular expression can now also be used on remote db's + return { + '.type': 'regexp', + '.val': { + pattern: val.source, + flags: val.flags + } + }; + } + else if (typeof val === 'object' && val !== null) { + if (val instanceof Array) { + const copy: V2SerializedArray = []; + for (let i = 0; i < val.length; i++) { + copy[i] = getSerializedValue(val[i]); + } + return copy; + } + else { + const copy: V2SerializedObject = {}; //val instanceof Array ? [] : {} as SerializedValueV2; + if (val instanceof PartialArray) { + // Mark the object as partial ("sparse") array + (copy as V2SerializedPartialArray)['.type'] = 'array'; + } + for (const prop in val) { + copy[prop] = getSerializedValue(val[prop]); + } + return copy; + } + } + else { + // Primitive value. Don't serialize + return val; + } + } + + const serialized = getSerializedValue(obj); + if (typeof serialized === 'object' && 'val' in serialized && Object.keys(serialized).length === 1) { + // acebase-core v1.14.1 made the 'map' property optional. + // This v2 serialized object might be confused with a v1 without mappings, because it only has a "val" property + // To prevent this, mark the serialized object with version 2 + serialized['.version'] = 2; + } + return serialized; + }, + + deserialize2(data: V2SerializedValue): any { + if (typeof data !== 'object' || data === null) { + // primitive value, not serialized + return data; + } + switch (data['.type']) { + case undefined: { + // No type given: this is a plain object or array + + if (data instanceof Array) { + // Plain array, deserialize items into a copy + const copy = []; + const arr = data as V2SerializedArray; + for (let i = 0; i < arr.length; i++) { + copy.push(Transport.deserialize2(arr[i])); + } + return copy; + } + else { + // Plain object, deserialize properties into a copy + const copy = {}; + const obj = data as V2SerializedObject; + for (const prop in obj) { + copy[prop] = Transport.deserialize2(obj[prop]); + } + return copy; + } + } + case 'array': { + // partial ("sparse") array, deserialize children into a copy + const copy = {}; + for (const index in data as V2SerializedPartialArray) { + copy[index] = Transport.deserialize2(data[index]); + } + delete copy['.type']; + return new PartialArray(copy); + } + case 'date': { + // Date was serialized as a string (UTC) + const val = (data as V2SerializedDate)['.val']; + return new Date(val); + } + case 'binary': { + // ascii85 encoded binary data + const val = (data as V2SerializedBinary)['.val']; + return ascii85.decode(val); + } + case 'reference': { + const val = (data as V2SerializedReference)['.val']; + return new PathReference(val); + } + case 'regexp': { + const val = (data as V2SerializedRegExp)['.val']; + return new RegExp(val.pattern, val.flags); + } + } + throw new Error(`Unknown data type "${data['.type']}" in serialized value`); } + }; \ No newline at end of file diff --git a/types/transport.d.ts b/types/transport.d.ts index 8a76cbf..a701415 100644 --- a/types/transport.d.ts +++ b/types/transport.d.ts @@ -1,7 +1,19 @@ export type SerializedDataType = 'date'|'binary'|'reference'|'regexp'|'array'; export type SerializedDataMap = { [path: string]: SerializedDataType }; export type SerializedValue = { map?: SerializedDataType | SerializedDataMap, val: any }; + +export type V2SerializedPrimitive = string|number|boolean; +export type V2SerializedDate = { '.type': 'date'; '.val': string } +export type V2SerializedBinary = { '.type': 'binary'; '.val': string }; +export type V2SerializedReference = { '.type': 'reference'; '.val': string }; +export type V2SerializedRegExp = { '.type': 'regexp'; '.val': { pattern: string; flags: string } }; +export type V2SerializedPartialArray = { '.type': 'array'; [index: string]: any }; +export type V2SerializedObject = { [key: string]: V2SerializedValue }; +export type V2SerializedArray = V2SerializedValue[]; +export type V2SerializedValue = V2SerializedPrimitive|V2SerializedDate|V2SerializedBinary|V2SerializedReference|V2SerializedRegExp|V2SerializedPartialArray|V2SerializedObject|V2SerializedArray; export abstract class Transport { static serialize(obj: any): any static deserialize(obj: any): any + static serialize2(obj: any): any + static deserialize2(obj: any): any } \ No newline at end of file