@lsmsgpack/client 0.0.0-stage → 2026.10.2201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/dist/client.d.ts +76 -0
- package/dist/client.js +150 -0
- package/dist/client.js.map +1 -0
- package/dist/decoder.d.ts +174 -0
- package/dist/decoder.js +720 -0
- package/dist/decoder.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +56 -0
- package/dist/models.js +64 -0
- package/dist/models.js.map +1 -0
- package/dist/names.d.ts +9 -0
- package/dist/names.js +37 -0
- package/dist/names.js.map +1 -0
- package/dist/reader.d.ts +62 -0
- package/dist/reader.js +338 -0
- package/dist/reader.js.map +1 -0
- package/dist/schema.d.ts +76 -0
- package/dist/schema.js +107 -0
- package/dist/schema.js.map +1 -0
- package/dist/sha256.d.ts +4 -0
- package/dist/sha256.js +75 -0
- package/dist/sha256.js.map +1 -0
- package/dist/store.d.ts +49 -0
- package/dist/store.js +148 -0
- package/dist/store.js.map +1 -0
- package/dist/values.d.ts +34 -0
- package/dist/values.js +169 -0
- package/dist/values.js.map +1 -0
- package/dist/writer.d.ts +18 -0
- package/dist/writer.js +685 -0
- package/dist/writer.js.map +1 -0
- package/package.json +38 -6
package/dist/decoder.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// Reads application/x-lsmsgpack payloads (the schema, or a reference to it, followed by the body) into complete objects:
|
|
2
|
+
// every property of the schema entry is set, the ones the writer left out (nil, or missing) get their default.
|
|
3
|
+
// An object that came with a type id gets its type name as the property $type (as System.Text.Json names it): the encoder writes it back as the type id.
|
|
4
|
+
import { MissingSchemaError, MsgPackError } from './errors.js';
|
|
5
|
+
import { MsgPackExtension, MsgPackReader } from './reader.js';
|
|
6
|
+
import { nameFunction } from './names.js';
|
|
7
|
+
import { AnyType, Schema } from './schema.js';
|
|
8
|
+
import { toHex } from './sha256.js';
|
|
9
|
+
import { convertDecimal, dateOnlyText, decodeDecimal, EmptyGuid, guidString, MinDateTimeMs, offsetText, timeOnlyText, timeSpanText } from './values.js';
|
|
10
|
+
let codeAllowed = true; // false once new Function failed (Content-Security-Policy without 'unsafe-eval')
|
|
11
|
+
/** The property that holds the type name of an object read with a type id, and that the encoder writes as the type id. */
|
|
12
|
+
export const TypeProperty = '$type';
|
|
13
|
+
/**
|
|
14
|
+
* The generated create and fill of a plan (see DecodeOptions.generateCode), undefined when code cannot be generated. Instances of classes only get the fill (create is new).
|
|
15
|
+
* The fill has the read functions of the properties as constants: each call site then always calls the same function (reads[t](r) was one call site for all of them).
|
|
16
|
+
*/
|
|
17
|
+
function generate(names, reads, literals) {
|
|
18
|
+
if (!codeAllowed || names.length === 0 || names.length > 512 || names.includes('__proto__') || names.includes(TypeProperty))
|
|
19
|
+
return undefined; // an object literal would set the prototype, or have $type twice
|
|
20
|
+
const literal = names.map((n, t) => `${JSON.stringify(n)}: v[${t}]`).join(', ');
|
|
21
|
+
let fill = `const ${names.map((_, t) => `read${t} = reads[${t}]`).join(', ')};\nreturn function fill(r, o, n) {\nconst b = r.bytes;\n`;
|
|
22
|
+
names.forEach((n, t) => {
|
|
23
|
+
fill += `if (n === ${t}) return;\nif (b[r.pos] === 0xc0) r.pos++; else o[${JSON.stringify(n)}] = read${t}(r);\n`;
|
|
24
|
+
});
|
|
25
|
+
fill += `for (let k = ${names.length}; k < n; k++) r.value();\n};`;
|
|
26
|
+
try {
|
|
27
|
+
const fillFn = new Function('reads', fill)(reads);
|
|
28
|
+
if (!literals)
|
|
29
|
+
return { fill: fillFn };
|
|
30
|
+
return {
|
|
31
|
+
create: new Function('v', `return { ${literal} };`),
|
|
32
|
+
createTyped: new Function('v', 't', `return { ${JSON.stringify(TypeProperty)}: t, ${literal} };`), // $type first, as System.Text.Json writes it
|
|
33
|
+
fill: fillFn,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
codeAllowed = false;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generates the reading of an object of the array layout: the values into variables first (the defaults where the data left them out), then the object at once,
|
|
43
|
+
* so its properties are written once (a literal, or for a model a constructor function with the class's prototype: new of the class would make its initial values
|
|
44
|
+
* only to have them replaced). The defaults that are made per object (makers, null until then) are made after the values, as Plan.complete does.
|
|
45
|
+
* @param order the properties in the order the object gets them: an index of names, or an Extra
|
|
46
|
+
* @param proto the prototype of the model's class, undefined for a plain object
|
|
47
|
+
*/
|
|
48
|
+
function generateReadArray(names, reads, defaults, makers, order, proto) {
|
|
49
|
+
const count = names.length;
|
|
50
|
+
if (!codeAllowed || count === 0 || count > 512 || names.includes('__proto__') || names.includes(TypeProperty) || order.some((p) => typeof p !== 'number' && p.name === '__proto__'))
|
|
51
|
+
return undefined;
|
|
52
|
+
const v = names.map((_, t) => `v${t}`);
|
|
53
|
+
let code = `const ${names.map((_, t) => `read${t} = reads[${t}], d${t} = defaults[${t}], m${t} = makers[${t}]`).join(', ')};\n`;
|
|
54
|
+
let result;
|
|
55
|
+
if (proto === undefined)
|
|
56
|
+
result = `{ ${order.map((p) => `${JSON.stringify(names[p])}: v${p}`).join(', ')} }`;
|
|
57
|
+
else {
|
|
58
|
+
code += `const extras = order.filter((p) => typeof p !== 'number');\n`;
|
|
59
|
+
let extra = 0;
|
|
60
|
+
code += `function Model(${v.join(', ')}) {\n${order.map((p) => typeof p === 'number'
|
|
61
|
+
? `this[${JSON.stringify(names[p])}] = v${p};\n`
|
|
62
|
+
: `this[${JSON.stringify(p.name)}] = ${p.make === undefined ? `extras[${extra++}].value` : `extras[${extra++}].make()`};\n`).join('')}}\nModel.prototype = proto;\n`;
|
|
63
|
+
result = `new Model(${v.join(', ')})`;
|
|
64
|
+
}
|
|
65
|
+
code += `return function readArray(r, n) {\nconst b = r.bytes;\nlet ${names.map((_, t) => `v${t} = d${t}`).join(', ')};\nvalues: {\n`;
|
|
66
|
+
names.forEach((_, t) => {
|
|
67
|
+
code += `if (n === ${t}) break values;\nif (b[r.pos] === 0xc0) r.pos++; else v${t} = read${t}(r);\n`;
|
|
68
|
+
});
|
|
69
|
+
code += `for (let k = ${count}; k < n; k++) r.value();\n}\n`;
|
|
70
|
+
makers.forEach((m, t) => {
|
|
71
|
+
if (m !== undefined)
|
|
72
|
+
code += `if (v${t} === null) v${t} = m${t}();\n`;
|
|
73
|
+
});
|
|
74
|
+
code += `return ${result};\n};`;
|
|
75
|
+
try {
|
|
76
|
+
return new Function('reads', 'defaults', 'makers', 'order', 'proto', code)(reads, defaults, makers, order, proto);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
codeAllowed = false;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The initial value of a field of a model's class (probe: an instance), and how to make it for each object when it is not a primitive. */
|
|
84
|
+
function initialOf(cls, probe, name) {
|
|
85
|
+
const value = probe[name];
|
|
86
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
|
|
87
|
+
return { value };
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
const time = value.getTime();
|
|
90
|
+
return { value: null, make: () => new Date(time) };
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value) && value.length === 0 && Object.getPrototypeOf(value) === Array.prototype)
|
|
93
|
+
return { value: null, make: () => [] };
|
|
94
|
+
return { value: null, make: () => new cls()[name] }; // e.g. a nested object: as the class makes it
|
|
95
|
+
}
|
|
96
|
+
/** Sets a property, also one named __proto__ (as JSON.parse does: an own property, not the prototype). */
|
|
97
|
+
export function setOwn(o, name, value) {
|
|
98
|
+
if (name === '__proto__')
|
|
99
|
+
Object.defineProperty(o, name, { value, writable: true, enumerable: true, configurable: true });
|
|
100
|
+
else
|
|
101
|
+
o[name] = value;
|
|
102
|
+
}
|
|
103
|
+
/** How to read the values of one schema entry. */
|
|
104
|
+
class Plan {
|
|
105
|
+
/** Makes what create and fill use, once the properties are known: the template, and with generateCode their code. */
|
|
106
|
+
prepare(generateCode) {
|
|
107
|
+
if (this.cls === undefined) {
|
|
108
|
+
const o = {};
|
|
109
|
+
for (let t = 0; t < this.names.length; t++)
|
|
110
|
+
setOwn(o, this.names[t], this.values[t]);
|
|
111
|
+
this.template = o;
|
|
112
|
+
}
|
|
113
|
+
if (!generateCode)
|
|
114
|
+
return;
|
|
115
|
+
const code = generate(this.names, this.reads, this.cls === undefined);
|
|
116
|
+
this.createFn = code?.create;
|
|
117
|
+
this.createTypedFn = code?.createTyped;
|
|
118
|
+
this.fillFn = code?.fill;
|
|
119
|
+
this.readArrayFn = this.generateReadArray();
|
|
120
|
+
}
|
|
121
|
+
/** readArrayFn for plain objects and the models (not for DecodeOptions.classes: their constructors may do more than set fields). */
|
|
122
|
+
generateReadArray() {
|
|
123
|
+
const names = this.names;
|
|
124
|
+
if (this.cls === undefined)
|
|
125
|
+
return generateReadArray(names, this.reads, this.values, this.makers, names.map((_, t) => t), undefined);
|
|
126
|
+
if (!this.model)
|
|
127
|
+
return undefined;
|
|
128
|
+
// The generated classes only initialize their fields: the same fields with the same values in the same order, without new of the class.
|
|
129
|
+
const cls = this.cls, probe = new cls();
|
|
130
|
+
const defaults = this.values.slice(), makers = this.makers.slice(), order = [];
|
|
131
|
+
for (const name of Object.keys(probe)) {
|
|
132
|
+
const t = this.indexOf(name), initial = initialOf(cls, probe, name);
|
|
133
|
+
if (t < 0)
|
|
134
|
+
order.push({ name, ...initial });
|
|
135
|
+
else {
|
|
136
|
+
order.push(t);
|
|
137
|
+
defaults[t] = initial.value;
|
|
138
|
+
makers[t] = initial.make;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (let t = 0; t < this.missing.length; t++)
|
|
142
|
+
order.push(this.missing[t]); // after the class's fields, as create sets them
|
|
143
|
+
return generateReadArray(names, this.reads, defaults, makers, order, cls.prototype);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* @param names the names of the properties in JavaScript (DecodeOptions.propertyNames)
|
|
147
|
+
* @param cls the class of the entry (a model, or one of DecodeOptions.classes)
|
|
148
|
+
* @param model the class is a model: it has $type on its prototype
|
|
149
|
+
*/
|
|
150
|
+
constructor(entry, names, cls, model = false) {
|
|
151
|
+
this.entry = entry;
|
|
152
|
+
this.cls = cls;
|
|
153
|
+
this.model = model;
|
|
154
|
+
/** The properties the class does not initialize (they get the defaults of the schema: e.g. a property the server has and the models do not have yet). */
|
|
155
|
+
this.missing = [];
|
|
156
|
+
const count = entry.properties.length;
|
|
157
|
+
this.names = names;
|
|
158
|
+
this.reads = new Array(count);
|
|
159
|
+
this.values = new Array(count);
|
|
160
|
+
this.makers = new Array(count);
|
|
161
|
+
this.isObject = entry.content === undefined;
|
|
162
|
+
if (cls !== undefined) {
|
|
163
|
+
const probe = new cls(); // the same fields for every instance
|
|
164
|
+
for (let t = 0; t < count; t++)
|
|
165
|
+
if (probe[this.names[t]] === undefined)
|
|
166
|
+
this.missing.push(t);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** A new object with every property at its default (null for the ones that are made, see complete), in the order of the schema. */
|
|
170
|
+
create() {
|
|
171
|
+
const names = this.names, values = this.values;
|
|
172
|
+
if (this.createFn !== undefined)
|
|
173
|
+
return this.createFn(values, null);
|
|
174
|
+
if (this.template !== undefined)
|
|
175
|
+
return { ...this.template }; // own properties, also one named __proto__
|
|
176
|
+
const o = new this.cls(); // new rather than a copy of one instance: the initial values (Date, arrays, nested objects) are not shared
|
|
177
|
+
const missing = this.missing;
|
|
178
|
+
for (let t = 0; t < missing.length; t++)
|
|
179
|
+
setOwn(o, names[missing[t]], values[missing[t]]);
|
|
180
|
+
return o;
|
|
181
|
+
}
|
|
182
|
+
/** create for an object that came with a type id: with its type name in $type (the first property; on instances of your classes after their own). */
|
|
183
|
+
createTyped() {
|
|
184
|
+
const typeName = this.entry.name;
|
|
185
|
+
if (typeName === null)
|
|
186
|
+
return this.create(); // the root entry of a described schema (type ids never refer to it) or a nameless entry
|
|
187
|
+
if (this.createTypedFn !== undefined)
|
|
188
|
+
return this.createTypedFn(this.values, typeName);
|
|
189
|
+
if (this.template !== undefined)
|
|
190
|
+
return { [TypeProperty]: typeName, ...this.template };
|
|
191
|
+
const o = this.create();
|
|
192
|
+
if (!this.model)
|
|
193
|
+
o[TypeProperty] = typeName; // a model has it on its prototype
|
|
194
|
+
return o;
|
|
195
|
+
}
|
|
196
|
+
/** Makes the defaults the data did not replace (a Date or struct is never null, so null means not read). */
|
|
197
|
+
complete(o) {
|
|
198
|
+
const made = this.made;
|
|
199
|
+
if (made === undefined)
|
|
200
|
+
return o;
|
|
201
|
+
for (let t = 0; t < made.length; t++) {
|
|
202
|
+
const name = this.names[made[t]];
|
|
203
|
+
if (o[name] === null)
|
|
204
|
+
o[name] = this.makers[made[t]]();
|
|
205
|
+
}
|
|
206
|
+
return o;
|
|
207
|
+
}
|
|
208
|
+
/** A new object with every default made (e.g. the default of a struct). */
|
|
209
|
+
createComplete() {
|
|
210
|
+
return this.complete(this.create());
|
|
211
|
+
}
|
|
212
|
+
indexOf(name) {
|
|
213
|
+
if (this._byName === undefined) {
|
|
214
|
+
this._byName = new Map();
|
|
215
|
+
for (let t = this.names.length - 1; t >= 0; t--)
|
|
216
|
+
this._byName.set(this.names[t], t);
|
|
217
|
+
}
|
|
218
|
+
return this._byName.get(name) ?? -1;
|
|
219
|
+
}
|
|
220
|
+
/** The index of a property by the name the writer gave it (a key of ObjectLayout.Map without the schema): the .NET name. */
|
|
221
|
+
indexOfWriterName(name) {
|
|
222
|
+
const properties = this.entry.properties;
|
|
223
|
+
if (this._byWriterName === undefined) {
|
|
224
|
+
this._byWriterName = new Map();
|
|
225
|
+
for (let t = properties.length - 1; t >= 0; t--)
|
|
226
|
+
this._byWriterName.set(properties[t].name, t);
|
|
227
|
+
}
|
|
228
|
+
return this._byWriterName.get(name) ?? -1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** The plans of one schema for one set of options, made when first used. */
|
|
232
|
+
class Plans {
|
|
233
|
+
constructor(schema, options) {
|
|
234
|
+
this.schema = schema;
|
|
235
|
+
this.options = options;
|
|
236
|
+
this.inline = new Map();
|
|
237
|
+
this.field = nameFunction(options.propertyNames);
|
|
238
|
+
this.keyName = this.field('Key');
|
|
239
|
+
this.valueName = this.field('Value');
|
|
240
|
+
this.plans = new Array(schema.entries.length);
|
|
241
|
+
this.root = schema.root === undefined ? readDynamic : this.compile(schema.root);
|
|
242
|
+
}
|
|
243
|
+
plan(index) {
|
|
244
|
+
const plan = this.plans[index];
|
|
245
|
+
if (plan !== undefined)
|
|
246
|
+
return plan;
|
|
247
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.plans.length)
|
|
248
|
+
throw new MsgPackError(`The type id ${index} is not an entry of the schema (${this.plans.length} entries).`);
|
|
249
|
+
return this.build(index);
|
|
250
|
+
}
|
|
251
|
+
/** The plan of an entry by index, or of a struct described inline. */
|
|
252
|
+
planOf(entry) {
|
|
253
|
+
if (typeof entry === 'number')
|
|
254
|
+
return this.plan(entry);
|
|
255
|
+
const plan = this.inline.get(entry);
|
|
256
|
+
return plan ?? this.build(-1, entry);
|
|
257
|
+
}
|
|
258
|
+
build(index, entry = this.schema.entries[index]) {
|
|
259
|
+
const model = entry.name === null ? undefined : this.options.models?.byName(entry.name);
|
|
260
|
+
const plan = new Plan(entry, entry.properties.map((p) => this.field(p.name)), model?.ctor ?? (entry.name === null ? undefined : this.options.classes?.[entry.name]), model !== undefined);
|
|
261
|
+
if (index >= 0)
|
|
262
|
+
this.plans[index] = plan; // before the properties: they may refer to this entry
|
|
263
|
+
else
|
|
264
|
+
this.inline.set(entry, plan);
|
|
265
|
+
entry.properties.forEach((p, t) => {
|
|
266
|
+
const type = p.type ?? model?.byName.get(p.name) ?? AnyType; // a plain schema has names only: the models know the types
|
|
267
|
+
plan.reads[t] = this.compile(type);
|
|
268
|
+
const d = p.hasDefault ? this.convertDefault(p.default, type) : this.defaultOf(type);
|
|
269
|
+
plan.values[t] = d.make === undefined ? d.value : null;
|
|
270
|
+
plan.makers[t] = d.make;
|
|
271
|
+
if (d.make !== undefined && (plan.cls === undefined || plan.missing.includes(t)))
|
|
272
|
+
(plan.made ?? (plan.made = [])).push(t);
|
|
273
|
+
});
|
|
274
|
+
if (entry.content !== undefined)
|
|
275
|
+
plan.content = this.compile(entry.content);
|
|
276
|
+
plan.prepare(this.options.generateCode);
|
|
277
|
+
return plan;
|
|
278
|
+
}
|
|
279
|
+
/** A read function for values of the type (each handles nil and values wrapped with a type id). */
|
|
280
|
+
compile(type) {
|
|
281
|
+
switch (type.k) {
|
|
282
|
+
case 'kind': return this.kind(type.kind);
|
|
283
|
+
case 'nullable': return this.compile(type.of);
|
|
284
|
+
case 'object': {
|
|
285
|
+
const entry = type.entry;
|
|
286
|
+
let plan; // not when compiled: the entry may refer to itself
|
|
287
|
+
return (r) => r.readObject(plan ?? (plan = this.planOf(entry)));
|
|
288
|
+
}
|
|
289
|
+
case 'list': {
|
|
290
|
+
const element = this.compile(type.of);
|
|
291
|
+
return (r) => r.readList(element);
|
|
292
|
+
}
|
|
293
|
+
case 'dict': {
|
|
294
|
+
const key = this.compile(type.key), value = this.compile(type.value);
|
|
295
|
+
return (r) => r.readDictionary(key, value);
|
|
296
|
+
}
|
|
297
|
+
case 'pair': {
|
|
298
|
+
const key = this.compile(type.key), value = this.compile(type.value), keyName = this.keyName, valueName = this.valueName;
|
|
299
|
+
return (r) => {
|
|
300
|
+
const items = r.readItems([key, value]);
|
|
301
|
+
return Array.isArray(items) ? { [keyName]: items[0], [valueName]: items[1] } : items;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
case 'tuple': {
|
|
305
|
+
const items = type.items.map((t) => this.compile(t));
|
|
306
|
+
return (r) => r.readItems(items);
|
|
307
|
+
}
|
|
308
|
+
case 'model': { // a class of the models: the entry of its name (none: the value is nil, or has a type id)
|
|
309
|
+
const index = this.schema.indexOf(type.name);
|
|
310
|
+
return index < 0 ? readDynamic : (r) => r.readObject(this.plan(index));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** The kinds the formatters write most have their usual format read in place, without the dispatch of value(). */
|
|
315
|
+
kind(kind) {
|
|
316
|
+
switch (kind) {
|
|
317
|
+
case 'guid': return readGuid;
|
|
318
|
+
case 'string': return readString;
|
|
319
|
+
case 'int': return readInt;
|
|
320
|
+
case 'decimal': {
|
|
321
|
+
const mode = this.options.decimal, convert = this.converter(kind);
|
|
322
|
+
return (r) => {
|
|
323
|
+
const bytes = r.bytes, p = r.pos;
|
|
324
|
+
if (bytes[p] === 0xd8 && bytes[p + 1] === 1 && p + 18 <= bytes.length) { // fixext 16 of type 1
|
|
325
|
+
r.pos = p + 18;
|
|
326
|
+
return decodeDecimal(bytes, p + 2, mode);
|
|
327
|
+
}
|
|
328
|
+
return convert(r.value());
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
case 'datetime':
|
|
332
|
+
if (this.options.dates === 'iso')
|
|
333
|
+
return (r) => {
|
|
334
|
+
const v = r.readDate();
|
|
335
|
+
return v instanceof Date ? v.toISOString() : v;
|
|
336
|
+
};
|
|
337
|
+
return (r) => r.readDate();
|
|
338
|
+
}
|
|
339
|
+
const convert = this.converter(kind);
|
|
340
|
+
return convert === undefined ? readDynamic : (r) => convert(r.value());
|
|
341
|
+
}
|
|
342
|
+
/** Converts a value of the kind as it was read (undefined: no conversion). Values in another form are returned as they are. */
|
|
343
|
+
converter(kind) {
|
|
344
|
+
const o = this.options;
|
|
345
|
+
switch (kind) {
|
|
346
|
+
case 'long':
|
|
347
|
+
case 'ulong':
|
|
348
|
+
return o.int64 === 'bigint'
|
|
349
|
+
? (v) => (typeof v === 'number' ? BigInt(v) : v)
|
|
350
|
+
: (v) => (typeof v === 'bigint' ? Number(v) : v);
|
|
351
|
+
case 'bigint': return (v) => (typeof v === 'number' ? BigInt(v) : v);
|
|
352
|
+
case 'char': return (v) => (typeof v === 'number' ? String.fromCharCode(v) : v);
|
|
353
|
+
case 'decimal': return (v) => (v instanceof MsgPackExtension && v.type === 1 && v.data.length === 16 ? decodeDecimal(v.data, 0, o.decimal) : convertDecimal(v, o.decimal));
|
|
354
|
+
case 'guid': return (v) => (v instanceof Uint8Array && v.length === 16 ? guidString(v) : v);
|
|
355
|
+
case 'datetime': return o.dates === 'iso' ? (v) => (v instanceof Date ? v.toISOString() : v) : undefined;
|
|
356
|
+
case 'dto':
|
|
357
|
+
case 'dto-clock': {
|
|
358
|
+
const clock = kind === 'dto-clock';
|
|
359
|
+
const iso = o.dates === 'iso';
|
|
360
|
+
return (v) => {
|
|
361
|
+
if (v instanceof Date)
|
|
362
|
+
return iso ? offsetText(v, 0) : v; // a bare timestamp (DateTimeOffsetFormat.Timestamp)
|
|
363
|
+
if (!Array.isArray(v) || v.length !== 2 || !(v[0] instanceof Date) || typeof v[1] !== 'number')
|
|
364
|
+
return v;
|
|
365
|
+
const moment = clock ? new Date(v[0].getTime() - v[1] * 60000) : v[0];
|
|
366
|
+
return iso ? offsetText(moment, v[1]) : moment;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
case 'timespan': return (v) => (typeof v === 'number' || typeof v === 'bigint' ? timeSpanText(v) : v);
|
|
370
|
+
case 'dateonly': return (v) => (typeof v === 'number' ? dateOnlyText(v) : v);
|
|
371
|
+
case 'timeonly': return (v) => (typeof v === 'number' || typeof v === 'bigint' ? timeOnlyText(v) : v);
|
|
372
|
+
}
|
|
373
|
+
return undefined; // string, bool, int, float, bin, uri, any: as read
|
|
374
|
+
}
|
|
375
|
+
/** The value of a property the writer left out: the default of its type. */
|
|
376
|
+
defaultOf(type) {
|
|
377
|
+
const o = this.options;
|
|
378
|
+
switch (type.k) {
|
|
379
|
+
case 'kind':
|
|
380
|
+
switch (type.kind) {
|
|
381
|
+
case 'bool': return { value: false };
|
|
382
|
+
case 'int':
|
|
383
|
+
case 'float': return { value: 0 };
|
|
384
|
+
case 'long':
|
|
385
|
+
case 'ulong': return { value: o.int64 === 'bigint' ? 0n : 0 };
|
|
386
|
+
case 'bigint': return { value: 0n };
|
|
387
|
+
case 'decimal': return { value: convertDecimal(0, o.decimal) };
|
|
388
|
+
case 'char': return { value: '\0' };
|
|
389
|
+
case 'guid': return { value: EmptyGuid };
|
|
390
|
+
case 'datetime': return o.dates === 'iso' ? { value: new Date(MinDateTimeMs).toISOString() } : { value: null, make: () => new Date(MinDateTimeMs) };
|
|
391
|
+
case 'dto':
|
|
392
|
+
case 'dto-clock': return o.dates === 'iso' ? { value: offsetText(new Date(MinDateTimeMs), 0) } : { value: null, make: () => new Date(MinDateTimeMs) };
|
|
393
|
+
case 'timespan': return { value: '00:00:00' };
|
|
394
|
+
case 'dateonly': return { value: '0001-01-01' };
|
|
395
|
+
case 'timeonly': return { value: '00:00:00' };
|
|
396
|
+
}
|
|
397
|
+
return { value: null };
|
|
398
|
+
case 'object':
|
|
399
|
+
if (type.struct) {
|
|
400
|
+
const entry = type.entry;
|
|
401
|
+
return { value: null, make: () => this.planOf(entry).createComplete() };
|
|
402
|
+
}
|
|
403
|
+
return { value: null };
|
|
404
|
+
case 'pair': {
|
|
405
|
+
const key = this.defaultOf(type.key), value = this.defaultOf(type.value), keyName = this.keyName, valueName = this.valueName;
|
|
406
|
+
return { value: null, make: () => ({ [keyName]: key.make ? key.make() : key.value, [valueName]: value.make ? value.make() : value.value }) };
|
|
407
|
+
}
|
|
408
|
+
case 'model': {
|
|
409
|
+
if (!type.struct)
|
|
410
|
+
return { value: null };
|
|
411
|
+
const ctor = this.options.models?.byName(type.name)?.ctor;
|
|
412
|
+
return ctor === undefined ? { value: null } : { value: null, make: () => new ctor() };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { value: null }; // classes, nullables, collections, tuples
|
|
416
|
+
}
|
|
417
|
+
/** A [DefaultValue] of the schema (as read: decimals raw), converted as the values of the property are. */
|
|
418
|
+
convertDefault(raw, type) {
|
|
419
|
+
const kind = type.k === 'nullable' ? type.of : type;
|
|
420
|
+
const convert = kind.k === 'kind' ? this.converter(kind.kind) : undefined;
|
|
421
|
+
const value = convert === undefined ? raw : convert(raw);
|
|
422
|
+
if (value instanceof Date)
|
|
423
|
+
return { value: null, make: () => new Date(value.getTime()) };
|
|
424
|
+
return { value };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const readDynamic = (r) => r.value();
|
|
428
|
+
/** A string: fixstr and str8 read in place. */
|
|
429
|
+
const readString = (r) => {
|
|
430
|
+
const bytes = r.bytes, p = r.pos, b = bytes[p];
|
|
431
|
+
if (b >= 0xa0 && b <= 0xbf) {
|
|
432
|
+
r.pos = p + 1;
|
|
433
|
+
return r.str(b & 0x1f);
|
|
434
|
+
}
|
|
435
|
+
if (b === 0xd9 && p + 1 < bytes.length) {
|
|
436
|
+
r.pos = p + 2;
|
|
437
|
+
return r.str(bytes[p + 1]);
|
|
438
|
+
}
|
|
439
|
+
return r.value();
|
|
440
|
+
};
|
|
441
|
+
/** An int (also enums): a positive fixint read in place. */
|
|
442
|
+
const readInt = (r) => {
|
|
443
|
+
const b = r.bytes[r.pos];
|
|
444
|
+
if (b <= 0x7f) {
|
|
445
|
+
r.pos++;
|
|
446
|
+
return b;
|
|
447
|
+
}
|
|
448
|
+
return r.value();
|
|
449
|
+
};
|
|
450
|
+
/** A Guid: bin 16 read in place (no copy of the bytes), any other form as the converter does. */
|
|
451
|
+
const readGuid = (r) => {
|
|
452
|
+
const bytes = r.bytes, p = r.pos;
|
|
453
|
+
if (bytes[p] === 0xc4 && bytes[p + 1] === 16 && p + 18 <= bytes.length) {
|
|
454
|
+
r.pos = p + 18;
|
|
455
|
+
return guidString(bytes, p + 2);
|
|
456
|
+
}
|
|
457
|
+
const v = r.value();
|
|
458
|
+
return v instanceof Uint8Array && v.length === 16 ? guidString(v) : v;
|
|
459
|
+
};
|
|
460
|
+
/** The reader of a body: values wrapped with a type id ({ "": index, "@": content }, or { "": index, 0: value, ... }) become what their schema entry describes. */
|
|
461
|
+
export class SchemaReader extends MsgPackReader {
|
|
462
|
+
constructor(bytes, options, plans, pos) {
|
|
463
|
+
super(bytes, options, pos);
|
|
464
|
+
this.plans = plans;
|
|
465
|
+
}
|
|
466
|
+
/** The first key of the map whose header was just read is "" (a type id). */
|
|
467
|
+
firstKeyIsTypeId() {
|
|
468
|
+
const k = this.bytes[this.pos];
|
|
469
|
+
return k === 0xa0 || (k === 0xd9 && this.bytes[this.pos + 1] === 0);
|
|
470
|
+
}
|
|
471
|
+
map(n) {
|
|
472
|
+
if (this.plans !== null && n > 0 && this.firstKeyIsTypeId())
|
|
473
|
+
return this.wrapped(n);
|
|
474
|
+
return super.map(n);
|
|
475
|
+
}
|
|
476
|
+
/** A value wrapped with a type id, the header (n entries) is read. */
|
|
477
|
+
wrapped(n) {
|
|
478
|
+
this.pos += this.bytes[this.pos] === 0xa0 ? 1 : 2; // the key ""
|
|
479
|
+
const index = this.value();
|
|
480
|
+
if (typeof index !== 'number')
|
|
481
|
+
throw new MsgPackError('A type id is not a schema index.', this.pos);
|
|
482
|
+
const plan = this.plans.plan(index);
|
|
483
|
+
this.enter();
|
|
484
|
+
let result = null;
|
|
485
|
+
if (plan.isObject) {
|
|
486
|
+
const o = plan.createTyped();
|
|
487
|
+
for (let t = 1; t < n; t++) {
|
|
488
|
+
const key = this.value();
|
|
489
|
+
if (key === '@') { // the array layout: { "": index, "@": [values] }
|
|
490
|
+
const count = this.arrayLength();
|
|
491
|
+
if (count >= 0)
|
|
492
|
+
this.fill(plan, o, count);
|
|
493
|
+
else
|
|
494
|
+
this.value();
|
|
495
|
+
}
|
|
496
|
+
else
|
|
497
|
+
this.setByKey(plan, o, key);
|
|
498
|
+
}
|
|
499
|
+
result = plan.complete(o);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
for (let t = 1; t < n; t++) {
|
|
503
|
+
const key = this.value();
|
|
504
|
+
if (key === '@')
|
|
505
|
+
result = plan.content(this);
|
|
506
|
+
else
|
|
507
|
+
this.value(); // a property of a collection ([SerializeEnumerable(SerializeProperties)]): not kept, a JS array has no place for it
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
this.depth--;
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
/** An object of the plan: an array of its values (the array layout), a map from property index to value, or a value with a type id. */
|
|
514
|
+
readObject(plan) {
|
|
515
|
+
const b = this.peek();
|
|
516
|
+
if (b === 0xc0) {
|
|
517
|
+
this.pos++;
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
let n = this.arrayLength();
|
|
521
|
+
if (n >= 0) {
|
|
522
|
+
this.enter();
|
|
523
|
+
const readArray = plan.readArrayFn;
|
|
524
|
+
if (readArray !== undefined) {
|
|
525
|
+
const o = readArray(this, n);
|
|
526
|
+
this.depth--;
|
|
527
|
+
return o;
|
|
528
|
+
}
|
|
529
|
+
const o = plan.create();
|
|
530
|
+
this.fill(plan, o, n);
|
|
531
|
+
this.depth--;
|
|
532
|
+
return plan.complete(o);
|
|
533
|
+
}
|
|
534
|
+
n = this.mapLength();
|
|
535
|
+
if (n < 0)
|
|
536
|
+
return this.value(); // not an object: as it is
|
|
537
|
+
if (n > 0 && this.firstKeyIsTypeId())
|
|
538
|
+
return this.wrapped(n);
|
|
539
|
+
this.enter();
|
|
540
|
+
const o = plan.create();
|
|
541
|
+
for (let t = 0; t < n; t++)
|
|
542
|
+
this.setByKey(plan, o, this.value());
|
|
543
|
+
this.depth--;
|
|
544
|
+
return plan.complete(o);
|
|
545
|
+
}
|
|
546
|
+
/** Values by position, nil leaves the default (the writer left the value out), values beyond the entry are skipped. */
|
|
547
|
+
fill(plan, o, n) {
|
|
548
|
+
if (plan.fillFn !== undefined)
|
|
549
|
+
return plan.fillFn(this, o, n);
|
|
550
|
+
const names = plan.names, reads = plan.reads, count = names.length, bytes = this.bytes;
|
|
551
|
+
for (let k = 0; k < n; k++) {
|
|
552
|
+
if (bytes[this.pos] === 0xc0) {
|
|
553
|
+
this.pos++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (k < count)
|
|
557
|
+
setOwn(o, names[k], reads[k](this));
|
|
558
|
+
else
|
|
559
|
+
this.value();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** ObjectLayout.Map: the key is the index of the property in the entry (or its name). */
|
|
563
|
+
setByKey(plan, o, key) {
|
|
564
|
+
const index = typeof key === 'number' ? key : typeof key === 'string' ? plan.indexOfWriterName(key) : -1;
|
|
565
|
+
if (index >= 0 && index < plan.names.length)
|
|
566
|
+
setOwn(o, plan.names[index], plan.reads[index](this));
|
|
567
|
+
else
|
|
568
|
+
this.value();
|
|
569
|
+
}
|
|
570
|
+
readList(element) {
|
|
571
|
+
const b = this.peek();
|
|
572
|
+
if (b === 0xc0) {
|
|
573
|
+
this.pos++;
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const n = this.arrayLength();
|
|
577
|
+
if (n < 0)
|
|
578
|
+
return this.value(); // wrapped with a type id, or not a list
|
|
579
|
+
this.enter();
|
|
580
|
+
const result = new Array(n);
|
|
581
|
+
for (let t = 0; t < n; t++)
|
|
582
|
+
result[t] = element(this);
|
|
583
|
+
this.depth--;
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
readDictionary(key, value) {
|
|
587
|
+
const b = this.peek();
|
|
588
|
+
if (b === 0xc0) {
|
|
589
|
+
this.pos++;
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (this.startsWithTypeId())
|
|
593
|
+
return this.value();
|
|
594
|
+
const n = this.mapLength();
|
|
595
|
+
if (n < 0)
|
|
596
|
+
return this.value();
|
|
597
|
+
this.enter();
|
|
598
|
+
const result = {};
|
|
599
|
+
for (let t = 0; t < n; t++) {
|
|
600
|
+
const k = key(this);
|
|
601
|
+
setOwn(result, typeof k === 'string' ? k : String(k), value(this));
|
|
602
|
+
}
|
|
603
|
+
this.depth--;
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
/** An array of items of the given types (pairs, tuples), extra items as they are. */
|
|
607
|
+
readItems(items) {
|
|
608
|
+
const b = this.peek();
|
|
609
|
+
if (b === 0xc0) {
|
|
610
|
+
this.pos++;
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const n = this.arrayLength();
|
|
614
|
+
if (n < 0)
|
|
615
|
+
return this.value();
|
|
616
|
+
this.enter();
|
|
617
|
+
const result = new Array(n);
|
|
618
|
+
for (let t = 0; t < n; t++)
|
|
619
|
+
result[t] = t < items.length ? items[t](this) : this.value();
|
|
620
|
+
this.depth--;
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
/** A DateTime: the timestamps of 4 and 8 bytes read in place. */
|
|
624
|
+
readDate() {
|
|
625
|
+
const bytes = this.bytes, p = this.pos, b = bytes[p];
|
|
626
|
+
if (bytes[p + 1] === 0xff && (b === 0xd6 || b === 0xd7)) {
|
|
627
|
+
const length = b === 0xd6 ? 4 : 8;
|
|
628
|
+
if (p + 2 + length <= bytes.length) {
|
|
629
|
+
this.pos = p + 2;
|
|
630
|
+
return this.timestamp(length);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return this.value();
|
|
634
|
+
}
|
|
635
|
+
readRoot() {
|
|
636
|
+
return this.plans === null ? this.value() : this.plans.root(this);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/** The models' TypeScript types assume the options they were generated with. */
|
|
640
|
+
function checkModelOptions(models, o) {
|
|
641
|
+
const differs = (name, model, actual) => {
|
|
642
|
+
if (model !== undefined && model !== actual)
|
|
643
|
+
throw new Error(`The models (${models.generator ?? 'lsmsgpack-ts'}) were generated for ${name}: '${model}', the client reads with '${typeof actual === 'function' ? 'a function' : actual}'. Generate them with the same option, or leave it out here.`);
|
|
644
|
+
};
|
|
645
|
+
differs('int64', models.int64, o.int64);
|
|
646
|
+
differs('dates', models.dates, o.dates);
|
|
647
|
+
if (typeof o.decimal !== 'function')
|
|
648
|
+
differs('decimal', models.decimal, o.decimal); // a function of the text: the user's own type
|
|
649
|
+
differs('propertyNames', models.propertyNames, o.propertyNames); // the field names of the classes
|
|
650
|
+
}
|
|
651
|
+
/** Decodes payloads with the given options (one per client). Thread-safe in the sense of JavaScript: no state between calls except the plans per schema. */
|
|
652
|
+
export class Decoder {
|
|
653
|
+
constructor(options = {}) {
|
|
654
|
+
this.plans = new WeakMap();
|
|
655
|
+
const models = options.models;
|
|
656
|
+
this.options = {
|
|
657
|
+
decimal: options.decimal ?? models?.decimal ?? 'number',
|
|
658
|
+
int64: options.int64 ?? models?.int64 ?? 'number',
|
|
659
|
+
dates: options.dates ?? models?.dates ?? 'date',
|
|
660
|
+
models,
|
|
661
|
+
classes: options.classes,
|
|
662
|
+
propertyNames: options.propertyNames ?? models?.propertyNames ?? 'dotnet',
|
|
663
|
+
generateCode: options.generateCode ?? true,
|
|
664
|
+
};
|
|
665
|
+
if (models !== undefined)
|
|
666
|
+
checkModelOptions(models, this.options);
|
|
667
|
+
this.readerOptions = { decimal: this.options.decimal, maxDepth: options.maxDepth };
|
|
668
|
+
this.indexedSchema = options.indexedSchema ?? true;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Decodes application/x-lsmsgpack data: the schema (or a reference to it) followed by the body.
|
|
672
|
+
* @param lookup finds a schema the data refers to by its id
|
|
673
|
+
* @param inline makes the schema of the inline bytes (e.g. the known instance when the bytes are the same), default: parses them
|
|
674
|
+
*/
|
|
675
|
+
decode(bytes, lookup, inline) {
|
|
676
|
+
if (bytes.length === 0)
|
|
677
|
+
return { value: undefined, reference: false };
|
|
678
|
+
if (!this.indexedSchema || bytes[0] === 0xc0) // null is written without a schema
|
|
679
|
+
return { value: this.read(bytes, null, 0), reference: false };
|
|
680
|
+
if (bytes[0] === 0xd8 && bytes[1] === 2) { // a reference: fixext 16 of type 2 with the id
|
|
681
|
+
if (bytes.length < 18)
|
|
682
|
+
throw new MsgPackError('Unexpected end of data.', bytes.length);
|
|
683
|
+
const id = toHex(bytes, 2, 18);
|
|
684
|
+
const schema = lookup?.(id);
|
|
685
|
+
if (schema === undefined)
|
|
686
|
+
throw new MissingSchemaError(id);
|
|
687
|
+
return { value: this.read(bytes, schema, 18), schema, reference: true };
|
|
688
|
+
}
|
|
689
|
+
const b = bytes[0];
|
|
690
|
+
if (!((b >= 0x80 && b <= 0x8f) || b === 0xde || b === 0xdf))
|
|
691
|
+
throw new MsgPackError(`The data does not start with a schema (0x${b.toString(16)}), set indexedSchema to false for data without it.`, 0);
|
|
692
|
+
const skip = new MsgPackReader(bytes, { decimal: 'raw' });
|
|
693
|
+
skip.skip();
|
|
694
|
+
const schemaBytes = bytes.subarray(0, skip.pos);
|
|
695
|
+
const schema = inline ? inline(schemaBytes) : new Schema(schemaBytes);
|
|
696
|
+
return { value: this.read(bytes, schema, skip.pos), schema, reference: false };
|
|
697
|
+
}
|
|
698
|
+
/** Plain MsgPack (application/msgpack): any value, maps as objects. */
|
|
699
|
+
decodePlain(bytes) {
|
|
700
|
+
if (bytes.length === 0)
|
|
701
|
+
return undefined;
|
|
702
|
+
return this.read(bytes, null, 0);
|
|
703
|
+
}
|
|
704
|
+
read(bytes, schema, pos) {
|
|
705
|
+
const reader = new SchemaReader(bytes, this.readerOptions, schema === null ? null : this.plansOf(schema), pos);
|
|
706
|
+
const value = reader.readRoot();
|
|
707
|
+
if (reader.pos !== bytes.length)
|
|
708
|
+
throw new MsgPackError('Data after the value.', reader.pos);
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
plansOf(schema) {
|
|
712
|
+
let plans = this.plans.get(schema);
|
|
713
|
+
if (plans === undefined) {
|
|
714
|
+
plans = new Plans(schema, this.options);
|
|
715
|
+
this.plans.set(schema, plans);
|
|
716
|
+
}
|
|
717
|
+
return plans;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=decoder.js.map
|