@muze-nl/od-jsontag 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 muze
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # od-jsontag: On Demand JSONTag
2
+
3
+ This library implements a parser and stringifier for a variant of jsontag which is optimized so that you only need to parse objects that you use and skip parsing any other objects.
4
+ This is especially useful to share data between threads or other workers using sharedArrayBuffers, the only shared memory option in javascript currently.
5
+
6
+ The parse function creates in memory Proxy objects that trigger parsing only when accessed. You can use the data as normal objects for most use-cases. The format supports non-enumerable
7
+ properties, which aren't part of the normal JSONTag format. The parse function expects an ArrayBuffer a input.
8
+
9
+ The stringify function creates a sharedArrayBuffer, which represents a file with one object per line. Each line is prefixed with a byte counter that indicates the length of the line. References to other objects are encoded as ~n, where n is the line number (starting at 0).
10
+
11
+ The parse function doesn't build an id index, because that requires parsing all objects. Instead the stringify function builds or updates the id index. It isn't included in the string result.
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@muze-nl/od-jsontag",
3
+ "version": "0.1.0",
4
+ "description": "On Demand JSONTag: parse/serialize large datastructures on demand, useful for sharing data between threads",
5
+ "type": "module",
6
+ "author": "Auke van Slooten <auke@muze.nl>",
7
+ "keywords": [
8
+ "JSONTag",
9
+ "On Demand Parsing"
10
+ ],
11
+ "homepage": "https://github.com/muze-nl/od-jsontag",
12
+ "bugs": {
13
+ "url": "https://github.com/muze-nl/od-jsontag/issues"
14
+ },
15
+ "license": "MIT",
16
+ "main": "./src/parse.mjs",
17
+ "scripts": {
18
+ "test": "tap test/*.mjs"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/muze-nl/od-jsontag.git"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "eslint": "^8.48.0",
29
+ "tap": "~16.3.7"
30
+ },
31
+ "files": [
32
+ "src/",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "dependencies": {
37
+ "@muze-nl/jsontag": "^0.9.2"
38
+ }
39
+ }
@@ -0,0 +1,26 @@
1
+ import JSONTag from '@muze-nl/jsontag'
2
+ import {source} from './symbols.mjs'
3
+
4
+ export function getType(obj) {
5
+ return JSONTag.getType(obj?.[source] ?? obj)
6
+ }
7
+
8
+ export function getAttribute(obj, attr) {
9
+ return JSONTag.getAttribute(obj?.[source] ?? obj, attr)
10
+ }
11
+
12
+ export function getAttributes(obj) {
13
+ return JSONTag.getAttributes(obj?.[source] ?? obj)
14
+ }
15
+
16
+ export function getAttributeString(obj) {
17
+ return JSONTag.getAttributesString(obj?.[source] ?? obj)
18
+ }
19
+
20
+ export function getTypeString(obj) {
21
+ return JSONTag.getTypeString(obj?.[source] ?? obj)
22
+ }
23
+
24
+ export function isNull(obj) {
25
+ return JSONTag.isNull(obj?.[source] ?? obj)
26
+ }
package/src/parse.mjs ADDED
@@ -0,0 +1,998 @@
1
+ import JSONTag from '@muze-nl/jsontag';
2
+ import Null from '@muze-nl/jsontag/src/lib/Null.mjs'
3
+ import serialize from './serialize.mjs'
4
+ import {source,isProxy,getBuffer,getIndex,isChanged,isParsed,position,parent,resultSet} from './symbols.mjs'
5
+
6
+ const decoder = new TextDecoder()
7
+ const encoder = new TextEncoder()
8
+
9
+ function stringToSAB(strData) {
10
+ const buffer = encoder.encode(strData)
11
+ const sab = new SharedArrayBuffer(buffer.length)
12
+ let uint8sab = new Uint8Array(sab)
13
+ uint8sab.set(buffer,0)
14
+ return uint8sab
15
+ }
16
+
17
+ export default function parse(input, meta, immutable=true)
18
+ {
19
+ if (!meta) {
20
+ meta = {}
21
+ }
22
+ if (!meta.unresolved) {
23
+ meta.unresolved = new Map()
24
+ }
25
+ if (!meta.baseURL) {
26
+ meta.baseURL = 'http://localhost/'
27
+ }
28
+
29
+ let at, ch, value, result;
30
+ let escapee = {
31
+ '"': '"',
32
+ "\\":"\\",
33
+ '/': '/',
34
+ b: "\b",
35
+ f: "\f",
36
+ n: "\n",
37
+ r: "\r",
38
+ t: "\t"
39
+ }
40
+ let offsetArray = []
41
+ let resultArray = []
42
+
43
+ at = 0
44
+ ch = " "
45
+
46
+ let error = function(m)
47
+ {
48
+ let context
49
+ try {
50
+ context = decoder.decode(input.slice(at-100,at+100));
51
+ } catch(err) {}
52
+ throw {
53
+ name: 'SyntaxError',
54
+ message: m,
55
+ at: at,
56
+ input: context
57
+ }
58
+ }
59
+
60
+ if (typeof input == 'string' || input instanceof String) {
61
+ input = stringToSAB(input)
62
+ }
63
+ if (!(input instanceof Uint8Array)) {
64
+ error('parse only accepts Uint8Array or String as input')
65
+ }
66
+
67
+ let next = function(c)
68
+ {
69
+ if (c && c!==ch) {
70
+ error("Expected '"+c+"' instead of '"+ch+"': "+at+':'+input)
71
+ }
72
+ ch = String.fromCharCode(input.at(at))
73
+ at+=1
74
+ return ch
75
+ }
76
+
77
+ let number = function(tagName)
78
+ {
79
+ let numString = ''
80
+ if (ch==='-') {
81
+ numString = '-'
82
+ next('-')
83
+ }
84
+ while(ch>='0' && ch<='9') {
85
+ numString += ch
86
+ next()
87
+ }
88
+ if (ch==='.') {
89
+ numString+='.'
90
+ while(next() && ch >= '0' && ch <= '9') {
91
+ numString += ch
92
+ }
93
+ }
94
+ if (ch === 'e' || ch === 'E') {
95
+ numString += ch
96
+ next()
97
+ if (ch === '-' || ch === '+') {
98
+ numString += ch
99
+ next()
100
+ }
101
+ while (ch >= '0' && ch <= '9') {
102
+ numString += ch
103
+ next()
104
+ }
105
+ }
106
+ let result = new Number(numString).valueOf()
107
+ if (tagName) {
108
+ switch(tagName) {
109
+ case "int":
110
+ isInt(numString)
111
+ break
112
+ case "uint":
113
+ isInt(numString, [0,Infinity])
114
+ break
115
+ case "int8":
116
+ isInt(numString, [-128,127])
117
+ break
118
+ case "uint8":
119
+ isInt(numString, [0,255])
120
+ break
121
+ case "int16":
122
+ isInt(numString, [-32768,32767])
123
+ break
124
+ case "uint16":
125
+ isInt(numString, [0,65535])
126
+ break
127
+ case "int32":
128
+ isInt(numString, [-2147483648, 2147483647])
129
+ break
130
+ case "uint32":
131
+ isInt(numString, [0,4294967295])
132
+ break
133
+ case "timestamp":
134
+ case "int64":
135
+ isInt(numString, [-9223372036854775808,9223372036854775807])
136
+ break
137
+ case "uint64":
138
+ isInt(numString, [0,18446744073709551615])
139
+ break
140
+ case "float":
141
+ isFloat(numString)
142
+ break
143
+ case "float32":
144
+ isFloat(numString, [-3.4e+38,3.4e+38])
145
+ break
146
+ case "float64":
147
+ isFloat(numString, [-1.7e+308,+1.7e+308])
148
+ break
149
+ case "number":
150
+ //FIXME: what to check? should already be covered by JSON parsing rules?
151
+ break
152
+ default:
153
+ isTypeError(tagName, numString)
154
+ break
155
+ }
156
+ }
157
+ return result
158
+ }
159
+
160
+ let isTypeError = function(type, value)
161
+ {
162
+ error('Syntax error, expected '+type+', got: '+value)
163
+ }
164
+
165
+ const regexes = {
166
+ color: /^(rgb|hsl)a?\((\d+%?(deg|rad|grad|turn)?[,\s]+){2,3}[\s\/]*[\d\.]+%?\)$/i,
167
+ email: /^[A-Za-z0-9_!#$%&'*+\/=?`{|}~^.-]+@[A-Za-z0-9.-]+$/,
168
+ uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
169
+ decimal: /^\d*\.?\d*$/,
170
+ money: /^[A-Z]+\$\d*\.?\d*$/,
171
+ duration: /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/,
172
+ phone: /^[+]?(?:\(\d+(?:\.\d+)?\)|\d+(?:\.\d+)?)(?:[ -]?(?:\(\d+(?:\.\d+)?\)|\d+(?:\.\d+)?))*(?:[ ]?(?:x|ext)\.?[ ]?\d{1,5})?$/,
173
+ time: /^(\d{2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?$/,
174
+ date: /^-?[1-9][0-9]{3,}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])$/,
175
+ datetime: /^(\d{4,})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?$/,
176
+ range: /^\[-?(\d+\.)?\d+\,-?(\d+\.)?\d+\]$/
177
+ }
178
+
179
+ let isFloat = function(float, range)
180
+ {
181
+ let test = new Number(parseFloat(float))
182
+ let str = test.toString()
183
+ if (float!==str) {
184
+ error('Syntax Error: expected float value')
185
+ }
186
+ if (range) {
187
+ if (typeof range[0] === 'number') {
188
+ if (test<range[0]) {
189
+ error('Syntax Error: float value out of range')
190
+ }
191
+ }
192
+ if (typeof range[1] === 'number') {
193
+ if (test>range[1]) {
194
+ error('Syntax Error: float value out of range')
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ let isInt = function(int, range)
201
+ {
202
+ let test = new Number(parseInt(int))
203
+ let str = test.toString()
204
+ if (int!==str) {
205
+ error('Syntax Error: expected integer value')
206
+ }
207
+ if (range) {
208
+ if (typeof range[0] === 'number') {
209
+ if (test<range[0]) {
210
+ error('Syntax Error: integer value out of range')
211
+ }
212
+ }
213
+ if (typeof range[1] === 'number') {
214
+ if (test>range[1]) {
215
+ error('Syntax Error: integer value out of range')
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ let isColor = function(color)
222
+ {
223
+ let result = false
224
+ if (color.charAt(0) === "#") {
225
+ color = color.substring(1)
226
+ result = ([3, 4, 6, 8].indexOf(color.length) > -1) && !isNaN(parseInt(color, 16))
227
+ if (result.toString(16)!==color) {
228
+ isTypeError('color', color)
229
+ }
230
+ } else {
231
+ result = regexes.color.test(color)
232
+ }
233
+ if (!result) {
234
+ isTypeError('color',color)
235
+ }
236
+ return true
237
+ }
238
+
239
+ let isEmail = function(email)
240
+ {
241
+ let result = regexes.email.test(email)
242
+ if (!result) {
243
+ isTypeError('email',email)
244
+ }
245
+ return true
246
+ }
247
+
248
+ let isUuid = function(uuid)
249
+ {
250
+ let result = regexes.uuid.test(uuid)
251
+ if (!result) {
252
+ isTypeError('uuid',uuid)
253
+ }
254
+ return true
255
+ }
256
+
257
+ let isDecimal = function(decimal)
258
+ {
259
+ let result = regexes.decimal.test(decimal)
260
+ if (!result) {
261
+ isTypeError('decimal',decimal)
262
+ }
263
+ return true
264
+ }
265
+
266
+ let isMoney = function(money)
267
+ {
268
+ let result = regexes.money.test(money)
269
+ if (!result) {
270
+ isTypeError('money',money)
271
+ }
272
+ return true
273
+ }
274
+
275
+ let isUrl = function(url)
276
+ {
277
+ try {
278
+ return Boolean(new URL(url, meta.baseURL))
279
+ } catch(e) {
280
+ isTypeError('url',url)
281
+ }
282
+ }
283
+
284
+ let isDuration = function(duration)
285
+ {
286
+ let result = regexes.duration.test(duration)
287
+ if (!result) {
288
+ isTypeError('duration',duration)
289
+ }
290
+ return true
291
+ }
292
+
293
+ let isPhone = function(phone)
294
+ {
295
+ let result = regexes.phone.test(phone)
296
+ if (!result) {
297
+ isTypeError('phone',phone)
298
+ }
299
+ return true
300
+ }
301
+
302
+ let isRange = function(range)
303
+ {
304
+ let result = regexes.range.test(range)
305
+ if (!result) {
306
+ isTypeError('range',range)
307
+ }
308
+ return true
309
+ }
310
+
311
+ let isTime = function(time)
312
+ {
313
+ let result = regexes.time.test(time)
314
+ if (!result) {
315
+ isTypeError('time',time)
316
+ }
317
+ return true
318
+ }
319
+
320
+ let isDate = function(date)
321
+ {
322
+ let result = regexes.date.test(date)
323
+ if (!result) {
324
+ isTypeError('date',date)
325
+ }
326
+ return true
327
+ }
328
+
329
+ let isDatetime = function(datetime)
330
+ {
331
+ let result = regexes.datetime.test(datetime)
332
+ if (!result) {
333
+ isTypeError('datetime',datetime)
334
+ }
335
+ return true
336
+ }
337
+
338
+ let checkStringType = function(tagName, value)
339
+ {
340
+ if (!tagName) {
341
+ return
342
+ }
343
+ switch(tagName){
344
+ case "object":
345
+ case "array":
346
+ case "int8":
347
+ case "uint8":
348
+ case "int16":
349
+ case "uint16":
350
+ case "int32":
351
+ case "uint32":
352
+ case "int64":
353
+ case "uint64":
354
+ case "int":
355
+ case "uint":
356
+ case "float32":
357
+ case "float64":
358
+ case "float":
359
+ case "timestamp":
360
+ isTypeError(tagName, value)
361
+ break
362
+ case "uuid":
363
+ return isUuid(value)
364
+ case "decimal":
365
+ return isDecimal(value)
366
+ case "money":
367
+ return isMoney(value)
368
+ case "url":
369
+ return isUrl(value)
370
+ case "link":
371
+ case "string":
372
+ case "text":
373
+ case "blob":
374
+ case "hash":
375
+ //anything goes
376
+ return true
377
+ case "color":
378
+ return isColor(value)
379
+ case "email":
380
+ return isEmail(value)
381
+ case "duration":
382
+ return isDuration(value)
383
+ case "phone":
384
+ return isPhone(value)
385
+ case "range":
386
+ return isRange(value)
387
+ case "time":
388
+ return isTime(value)
389
+ case "date":
390
+ return isDate(value)
391
+ case "datetime":
392
+ return isDatetime(value)
393
+ }
394
+ error('Syntax error: unknown tagName '+tagName)
395
+ }
396
+
397
+ let string = function(tagName)
398
+ {
399
+ let value = [], hex, i, uffff;
400
+ if (ch !== '"') {
401
+ error("Syntax Error")
402
+ }
403
+ next('"')
404
+ while(ch) {
405
+ if (ch==='"') {
406
+ next()
407
+ let bytes = new Uint8Array(value)
408
+ value = decoder.decode(bytes)
409
+ checkStringType(tagName, value)
410
+ return value
411
+ }
412
+ if (ch==='\\') {
413
+ next()
414
+ if (ch==='u') {
415
+ for (i=0; i<4; i++) {
416
+ hex = parseInt(next(), 16)
417
+ if (!isFinite(hex)) {
418
+ break
419
+ }
420
+ uffff = uffff * 16 + hex
421
+ }
422
+ let str = String.fromCharCode(uffff)
423
+ let bytes = encoder.encode(str)
424
+ value.push.apply(value, bytes)
425
+ next()
426
+ } else if (typeof escapee[ch] === 'string') {
427
+ value.push(escapee[ch].charCodeAt(0))
428
+ next()
429
+ } else {
430
+ break
431
+ }
432
+ } else {
433
+ value.push(ch.charCodeAt(0))
434
+ next()
435
+ }
436
+ }
437
+ error("Syntax error: incomplete string")
438
+ }
439
+
440
+ let tag = function()
441
+ {
442
+ let key, val, tagOb={
443
+ attributes: {}
444
+ }
445
+ if (ch !== '<') {
446
+ error("Syntax Error")
447
+ }
448
+ next('<')
449
+ key = word()
450
+ if (!key) {
451
+ error('Syntax Error: expected tag name')
452
+ }
453
+ tagOb.tagName = key
454
+ whitespace()
455
+ while(ch) {
456
+ if (ch==='>') {
457
+ next('>')
458
+ return tagOb
459
+ }
460
+ key = word()
461
+ if (!key) {
462
+ error('Syntax Error: expected attribute name')
463
+ }
464
+ whitespace()
465
+ next('=')
466
+ whitespace()
467
+ val = string()
468
+ tagOb.attributes[key] = val
469
+ whitespace()
470
+ }
471
+ error('Syntax Error: unexpected end of input')
472
+ }
473
+
474
+ let whitespace = function()
475
+ {
476
+ while (ch) {
477
+ switch(ch) {
478
+ case ' ':
479
+ case "\t":
480
+ case "\r":
481
+ case "\n":
482
+ next()
483
+ break
484
+ default:
485
+ return
486
+ break
487
+ }
488
+ }
489
+ }
490
+
491
+ let word = function()
492
+ {
493
+ //[a-z][a-z0-9_]*
494
+ let val='';
495
+ if ((ch>='a' && ch<='z') || (ch>='A' && ch<='Z')) {
496
+ val += ch
497
+ next()
498
+ } else {
499
+ error('Syntax Error: expected word')
500
+ }
501
+ while((ch>='a' && ch<='z') || (ch>='A' && ch<='Z') || (ch>='0' && ch<='9') || ch=='_') {
502
+ val += ch
503
+ next()
504
+ }
505
+ return val
506
+ }
507
+
508
+ let boolOrNull = function(tagName)
509
+ {
510
+ let w = word()
511
+ if (!w || typeof w !== 'string') {
512
+ error('Syntax error: expected boolean or null, got "'+w+'"')
513
+ }
514
+ switch(w.toLowerCase()) {
515
+ case 'true':
516
+ if (tagName && tagName!=='boolean') {
517
+ isTypeError(tagName,w)
518
+ }
519
+ return true
520
+ break
521
+ case 'false':
522
+ if (tagName && tagName!=='boolean') {
523
+ isTypeError(tagName,w)
524
+ }
525
+ return false
526
+ break
527
+ case 'null':
528
+ return null
529
+ break
530
+ default:
531
+ error('Syntax error: expected boolean or null, got "'+w+'"')
532
+ break
533
+ }
534
+ }
535
+
536
+ let checkUnresolved = function(item, object, key)
537
+ {
538
+ if (JSONTag.getType(item)==='link') {
539
+ let link = ''+item
540
+ let links = meta.unresolved.get(link)
541
+ if (typeof links === 'undefined') {
542
+ meta.unresolved.set(link,[])
543
+ links = meta.unresolved.get(link)
544
+ }
545
+ let count = links.push({
546
+ src: new WeakRef(object),
547
+ key: key
548
+ })
549
+ }
550
+ }
551
+
552
+ let array = function()
553
+ {
554
+ let item, array = []
555
+ if (ch !== '[') {
556
+ error("Syntax error")
557
+ }
558
+ next('[')
559
+ whitespace()
560
+ if (ch===']') {
561
+ next(']')
562
+ return array
563
+ }
564
+ while(ch) {
565
+ item = value()
566
+ checkUnresolved(item, array, array.length)
567
+ array.push(item)
568
+ whitespace()
569
+ if (ch===']') {
570
+ next(']')
571
+ return array
572
+ }
573
+ next(',')
574
+ whitespace()
575
+ }
576
+ error("Input stopped early")
577
+ }
578
+
579
+ let object = function(object={})
580
+ {
581
+ let key, val
582
+ if (ch !== '{') {
583
+ error("Syntax Error")
584
+ }
585
+ next('{')
586
+ whitespace()
587
+ if (ch==='}') {
588
+ next('}')
589
+ return object
590
+ }
591
+ let enumerable = true
592
+ while(ch) {
593
+ if (ch==='#') {
594
+ enumerable = false
595
+ next()
596
+ } else {
597
+ enumerable = true
598
+ }
599
+ key = string()
600
+ if (key==='__proto__') {
601
+ error("Attempt at prototype pollution")
602
+ }
603
+ whitespace()
604
+ next(':')
605
+ val = value()
606
+ if (!enumerable) {
607
+ Object.defineProperty(object, key, {
608
+ configurable: true, //important, must be true, otherwise Proxies cannot use it
609
+ writable: true, // handle immutability in the Proxy traps
610
+ enumerable: false,
611
+ value: val
612
+ })
613
+ } else {
614
+ object[key] = val
615
+ }
616
+ checkUnresolved(val, object, key)
617
+ whitespace()
618
+ if (ch==='}') {
619
+ next('}')
620
+ return object
621
+ }
622
+ next(',')
623
+ whitespace()
624
+ }
625
+ error("Input stopped early")
626
+ }
627
+
628
+ let length = function()
629
+ {
630
+ whitespace()
631
+ next('(')
632
+ let numString=''
633
+ while(ch>='0' && ch<='9') {
634
+ numString += ch
635
+ next()
636
+ }
637
+ if (ch!==')') {
638
+ error('Syntax error: not a length')
639
+ }
640
+ next()
641
+ return parseInt(numString)
642
+ }
643
+
644
+ let offset = function()
645
+ {
646
+ next('~')
647
+ let numString = ''
648
+ while(ch>='0' && ch<='9') {
649
+ numString += ch
650
+ next()
651
+ }
652
+ return parseInt(numString)
653
+ }
654
+
655
+ let parseValue = function(position, ob={}) {
656
+ at = position.start
657
+ next()
658
+ return value(ob)
659
+ }
660
+
661
+ const makeChildProxies = function(parent) {
662
+ Object.entries(parent).forEach(([key,entry]) => {
663
+ if (Array.isArray(entry)) {
664
+ makeChildProxies(entry)
665
+ } else if (JSONTag.getType(entry)==='object') {
666
+ if (entry[isProxy]) {
667
+ // do nothing
668
+ } else {
669
+ parent[key] = getNewValueProxy(entry)
670
+ }
671
+ }
672
+ })
673
+ }
674
+
675
+ const handlers = {
676
+ newArrayHandler: {
677
+ get(target, prop) {
678
+ if (target[prop] instanceof Function) {
679
+ return (...args) => {
680
+ args = args.map(arg => {
681
+ if (JSONTag.getType(arg)==='object' && !arg[isProxy]) {
682
+ arg = getNewValueProxy(arg)
683
+ }
684
+ return arg
685
+ })
686
+ return target[prop].apply(target, args)
687
+ }
688
+ } else if (prop===isChanged) {
689
+ return true
690
+ } else {
691
+ if (Array.isArray(target[prop])) {
692
+ return new Proxy(target[prop], handlers.newArrayHandler)
693
+ }
694
+ return target[prop]
695
+ }
696
+ },
697
+ set(target, prop, value) {
698
+ if (JSONTag.getType(value)==='object' && !value[isProxy]) {
699
+ value = getNewValueProxy(value)
700
+ }
701
+ target[prop] = value
702
+ return true
703
+ }
704
+ },
705
+ newValueHandler: {
706
+ get(target, prop, receiver) {
707
+ switch(prop) {
708
+ case source:
709
+ return target
710
+ break
711
+ case isProxy:
712
+ return true
713
+ break
714
+ case getBuffer:
715
+ return (i) => {
716
+ let index = target[getIndex]
717
+ if (i != index) {
718
+ return encoder.encode('~'+index)
719
+ }
720
+ return serialize(target, meta, true, i)
721
+ }
722
+ break
723
+ case getIndex:
724
+ return target[getIndex]
725
+ break
726
+ case isChanged:
727
+ return true
728
+ break
729
+ default:
730
+ if (Array.isArray(target[prop])) {
731
+ return new Proxy(target[prop], handlers.newArrayHandler)
732
+ }
733
+ return target[prop]
734
+ break
735
+ }
736
+ },
737
+ set(target, prop, value) {
738
+ if (JSONTag.getType(value)==='object' && !value[isProxy]) {
739
+ value = getNewValueProxy(value)
740
+ }
741
+ target[prop] = value
742
+ return true
743
+ }
744
+ },
745
+ arrayHandler: {
746
+ get(target, prop) {
747
+ if (target[prop] instanceof Function) {
748
+ if (['copyWithin','fill','pop','push','reverse','shift','sort','splice','unshift'].indexOf(prop)!==-1) {
749
+ if (immutable) {
750
+ throw new Error('dataspace is immutable')
751
+ }
752
+ }
753
+ return (...args) => {
754
+ args = args.map(arg => {
755
+ if (JSONTag.getType(arg)==='object' && !arg[isProxy]) {
756
+ arg = getNewValueProxy(arg)
757
+ }
758
+ return arg
759
+ })
760
+ target[parent][isChanged] = true // incorrect target for isChanged...
761
+ let result = target[prop].apply(target, args)
762
+ return result
763
+ }
764
+ } else if (prop===isChanged) {
765
+ return target[parent][isChanged]
766
+ } else {
767
+ if (Array.isArray(target[prop])) {
768
+ target[prop][parent] = target[parent]
769
+ return new Proxy(target[prop], handlers.arrayHandler)
770
+ }
771
+ return target[prop]
772
+ }
773
+ },
774
+ set(target, prop, value) {
775
+ if (immutable) {
776
+ throw new Error('dataspace is immutable')
777
+ }
778
+ if (JSONTag.getType(value)==='object' && !value[isProxy]) {
779
+ value = getNewValueProxy(value)
780
+ }
781
+ target[prop] = value
782
+ target[parent][isChanged] = true
783
+ return true
784
+ },
785
+ deleteProperty(target, prop) {
786
+ if (immutable) {
787
+ throw new Error('dataspace is immutable')
788
+ }
789
+ //FIXME: if target[prop] was the last reference to an object
790
+ //that object should be deleted so that its line will become empty
791
+ //when stringifying resultArray again
792
+ delete target[prop]
793
+ target[parent][isChanged] = true
794
+ return true
795
+ }
796
+ },
797
+ handler: {
798
+ get(target, prop, receiver) {
799
+ firstParse(target)
800
+ switch(prop) {
801
+ case source:
802
+ return target
803
+ break
804
+ case isProxy:
805
+ return true
806
+ break
807
+ case getBuffer:
808
+ return (i) => {
809
+ let index = target[getIndex]
810
+ if (i != index) {
811
+ return encoder.encode('~'+index)
812
+ }
813
+ if (target[isChanged]) {
814
+ return serialize(target, null, true)
815
+ }
816
+ return input.slice(target[position].start,target[position].end)
817
+ }
818
+ break
819
+ case getIndex:
820
+ return target[getIndex]
821
+ break
822
+ case isChanged:
823
+ return target[isChanged]
824
+ break
825
+ default:
826
+ if (Array.isArray(target[prop])) {
827
+ target[prop][parent] = target
828
+ return new Proxy(target[prop], handlers.arrayHandler)
829
+ }
830
+ return target[prop]
831
+ break
832
+ }
833
+ },
834
+ set(target, prop, value) {
835
+ if (immutable && prop!==resultSet) {
836
+ throw new Error('dataspace is immutable')
837
+ }
838
+ firstParse(target)
839
+ if (prop!==isChanged) {
840
+ if (JSONTag.getType(value)==='object' && !value[isProxy]) {
841
+ value = getNewValueProxy(value)
842
+ }
843
+ target[prop] = value
844
+ }
845
+ target[isChanged] = true
846
+ return true
847
+ },
848
+ deleteProperty(target, prop) {
849
+ if (immutable) {
850
+ throw new Error('dataspace is immutable')
851
+ }
852
+ firstParse(target)
853
+ delete target[prop]
854
+ target[isChanged] = true
855
+ return true
856
+ },
857
+ ownKeys(target) {
858
+ firstParse(target)
859
+ return Reflect.ownKeys(target)
860
+ },
861
+ getOwnPropertyDescriptor(target, prop) {
862
+ firstParse(target)
863
+ return Reflect.getOwnPropertyDescriptor(target, prop)
864
+ },
865
+ defineProperty(target, prop, descriptor) {
866
+ if (immutable) {
867
+ throw new Error('dataspace is immutable')
868
+ }
869
+ firstParse(target)
870
+ Object.defineProperty(target, prop, descriptor)
871
+ },
872
+ has(target, prop) {
873
+ firstParse()
874
+ return prop in target
875
+ },
876
+ setPrototypeOf(target,proto) {
877
+ throw new Error('changing prototypes is not supported')
878
+ }
879
+ }
880
+ }
881
+
882
+ const firstParse = function(target) {
883
+ if (!target[isParsed]) {
884
+ parseValue(target[position], target)
885
+ target[isParsed] = true
886
+ }
887
+ }
888
+
889
+ const getNewValueProxy = function(value) {
890
+ let index = resultArray.length
891
+ resultArray.push('')
892
+ value[getIndex] = index
893
+ makeChildProxies(value)
894
+ let result = new Proxy(value, handlers.newValueHandler)
895
+ resultArray[index] = result
896
+ return result
897
+ }
898
+
899
+ let valueProxy = function(length, index)
900
+ {
901
+ let cache = {}
902
+ cache[getIndex] = index
903
+ cache[isChanged] = false
904
+ cache[isParsed] = false
905
+ // current offset + length contains jsontag of this value
906
+ cache[position] = {
907
+ start: at-1,
908
+ end: at-1+length
909
+ }
910
+ at += length
911
+ next()
912
+ // newValueHandler makes sure that value[getBuffer] runs stringify
913
+ // arrayHandler makes sure that changes in the array set targetIsChanged to true
914
+ return new Proxy(cache, handlers.handler)
915
+ }
916
+
917
+ value = function(ob={})
918
+ {
919
+ let tagOb, result, tagName;
920
+ whitespace()
921
+ if (ch==='~') {
922
+ let vOffset = offset()
923
+ return resultArray[vOffset]
924
+ }
925
+ if (ch==='<') {
926
+ tagOb = tag()
927
+ tagName = tagOb.tagName
928
+ whitespace()
929
+ }
930
+ switch(ch) {
931
+ case '{':
932
+ if (tagName && tagName!=='object') {
933
+ isTypeError(tagName, ch)
934
+ }
935
+ result = object(ob)
936
+ break
937
+ case '[':
938
+ if (tagName && tagName!=='array') {
939
+ isTypeError(tagName, ch)
940
+ }
941
+ result = array()
942
+ break
943
+ case '"':
944
+ result = string(tagName)
945
+ break
946
+ case '-':
947
+ result = number(tagName)
948
+ break
949
+ default:
950
+ if (ch>='0' && ch<='9') {
951
+ result = number(tagName)
952
+ } else {
953
+ result = boolOrNull(tagName)
954
+ }
955
+ break
956
+ }
957
+ if (tagOb) {
958
+ if (result === null) {
959
+ result = new Null()
960
+ }
961
+ if (typeof result !== 'object') {
962
+ switch(typeof result) {
963
+ case 'string':
964
+ result = new String(result)
965
+ break
966
+ case 'number':
967
+ result = new Number(result)
968
+ break
969
+ default:
970
+ error('Syntax Error: unexpected type '+(typeof result))
971
+ break
972
+ }
973
+ }
974
+ if (tagOb.tagName) {
975
+ JSONTag.setType(result, tagOb.tagName)
976
+ }
977
+ if (tagOb.attributes) {
978
+ JSONTag.setAttributes(result, tagOb.attributes)
979
+ }
980
+ }
981
+ return result
982
+ }
983
+
984
+ function lengthValue(i) {
985
+ let l = length()
986
+ let v = valueProxy(l,i)
987
+ return [l, v]
988
+ }
989
+
990
+ while(ch && at<input.length) {
991
+ result = lengthValue(resultArray.length)
992
+ whitespace()
993
+ offsetArray.push(at)
994
+ resultArray.push(result[1])
995
+ }
996
+ resultArray[0][resultSet] = resultArray
997
+ return resultArray[0]
998
+ }
@@ -0,0 +1,184 @@
1
+ import JSONTag from '@muze-nl/jsontag';
2
+ import {source,isProxy, isChanged, getIndex, getBuffer, resultSet} from './symbols.mjs'
3
+ import * as odJSONTag from './jsontag.mjs'
4
+
5
+ // faststringify function for a fast parseable arraybuffer output
6
+ //
7
+ const encoder = new TextEncoder()
8
+ const decoder = new TextDecoder()
9
+
10
+ function stringToSAB(strData) {
11
+ const buffer = encoder.encode(strData)
12
+ const sab = new SharedArrayBuffer(buffer.length)
13
+ let uint8sab = new Uint8Array(sab)
14
+ uint8sab.set(buffer,0)
15
+ return uint8sab
16
+ }
17
+
18
+ export default function serialize(value, meta, skipLength=false, index=false) {
19
+ let resultArray = []
20
+ if (!meta) {
21
+ meta = {}
22
+ }
23
+ if (!meta.index) {
24
+ meta.index = {}
25
+ }
26
+ if (!meta.index.id) {
27
+ meta.index.id = new Map()
28
+ }
29
+ let references = new WeakMap()
30
+
31
+ function stringifyValue(value) {
32
+ let prop
33
+ let typeString = odJSONTag.getTypeString(value)
34
+ let type = odJSONTag.getType(value)
35
+ switch (type) {
36
+ case 'string':
37
+ case 'decimal':
38
+ case 'money':
39
+ case 'link':
40
+ case 'text':
41
+ case 'blob':
42
+ case 'color':
43
+ case 'email':
44
+ case 'hash':
45
+ case 'duration':
46
+ case 'phone':
47
+ case 'url':
48
+ case 'uuid':
49
+ case 'date':
50
+ case 'time':
51
+ case 'datetime':
52
+ if (odJSONTag.isNull(value)) {
53
+ value = 'null'
54
+ } else {
55
+ value = JSON.stringify(''+value)
56
+ }
57
+ prop = typeString + value
58
+ break
59
+ case 'int':
60
+ case 'uint':
61
+ case 'int8':
62
+ case 'uint8':
63
+ case 'int16':
64
+ case 'uint16':
65
+ case 'int32':
66
+ case 'uint32':
67
+ case 'int64':
68
+ case 'uint64':
69
+ case 'float':
70
+ case 'float32':
71
+ case 'float64':
72
+ case 'timestamp':
73
+ case 'number':
74
+ case 'boolean':
75
+ if (odJSONTag.isNull(value)) {
76
+ value = 'null'
77
+ } else {
78
+ value = JSON.stringify(value)
79
+ }
80
+ prop = typeString + value
81
+ break
82
+ case 'array':
83
+ let entries = value.map(e => stringifyValue(e)).join(',')
84
+ prop = typeString + '[' + entries + ']'
85
+ break
86
+ case 'object':
87
+ if (!value) {
88
+ prop = 'null'
89
+ } else if (value[isProxy]) {
90
+ prop = decoder.decode(value[getBuffer](current))
91
+ } else {
92
+ if (!references.has(value)) {
93
+ references.set(value, resultArray.length)
94
+ resultArray.push(value)
95
+ }
96
+ prop = '~'+references.get(value)
97
+ }
98
+ break
99
+ default:
100
+ throw new Error(JSONTag.getType(value)+' type not yet implemented')
101
+ break
102
+ }
103
+ return prop
104
+ }
105
+
106
+ const encoder = new TextEncoder()
107
+ const decoder = new TextDecoder()
108
+
109
+ // is only ever called on object values
110
+ // and should always return a stringified object, not a reference (~n)
111
+ const innerStringify = (current) => {
112
+ let object = resultArray[current]
113
+ let result
114
+
115
+ // if value is a valueProxy, just copy the input slice
116
+ if (object && !odJSONTag.isNull(object) && object[isProxy] && !object[isChanged]) {
117
+ return decoder.decode(object[getBuffer](current))
118
+ }
119
+ if (typeof object === 'undefined' || object === null) {
120
+ return 'null'
121
+ }
122
+
123
+ let props = []
124
+ for (let key of Object.getOwnPropertyNames(object)) {
125
+ let value = object[key]
126
+ let prop = stringifyValue(value)
127
+ let enumerable = object.propertyIsEnumerable(key) ? '' : '#'
128
+ props.push(enumerable+'"'+key+'":'+prop)
129
+ }
130
+ result = odJSONTag.getTypeString(object)+'{'+props.join(',')+'}'
131
+ return result
132
+ }
133
+
134
+ const encode = (s) => {
135
+ if (typeof s == 'string' || s instanceof String) {
136
+ s = encoder.encode(s)
137
+ }
138
+ if (skipLength) {
139
+ return new Uint8Array(s)
140
+ }
141
+ let length = encoder.encode('('+s.length+')')
142
+ let u8arr = new Uint8Array(length.length+s.length)
143
+ u8arr.set(length, 0)
144
+ u8arr.set(s, length.length)
145
+ return u8arr
146
+ }
147
+
148
+ if (value[resultSet]) {
149
+ resultArray = value[resultSet].slice()
150
+ } else {
151
+ resultArray.push(value)
152
+ }
153
+ let current = 0
154
+ while(current<resultArray.length) {
155
+ if (resultArray[current][isChanged] || !resultArray[current][isProxy]) {
156
+ resultArray[current] = encoder.encode(innerStringify(current))
157
+ } else {
158
+ resultArray[current] = resultArray[current][getBuffer](current)
159
+ }
160
+ current++
161
+ }
162
+ let arr = resultArray.map(encode)
163
+ let length = 0
164
+ for (let line of arr) {
165
+ length += line.length+1
166
+ }
167
+ length -= 1 // skip last newline
168
+ let sab = new SharedArrayBuffer(length)
169
+ let u8arr = new Uint8Array(sab)
170
+ let offset = 0
171
+ for(let line of arr) {
172
+ u8arr.set(line, offset)
173
+ offset+=line.length
174
+ if (offset<length) {
175
+ u8arr.set([10], offset)
176
+ offset++
177
+ }
178
+ }
179
+ return u8arr
180
+ }
181
+
182
+ export function stringify(buf) {
183
+ return decoder.decode(buf)
184
+ }
@@ -0,0 +1,10 @@
1
+ export const source = Symbol('source')
2
+ export const isProxy = Symbol('isProxy')
3
+ export const getBuffer = Symbol('getBuffer')
4
+ export const getIndex = Symbol('getIndex')
5
+ export const isChanged = Symbol('isChanged')
6
+ export const isParsed = Symbol('isParsed')
7
+ export const getString = Symbol('getString')
8
+ export const position = Symbol('position')
9
+ export const parent = Symbol('parent')
10
+ export const resultSet = Symbol('resultSet')