seroval 0.1.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/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "seroval",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "files": [
6
+ "dist",
7
+ "src"
8
+ ],
9
+ "engines": {
10
+ "node": ">=10"
11
+ },
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "pridepack"
15
+ ],
16
+ "devDependencies": {
17
+ "@types/node": "^18.11.18",
18
+ "eslint": "^8.32.0",
19
+ "eslint-config-lxsmnsyc": "^0.4.8",
20
+ "pridepack": "2.4.0",
21
+ "tslib": "^2.4.1",
22
+ "typescript": "^4.9.4",
23
+ "vitest": "^0.28.1"
24
+ },
25
+ "scripts": {
26
+ "prepublishOnly": "pridepack clean && pridepack build",
27
+ "build": "pridepack build",
28
+ "type-check": "pridepack check",
29
+ "lint": "pridepack lint",
30
+ "clean": "pridepack clean",
31
+ "watch": "pridepack watch",
32
+ "start": "pridepack start",
33
+ "dev": "pridepack dev",
34
+ "test": "vitest"
35
+ },
36
+ "private": false,
37
+ "description": "Stringify JS values",
38
+ "repository": {
39
+ "url": "https://github.com/lxsmnsyc/seroval.git",
40
+ "type": "git"
41
+ },
42
+ "homepage": "https://github.com/lxsmnsyc/seroval/tree/main/packages/seroval",
43
+ "bugs": {
44
+ "url": "https://github.com/lxsmnsyc/seroval/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "author": "Alexis Munsayac",
50
+ "types": "./dist/types/index.d.ts",
51
+ "main": "./dist/cjs/production/index.cjs",
52
+ "module": "./dist/esm/production/index.mjs",
53
+ "exports": {
54
+ ".": {
55
+ "development": {
56
+ "require": "./dist/cjs/development/index.cjs",
57
+ "import": "./dist/esm/development/index.mjs"
58
+ },
59
+ "require": "./dist/cjs/production/index.cjs",
60
+ "import": "./dist/esm/production/index.mjs",
61
+ "types": "./dist/types/index.d.ts"
62
+ }
63
+ },
64
+ "typesVersions": {
65
+ "*": {}
66
+ },
67
+ "gitHead": "c7db9d01e7f66662e4f890ded4533be0b0cb0ea1"
68
+ }
package/src/array.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function forEach<T>(arr: T[], callback: (value: T, index: number) => (boolean | void)) {
2
+ for (let i = 0, len = arr.length; i < len; i++) {
3
+ if (callback(arr[i], i)) {
4
+ break;
5
+ }
6
+ }
7
+ }
8
+
9
+ export function join(arr: string[], joiner: string): string {
10
+ let result = '';
11
+ forEach(arr, (value, index) => {
12
+ result += value;
13
+ if (index < arr.length - 1) {
14
+ result += joiner;
15
+ }
16
+ });
17
+ return result;
18
+ }
@@ -0,0 +1,16 @@
1
+ const REF_START_CHARS = 'hjkmoquxzABCDEFGHIJKLNPQRTUVWXYZ$_'; // Avoids chars that could evaluate to a reserved word.
2
+ const REF_START_CHARS_LEN = REF_START_CHARS.length;
3
+ const REF_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$_';
4
+ const REF_CHARS_LEN = REF_CHARS.length;
5
+
6
+ export default function getIdentifier(index: number) {
7
+ let mod = index % REF_START_CHARS_LEN;
8
+ let ref = REF_START_CHARS[mod];
9
+ index = (index - mod) / REF_START_CHARS_LEN;
10
+ while (index > 0) {
11
+ mod = index % REF_CHARS_LEN;
12
+ ref += REF_CHARS[mod];
13
+ index = (index - mod) / REF_CHARS_LEN;
14
+ }
15
+ return ref;
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,479 @@
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
+ }
167
+ }
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;
177
+ }
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];
186
+ }
187
+ const result = getIdentifier(index);
188
+ VARS[index] = result;
189
+ return result;
190
+ }
191
+
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;
356
+ }
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;
392
+ }
393
+ return EMPTY_ARRAY;
394
+ }
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;
438
+ }
439
+ throw new Error('Unserializable value');
440
+ }
441
+
442
+ export default function serialize(source: ServerValue) {
443
+ if (isPrimitive(source)) {
444
+ return serializePrimitive(source);
445
+ }
446
+ reset();
447
+ // Lookup possible shared references
448
+ lookupRefs(source);
449
+ // Add shared references to refs
450
+ for (const [key, value] of REF_COUNT.entries()) {
451
+ if (value > 1) {
452
+ createRef(key);
453
+ }
454
+ }
455
+ // Get top-level serialization
456
+ const result = traverse(source);
457
+ if (typeof result !== 'string') {
458
+ throw new Error('Unreachable error');
459
+ }
460
+
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;
479
+ }
package/src/quote.ts ADDED
@@ -0,0 +1,45 @@
1
+ // Written by https://github.com/DylanPiercey and is distributed under the MIT license.
2
+ // Creates a JavaScript double quoted string and escapes all characters
3
+ // not listed as DoubleStringCharacters on
4
+ // Also includes "<" to escape "</script>" and "\" to avoid invalid escapes in the output.
5
+ // http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
6
+ export default function quote(str: string) {
7
+ let result = '';
8
+ let lastPos = 0;
9
+ for (let i = 0, len = str.length; i < len; i++) {
10
+ let replacement;
11
+ switch (str[i]) {
12
+ case '"':
13
+ replacement = '\\"';
14
+ break;
15
+ case '\\':
16
+ replacement = '\\\\';
17
+ break;
18
+ case '<':
19
+ replacement = '\\x3C';
20
+ break;
21
+ case '\n':
22
+ replacement = '\\n';
23
+ break;
24
+ case '\r':
25
+ replacement = '\\r';
26
+ break;
27
+ case '\u2028':
28
+ replacement = '\\u2028';
29
+ break;
30
+ case '\u2029':
31
+ replacement = '\\u2029';
32
+ break;
33
+ default:
34
+ continue;
35
+ }
36
+ result += str.slice(lastPos, i) + replacement;
37
+ lastPos = i + 1;
38
+ }
39
+ if (lastPos === 0) {
40
+ result = str;
41
+ } else {
42
+ result += str.slice(lastPos);
43
+ }
44
+ return `"${result}"`;
45
+ }