seroval 0.1.0 → 0.2.0

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/src/index.ts CHANGED
@@ -1,479 +1,95 @@
1
- import { forEach, join } from './array';
2
- import getIdentifier from './get-identifier';
3
- import quote from './quote';
4
-
5
- const EMPTY_SET = 'new Set';
6
- const EMPTY_MAP = 'new Map';
7
- const EMPTY_ARRAY = '[]';
8
-
9
- // Values that are non-recursive
10
- export type PrimitiveValue =
11
- | boolean
12
- | string
13
- | number
14
- | undefined
15
- | null
16
- | bigint
17
- | Date
18
- | RegExp;
19
-
20
- export type ServerValue =
21
- | PrimitiveValue
22
- | Array<ServerValue>
23
- | { [key: string | number]: ServerValue }
24
- | Set<ServerValue>
25
- | Map<ServerValue, ServerValue>;
26
-
27
- // Reference counter
28
- const REF_COUNT = new Map<ServerValue, number>();
29
-
30
- const STACK = new Set();
31
-
32
- // Value-to-ref map
33
- const REFS = new Map<ServerValue, number>();
34
-
35
- // Set to check if ref id already has an assigned value
36
- const ASSIGNED_REFS = new Set<number>();
37
-
38
- // Variables
39
- let VARS: string[] = [];
40
-
41
- // Array of assignments to be done (used for recursion)
42
- type Assignment = [source: string, value: string];
43
- let ASSIGNMENTS: Assignment[] = [];
44
-
45
- // Array of Map.prototype.set calls
46
- type MapSet = [source: string, key: string, value: string];
47
- let MAP_SETS: MapSet[] = [];
48
-
49
- // Array of Set.prototype.add calls
50
- type SetAdd = [source: string, value: string];
51
- let SET_ADDS: SetAdd[] = [];
52
-
53
- function reset() {
54
- ASSIGNMENTS = [];
55
- MAP_SETS = [];
56
- SET_ADDS = [];
57
- VARS = [];
58
- REF_COUNT.clear();
59
- REFS.clear();
60
- ASSIGNED_REFS.clear();
61
- }
62
-
63
- function resolveAssignments() {
64
- if (ASSIGNMENTS.length) {
65
- const result: Record<string, string> = {};
66
-
67
- // Merge all assignments with similar source
68
- forEach(ASSIGNMENTS, ([source, value]) => {
69
- if (value in result) {
70
- result[value] = `${source}=${result[value]}`;
71
- } else {
72
- result[value] = `${source}=${value}`;
73
- }
74
- });
75
-
76
- return `${join(Object.values(result), ',')},`;
77
- }
78
- return '';
79
- }
80
-
81
- function resolveMapSets() {
82
- if (MAP_SETS.length) {
83
- const result: Record<string, string> = {};
84
-
85
- // Merge all assignments with similar source
86
- forEach(MAP_SETS, ([source, key, value]) => {
87
- if (source in result) {
88
- result[source] = `${result[source]}.set(${key},${value})`;
89
- } else {
90
- result[source] = `${source}.set(${key},${value})`;
91
- }
92
- });
93
-
94
- return `${join(Object.values(result), ',')},`;
95
- }
96
- return '';
97
- }
98
-
99
- function resolveSetAdds() {
100
- if (SET_ADDS.length) {
101
- const result: Record<string, string> = {};
102
-
103
- // Merge all assignments with similar source
104
- forEach(SET_ADDS, ([source, value]) => {
105
- if (source in result) {
106
- result[source] = `${result[source]}.add(${value})`;
107
- } else {
108
- result[source] = `${source}.add(${value})`;
109
- }
110
- });
111
-
112
- return `${join(Object.values(result), ',')},`;
113
- }
114
- return '';
115
- }
116
-
117
- function resolvePatches() {
118
- return `${resolveAssignments()}${resolveMapSets()}${resolveSetAdds()}`;
119
- }
120
-
121
- function insertRef(current: ServerValue) {
122
- const count = REF_COUNT.get(current) || 0;
123
- REF_COUNT.set(current, count + 1);
124
- return count === 0;
125
- }
126
-
127
- function constructorCheck<T extends NonNullable<ServerValue>>(
128
- value: NonNullable<ServerValue>,
129
- constructor: unknown,
130
- ): value is T {
131
- return value.constructor === constructor;
132
- }
133
-
134
- function isPrimitive(current: ServerValue): current is PrimitiveValue {
135
- if (!current || current === true) {
136
- return true;
137
- }
138
- const type = typeof current;
139
- const classConst = current.constructor;
140
- return type === 'number'
141
- || type === 'string'
142
- || type === 'bigint'
143
- || classConst === Date
144
- || classConst === RegExp;
145
- }
146
-
147
- function lookupRefs(current: ServerValue) {
148
- if (isPrimitive(current)) {
149
- return;
150
- }
151
- if (Array.isArray(current)) {
152
- if (insertRef(current)) {
153
- forEach(current, lookupRefs);
154
- }
155
- } else if (constructorCheck<Set<ServerValue>>(current, Set)) {
156
- if (insertRef(current)) {
157
- for (const item of current.keys()) {
158
- lookupRefs(item);
159
- }
160
- }
161
- } else if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
162
- if (insertRef(current)) {
163
- for (const [key, value] of current.entries()) {
164
- lookupRefs(key);
165
- lookupRefs(value);
166
- }
1
+ /* eslint-disable no-await-in-loop */
2
+ import { join } from './array';
3
+ import { lookupRefsAsync, traverseAsync } from './async';
4
+ import {
5
+ SerializationContext,
6
+ getRefParam,
7
+ createRef,
8
+ resolvePatches,
9
+ createSerializationContext,
10
+ } from './context';
11
+ import isPrimitive from './is-primitive';
12
+ import serializePrimitive from './serialize-primitive';
13
+ import { lookupRefs, traverseSync } from './sync';
14
+ import {
15
+ AsyncServerValue,
16
+ NonPrimitiveServerValue,
17
+ PrimitiveValue,
18
+ ServerValue,
19
+ } from './types';
20
+
21
+ export { AsyncServerValue, ServerValue, PrimitiveValue };
22
+
23
+ function finalize<T extends ServerValue | AsyncServerValue>(
24
+ ctx: SerializationContext,
25
+ source: NonPrimitiveServerValue<T>,
26
+ result: string,
27
+ ) {
28
+ // Shared references detected
29
+ if (ctx.vars.length) {
30
+ // Get (or create) a ref from the source
31
+ const index = getRefParam(ctx, createRef(ctx, source));
32
+ const patches = resolvePatches(ctx);
33
+ const params = ctx.vars.length > 1
34
+ ? `(${join(ctx.vars, ',')})`
35
+ : ctx.vars[0];
36
+ // Source is probably already assigned
37
+ if (result.startsWith(`${index}=`)) {
38
+ return `(${params}=>(${result},${patches}${index}))()`;
167
39
  }
168
- } else if (current.constructor === Object && insertRef(current)) {
169
- forEach(Object.values(current), lookupRefs);
170
- }
171
- }
172
-
173
- function createRef(value: ServerValue) {
174
- const current = REFS.get(value);
175
- if (current != null) {
176
- return current;
40
+ return `(${params}=>(${index}=${result},${patches}${index}))()`;
177
41
  }
178
- const id = REFS.size;
179
- REFS.set(value, id);
180
- return id;
181
- }
182
-
183
- function getRefParam(index: number) {
184
- if (VARS[index]) {
185
- return VARS[index];
42
+ if (source.constructor === Object) {
43
+ return `(${result})`;
186
44
  }
187
- const result = getIdentifier(index);
188
- VARS[index] = result;
189
45
  return result;
190
46
  }
191
47
 
192
- function assignRef(index: number, value: string) {
193
- return `${getRefParam(index)}=${value}`;
194
- }
195
-
196
- function createAssignment(source: string, value: string) {
197
- ASSIGNMENTS.push([source, value]);
198
- }
199
-
200
- function createSetAdd(ref: number, value: string) {
201
- SET_ADDS.push([getRefParam(ref), value]);
202
- }
203
-
204
- function createMapSet(ref: number, key: string, value: string) {
205
- MAP_SETS.push([getRefParam(ref), key, value]);
206
- }
207
-
208
- function createArrayAssign(ref: number, index: number, value: string) {
209
- createAssignment(`${getRefParam(ref)}[${index}]`, value);
210
- }
211
-
212
- function createObjectIdentifierAssign(ref: number, key: string, value: string) {
213
- createAssignment(`${getRefParam(ref)}.${key}`, value);
214
- }
215
-
216
- function createObjectStringAssign(ref: number, key: string, value: string) {
217
- createAssignment(`${getRefParam(ref)}[${quote(key)}]`, value);
218
- }
219
-
220
- function serializePrimitive(value: PrimitiveValue): string {
221
- // Shortened forms
222
- if (value === true) {
223
- return '!0';
224
- }
225
- if (value === false) {
226
- return '!1';
227
- }
228
- if (value === undefined) {
229
- return 'void 0';
230
- }
231
- if (value === null) {
232
- return 'null';
233
- }
234
- // negative 0 isn't the same as 0
235
- if (Object.is(value, -0)) {
236
- return '-0';
237
- }
238
- if (typeof value === 'bigint') {
239
- return `BigInt("${value}")`;
240
- }
241
- if (typeof value === 'string') {
242
- return quote(value);
243
- }
244
- if (value.constructor === Date) {
245
- return `new Date("${value.toISOString()}")`;
246
- }
247
- return String(value);
248
- }
249
-
250
- function traverse(current: ServerValue): string | { referred: string } {
251
- // Skip primitives
252
- if (isPrimitive(current)) {
253
- return serializePrimitive(current);
254
- }
255
- // Get reference of the current object
256
- let refResult = REFS.get(current);
257
- if (refResult != null) {
258
- // Return the reference if already assigned a value
259
- if (ASSIGNED_REFS.has(refResult)) {
260
- return { referred: getRefParam(refResult) };
261
- }
262
- // Early sign (to prevent recursion)
263
- ASSIGNED_REFS.add(refResult);
264
- }
265
- // Transform Set
266
- if (constructorCheck<Set<ServerValue>>(current, Set)) {
267
- if (current.size) {
268
- const values: string[] = [];
269
-
270
- STACK.add(current);
271
- for (const value of current.keys()) {
272
- if (STACK.has(value)) {
273
- // This object received a ref, we must generate a setter ref
274
- if (refResult == null) {
275
- refResult = createRef(current);
276
- }
277
- // Received a ref, this might be a recursive ref, defer an assignment
278
- createSetAdd(refResult, getRefParam(createRef(value)));
279
- } else {
280
- const result = traverse(value);
281
- if (typeof result === 'string') {
282
- values.push(result);
283
- } else {
284
- values.push(result.referred);
285
- }
286
- }
287
- }
288
- STACK.delete(current);
289
-
290
- const value = values.length ? `new Set([${join(values, ',')}])` : EMPTY_SET;
291
- if (refResult != null) {
292
- return assignRef(refResult, value);
293
- }
294
- return value;
295
- }
296
- return EMPTY_SET;
297
- }
298
- if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
299
- if (current.size) {
300
- const values: string[] = [];
301
-
302
- STACK.add(current);
303
- for (const [key, value] of current.entries()) {
304
- if (STACK.has(key)) {
305
- if (refResult == null) {
306
- refResult = createRef(current);
307
- }
308
- const keyRef = getRefParam(createRef(key));
309
- if (STACK.has(value)) {
310
- createMapSet(refResult, keyRef, getRefParam(createRef(value)));
311
- } else {
312
- const valueResult = traverse(value);
313
- if (typeof valueResult === 'string') {
314
- createMapSet(refResult, keyRef, valueResult);
315
- } else {
316
- createMapSet(refResult, keyRef, valueResult.referred);
317
- }
318
- }
319
- } else if (STACK.has(value)) {
320
- if (refResult == null) {
321
- refResult = createRef(current);
322
- }
323
- const valueRef = getRefParam(createRef(value));
324
- const keyResult = traverse(key);
325
- if (typeof keyResult === 'string') {
326
- createMapSet(refResult, keyResult, valueRef);
327
- } else {
328
- createMapSet(refResult, keyResult.referred, valueRef);
329
- }
330
- } else {
331
- const keyResult = traverse(key);
332
- const valueResult = traverse(value);
333
- if (typeof keyResult === 'string') {
334
- if (typeof valueResult === 'string') {
335
- values.push(`[${keyResult},${valueResult}]`);
336
- } else {
337
- values.push(`[${keyResult},${valueResult.referred}]`);
338
- }
339
- } else {
340
- if (typeof valueResult === 'string') {
341
- values.push(`[${keyResult.referred},${valueResult}]`);
342
- } else {
343
- values.push(`[${keyResult.referred},${valueResult.referred}]`);
344
- }
345
- }
346
- }
347
- }
348
- STACK.delete(current);
349
- const value = values.length ? `new Map([${join(values, ',')}])` : EMPTY_MAP;
350
- if (refResult != null) {
351
- return assignRef(refResult, value);
352
- }
353
- return value;
354
- }
355
- return EMPTY_MAP;
48
+ export function serialize(source: ServerValue) {
49
+ if (isPrimitive(source)) {
50
+ return serializePrimitive(source);
356
51
  }
357
- if (Array.isArray(current)) {
358
- if (current.length) {
359
- let values = '';
360
-
361
- STACK.add(current);
362
- forEach(current, (item, i) => {
363
- if (i in current) {
364
- if (STACK.has(item)) {
365
- if (refResult == null) {
366
- refResult = createRef(current);
367
- }
368
- createArrayAssign(refResult, i, getRefParam(createRef(item)));
369
- values += ',';
370
- } else {
371
- const result = traverse(item);
372
- if (typeof result === 'string') {
373
- values += result;
374
- } else {
375
- values += result.referred;
376
- }
377
- if (i < current.length - 1) {
378
- values += ',';
379
- }
380
- }
381
- } else {
382
- values += ',';
383
- }
384
- });
385
- STACK.delete(current);
386
-
387
- const value = `[${values}]`;
388
- if (refResult != null) {
389
- return assignRef(refResult, value);
390
- }
391
- return value;
52
+ const ctx = createSerializationContext();
53
+ // Lookup possible shared references
54
+ lookupRefs(ctx, source);
55
+ // Add shared references to refs
56
+ for (const [key, value] of ctx.refCount.entries()) {
57
+ if (value > 1) {
58
+ createRef(ctx, key);
392
59
  }
393
- return EMPTY_ARRAY;
394
60
  }
395
- if (current.constructor === Object) {
396
- const values: string[] = [];
397
-
398
- STACK.add(current);
399
- forEach(Object.entries(current), ([key, value]) => {
400
- const check = Number(key);
401
- // Test if key is a valid number or JS identifier
402
- // so that we don't have to serialize the key and wrap with brackets
403
- if (Number.isNaN(check) || /^([$A-Z_][0-9A-Z_$]*)$/i.test(key)) {
404
- if (STACK.has(value)) {
405
- if (refResult == null) {
406
- refResult = createRef(current);
407
- }
408
- createObjectIdentifierAssign(refResult, key, getRefParam(createRef(value)));
409
- } else {
410
- const result = traverse(value);
411
- if (typeof result === 'string') {
412
- values.push(`${key}:${result}`);
413
- } else {
414
- values.push(`${key}:${result.referred}`);
415
- }
416
- }
417
- } else if (STACK.has(value)) {
418
- if (refResult == null) {
419
- refResult = createRef(current);
420
- }
421
- createObjectStringAssign(refResult, key, getRefParam(createRef(value)));
422
- } else {
423
- const result = traverse(value);
424
- if (typeof result === 'string') {
425
- values.push(`${quote(key)}:${result}`);
426
- } else {
427
- values.push(`${quote(key)}:${result.referred}`);
428
- }
429
- }
430
- });
431
- STACK.delete(current);
432
-
433
- const value = `{${join(values, ',')}}`;
434
- if (refResult != null) {
435
- return assignRef(refResult, value);
436
- }
437
- return value;
61
+ // Get top-level serialization
62
+ const result = traverseSync(ctx, source);
63
+ if (typeof result !== 'string') {
64
+ throw new Error('Unreachable error');
438
65
  }
439
- throw new Error('Unserializable value');
66
+ return finalize(ctx, source, result);
440
67
  }
441
68
 
442
- export default function serialize(source: ServerValue) {
69
+ export async function serializeAsync(source: AsyncServerValue) {
443
70
  if (isPrimitive(source)) {
444
71
  return serializePrimitive(source);
445
72
  }
446
- reset();
73
+ const ctx = createSerializationContext();
447
74
  // Lookup possible shared references
448
- lookupRefs(source);
75
+ await lookupRefsAsync(ctx, source);
449
76
  // Add shared references to refs
450
- for (const [key, value] of REF_COUNT.entries()) {
77
+ for (const [key, value] of ctx.refCount.entries()) {
451
78
  if (value > 1) {
452
- createRef(key);
79
+ createRef(ctx, key);
453
80
  }
454
81
  }
455
82
  // Get top-level serialization
456
- const result = traverse(source);
83
+ const result = await traverseAsync(ctx, source);
457
84
  if (typeof result !== 'string') {
458
85
  throw new Error('Unreachable error');
459
86
  }
87
+ return finalize(ctx, source, result);
88
+ }
460
89
 
461
- // Shared references detected
462
- if (VARS.length) {
463
- // Get (or create) a ref from the source
464
- const index = getRefParam(createRef(source));
465
- const patches = resolvePatches();
466
- const params = VARS.length > 1
467
- ? `(${join(VARS, ',')})`
468
- : VARS[0];
469
- // Source is probably already assigned
470
- if (result.startsWith(index)) {
471
- return `(${params}=>(${result},${patches}${index}))()`;
472
- }
473
- return `(${params}=>(${index}=${result},${patches}${index}))()`;
474
- }
475
- if (source.constructor === Object) {
476
- return `(${result})`;
477
- }
478
- return result;
90
+ export function deserialize<T extends AsyncServerValue>(source: string): T {
91
+ // eslint-disable-next-line no-eval
92
+ return (0, eval)(source) as T;
479
93
  }
94
+
95
+ export default serialize;
@@ -0,0 +1,14 @@
1
+ import { PrimitiveValue } from './types';
2
+
3
+ export default function isPrimitive(current: unknown): current is PrimitiveValue {
4
+ if (!current || current === true) {
5
+ return true;
6
+ }
7
+ const type = typeof current;
8
+ const classConst = current.constructor;
9
+ return type === 'number'
10
+ || type === 'string'
11
+ || type === 'bigint'
12
+ || classConst === Date
13
+ || classConst === RegExp;
14
+ }
@@ -0,0 +1,6 @@
1
+ export default function isPromise<T>(obj: unknown): obj is PromiseLike<T> {
2
+ return !!obj
3
+ && (typeof obj === 'object' || typeof obj === 'function')
4
+ && 'then' in obj
5
+ && typeof obj.then === 'function';
6
+ }
@@ -0,0 +1,32 @@
1
+ import quote from './quote';
2
+ import { PrimitiveValue } from './types';
3
+
4
+ export default function serializePrimitive(value: PrimitiveValue): string {
5
+ // Shortened forms
6
+ if (value === true) {
7
+ return '!0';
8
+ }
9
+ if (value === false) {
10
+ return '!1';
11
+ }
12
+ if (value === undefined) {
13
+ return 'void 0';
14
+ }
15
+ if (value === null) {
16
+ return 'null';
17
+ }
18
+ // negative 0 isn't the same as 0
19
+ if (Object.is(value, -0)) {
20
+ return '-0';
21
+ }
22
+ if (typeof value === 'bigint') {
23
+ return `BigInt("${value}")`;
24
+ }
25
+ if (typeof value === 'string') {
26
+ return quote(value);
27
+ }
28
+ if (value.constructor === Date) {
29
+ return `new Date("${value.toISOString()}")`;
30
+ }
31
+ return String(value);
32
+ }