@covenant-rpc/ion 1.0.3

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/index.ts ADDED
@@ -0,0 +1,698 @@
1
+
2
+
3
+
4
+ const ION = {
5
+ parse(ionExpression: string): unknown {
6
+ type TokenType =
7
+ | 'LBRACE' | 'RBRACE' | 'LBRACKET' | 'RBRACKET'
8
+ | 'COMMA' | 'COLON' | 'STRING' | 'NUMBER'
9
+ | 'TRUE' | 'FALSE' | 'NULL' | 'NAN'
10
+ | 'INFINITY' | 'NEG_INFINITY' | 'DATE'
11
+ | 'MAP' | 'SET' | 'EOF';
12
+
13
+ interface Token {
14
+ type: TokenType;
15
+ value?: unknown;
16
+ position: number;
17
+ }
18
+
19
+ class Tokenizer {
20
+ private pos = 0;
21
+ private input: string;
22
+ private len: number;
23
+
24
+ constructor(input: string) {
25
+ this.input = input;
26
+ this.len = input.length;
27
+ }
28
+
29
+ private skipWhitespace(): void {
30
+ const input = this.input;
31
+ const len = this.len;
32
+ let pos = this.pos;
33
+
34
+ while (pos < len) {
35
+ const ch = input.charCodeAt(pos);
36
+ // Check for common whitespace: space(32), tab(9), newline(10), carriage return(13)
37
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
38
+ pos++;
39
+ } else {
40
+ break;
41
+ }
42
+ }
43
+
44
+ this.pos = pos;
45
+ }
46
+
47
+ private scanString(): string {
48
+ const input = this.input;
49
+ const len = this.len;
50
+ let pos = this.pos + 1; // skip opening quote
51
+ let start = pos;
52
+
53
+ // Fast path: scan for simple string without escapes
54
+ while (pos < len) {
55
+ const ch = input.charCodeAt(pos);
56
+
57
+ if (ch === 34) { // '"'
58
+ this.pos = pos + 1;
59
+ return input.slice(start, pos);
60
+ }
61
+
62
+ if (ch === 92) { // '\\'
63
+ // Found escape, switch to slow path
64
+ break;
65
+ }
66
+
67
+ pos++;
68
+ }
69
+
70
+ // Slow path: handle escapes
71
+ const parts: string[] = [];
72
+ if (pos > start) {
73
+ parts.push(input.slice(start, pos));
74
+ }
75
+
76
+ while (pos < len) {
77
+ const ch = input.charCodeAt(pos);
78
+
79
+ if (ch === 34) { // '"'
80
+ this.pos = pos + 1;
81
+ return parts.join('');
82
+ }
83
+
84
+ if (ch === 92) { // '\\'
85
+ pos++;
86
+ const escaped = input.charCodeAt(pos);
87
+ pos++;
88
+
89
+ switch (escaped) {
90
+ case 110: parts.push('\n'); break; // 'n'
91
+ case 116: parts.push('\t'); break; // 't'
92
+ case 114: parts.push('\r'); break; // 'r'
93
+ case 92: parts.push('\\'); break; // '\\'
94
+ case 34: parts.push('"'); break; // '"'
95
+ case 47: parts.push('/'); break; // '/'
96
+ case 98: parts.push('\b'); break; // 'b'
97
+ case 102: parts.push('\f'); break; // 'f'
98
+ case 117: { // 'u'
99
+ const hex = input.slice(pos, pos + 4);
100
+ pos += 4;
101
+ parts.push(String.fromCharCode(parseInt(hex, 16)));
102
+ break;
103
+ }
104
+ default:
105
+ throw new Error(`Invalid escape sequence at position ${pos}`);
106
+ }
107
+ start = pos;
108
+ } else {
109
+ // Accumulate unescaped chars
110
+ const chunkStart = pos;
111
+ pos++;
112
+ while (pos < len) {
113
+ const c = input.charCodeAt(pos);
114
+ if (c === 34 || c === 92) break;
115
+ pos++;
116
+ }
117
+ parts.push(input.slice(chunkStart, pos));
118
+ }
119
+ }
120
+
121
+ throw new Error(`Unterminated string at position ${pos}`);
122
+ }
123
+
124
+ private scanNumber(): number {
125
+ const input = this.input;
126
+ const len = this.len;
127
+ let pos = this.pos;
128
+ const start = pos;
129
+
130
+ // Check for negative
131
+ if (input.charCodeAt(pos) === 45) { // '-'
132
+ pos++;
133
+ }
134
+
135
+ // Scan integer part
136
+ const firstDigit = input.charCodeAt(pos);
137
+ if (firstDigit === 48) { // '0'
138
+ pos++;
139
+ } else if (firstDigit >= 49 && firstDigit <= 57) { // '1'-'9'
140
+ pos++;
141
+ while (pos < len && input.charCodeAt(pos) >= 48 && input.charCodeAt(pos) <= 57) {
142
+ pos++;
143
+ }
144
+ }
145
+
146
+ // Check for decimal
147
+ let hasDot = false;
148
+ if (pos < len && input.charCodeAt(pos) === 46) { // '.'
149
+ hasDot = true;
150
+ pos++;
151
+ while (pos < len && input.charCodeAt(pos) >= 48 && input.charCodeAt(pos) <= 57) {
152
+ pos++;
153
+ }
154
+ }
155
+
156
+ // Check for exponent
157
+ let hasExp = false;
158
+ if (pos < len) {
159
+ const ch = input.charCodeAt(pos);
160
+ if (ch === 101 || ch === 69) { // 'e' or 'E'
161
+ hasExp = true;
162
+ pos++;
163
+ if (pos < len) {
164
+ const sign = input.charCodeAt(pos);
165
+ if (sign === 43 || sign === 45) { // '+' or '-'
166
+ pos++;
167
+ }
168
+ }
169
+ while (pos < len && input.charCodeAt(pos) >= 48 && input.charCodeAt(pos) <= 57) {
170
+ pos++;
171
+ }
172
+ }
173
+ }
174
+
175
+ this.pos = pos;
176
+
177
+ // Fast path for simple integers
178
+ if (!hasDot && !hasExp) {
179
+ const numStr = input.slice(start, pos);
180
+ const num = parseInt(numStr, 10);
181
+ if (num.toString() === numStr) {
182
+ return num;
183
+ }
184
+ }
185
+
186
+ return parseFloat(input.slice(start, pos));
187
+ }
188
+
189
+ nextToken(): Token {
190
+ this.skipWhitespace();
191
+
192
+ const pos = this.pos;
193
+ if (pos >= this.len) {
194
+ return { type: 'EOF', position: pos };
195
+ }
196
+
197
+ const input = this.input;
198
+ const ch = input.charCodeAt(pos);
199
+
200
+ // Single character tokens (using charCode for speed)
201
+ switch (ch) {
202
+ case 123: // '{'
203
+ this.pos = pos + 1;
204
+ return { type: 'LBRACE', position: pos };
205
+ case 125: // '}'
206
+ this.pos = pos + 1;
207
+ return { type: 'RBRACE', position: pos };
208
+ case 91: // '['
209
+ this.pos = pos + 1;
210
+ return { type: 'LBRACKET', position: pos };
211
+ case 93: // ']'
212
+ this.pos = pos + 1;
213
+ return { type: 'RBRACKET', position: pos };
214
+ case 44: // ','
215
+ this.pos = pos + 1;
216
+ return { type: 'COMMA', position: pos };
217
+ case 58: // ':'
218
+ this.pos = pos + 1;
219
+ return { type: 'COLON', position: pos };
220
+ case 34: // '"'
221
+ return { type: 'STRING', value: this.scanString(), position: pos };
222
+ }
223
+
224
+ // Number or -Infinity
225
+ if (ch === 45 || (ch >= 48 && ch <= 57)) { // '-' or '0'-'9'
226
+ // Check for -Infinity
227
+ if (ch === 45 && input.slice(pos, pos + 9) === '-Infinity') {
228
+ this.pos = pos + 9;
229
+ return { type: 'NEG_INFINITY', position: pos };
230
+ }
231
+ return { type: 'NUMBER', value: this.scanNumber(), position: pos };
232
+ }
233
+
234
+ // Keywords - check first character for fast path
235
+ const remaining = input.slice(pos);
236
+
237
+ if (ch === 73) { // 'I'
238
+ if (remaining.startsWith('Infinity')) {
239
+ this.pos = pos + 8;
240
+ return { type: 'INFINITY', position: pos };
241
+ }
242
+ } else if (ch === 78) { // 'N'
243
+ if (remaining.startsWith('NaN')) {
244
+ this.pos = pos + 3;
245
+ return { type: 'NAN', position: pos };
246
+ }
247
+ } else if (ch === 116) { // 't'
248
+ if (remaining.startsWith('true')) {
249
+ this.pos = pos + 4;
250
+ return { type: 'TRUE', position: pos };
251
+ }
252
+ } else if (ch === 102) { // 'f'
253
+ if (remaining.startsWith('false')) {
254
+ this.pos = pos + 5;
255
+ return { type: 'FALSE', position: pos };
256
+ }
257
+ } else if (ch === 110) { // 'n'
258
+ if (remaining.startsWith('null')) {
259
+ this.pos = pos + 4;
260
+ return { type: 'NULL', position: pos };
261
+ }
262
+ } else if (ch === 100) { // 'd'
263
+ if (remaining.startsWith('date:')) {
264
+ this.pos = pos + 5;
265
+ const start = this.pos;
266
+ // Scan until whitespace or delimiter
267
+ while (this.pos < this.len) {
268
+ const c = input.charCodeAt(this.pos);
269
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 44 || c === 125 || c === 93) { // whitespace or , } ]
270
+ break;
271
+ }
272
+ this.pos++;
273
+ }
274
+ const dateStr = input.slice(start, this.pos);
275
+ return { type: 'DATE', value: dateStr, position: pos };
276
+ }
277
+ } else if (ch === 109) { // 'm'
278
+ if (remaining.startsWith('map')) {
279
+ this.pos = pos + 3;
280
+ return { type: 'MAP', position: pos };
281
+ }
282
+ } else if (ch === 115) { // 's'
283
+ if (remaining.startsWith('set')) {
284
+ this.pos = pos + 3;
285
+ return { type: 'SET', position: pos };
286
+ }
287
+ }
288
+
289
+ throw new Error(`Unexpected character '${input[pos]}' at position ${pos}`);
290
+ }
291
+ }
292
+
293
+ class Parser {
294
+ private tokenizer: Tokenizer;
295
+ private currentToken: Token;
296
+
297
+ constructor(input: string) {
298
+ this.tokenizer = new Tokenizer(input);
299
+ this.currentToken = this.tokenizer.nextToken();
300
+ }
301
+
302
+ private advance(): void {
303
+ this.currentToken = this.tokenizer.nextToken();
304
+ }
305
+
306
+ private expect(type: TokenType): Token {
307
+ const token = this.currentToken;
308
+ if (token.type !== type) {
309
+ throw new Error(
310
+ `Expected ${type} but got ${token.type} at position ${token.position}`
311
+ );
312
+ }
313
+ this.advance();
314
+ return token;
315
+ }
316
+
317
+ private parseValue(): unknown {
318
+ const token = this.currentToken;
319
+ const type = token.type;
320
+
321
+ // Inline simple cases for speed
322
+ switch (type) {
323
+ case 'STRING':
324
+ this.advance();
325
+ return token.value;
326
+
327
+ case 'NUMBER':
328
+ this.advance();
329
+ return token.value;
330
+
331
+ case 'TRUE':
332
+ this.advance();
333
+ return true;
334
+
335
+ case 'FALSE':
336
+ this.advance();
337
+ return false;
338
+
339
+ case 'NULL':
340
+ this.advance();
341
+ return null;
342
+
343
+ case 'NAN':
344
+ this.advance();
345
+ return NaN;
346
+
347
+ case 'INFINITY':
348
+ this.advance();
349
+ return Infinity;
350
+
351
+ case 'NEG_INFINITY':
352
+ this.advance();
353
+ return -Infinity;
354
+
355
+ case 'DATE': {
356
+ this.advance();
357
+ const dateStr = token.value as string;
358
+ const date = new Date(dateStr);
359
+ const time = date.getTime();
360
+ if (time !== time) { // isNaN check
361
+ throw new Error(`Invalid date string "${dateStr}" at position ${token.position}`);
362
+ }
363
+ return date;
364
+ }
365
+
366
+ case 'LBRACKET': {
367
+ this.advance(); // consume '['
368
+ const arr: unknown[] = [];
369
+
370
+ if (this.currentToken.type === 'RBRACKET') {
371
+ this.advance();
372
+ return arr;
373
+ }
374
+
375
+ while (true) {
376
+ arr.push(this.parseValue());
377
+
378
+ const currentType = this.currentToken.type as TokenType;
379
+ if (currentType === 'COMMA') {
380
+ this.advance();
381
+ continue;
382
+ }
383
+ if (currentType === 'RBRACKET') {
384
+ this.advance();
385
+ break;
386
+ }
387
+ throw new Error(
388
+ `Expected COMMA or RBRACKET but got ${currentType} at position ${this.currentToken.position}`
389
+ );
390
+ }
391
+
392
+ return arr;
393
+ }
394
+
395
+ case 'LBRACE': {
396
+ this.advance(); // consume '{'
397
+ const obj: Record<string, unknown> = {};
398
+
399
+ if (this.currentToken.type === 'RBRACE') {
400
+ this.advance();
401
+ return obj;
402
+ }
403
+
404
+ while (true) {
405
+ const keyToken = this.currentToken;
406
+ if (keyToken.type !== 'STRING') {
407
+ throw new Error(
408
+ `Expected STRING but got ${keyToken.type} at position ${keyToken.position}`
409
+ );
410
+ }
411
+ this.advance();
412
+ const key = keyToken.value as string;
413
+
414
+ this.expect('COLON');
415
+
416
+ obj[key] = this.parseValue();
417
+
418
+ const currentType = this.currentToken.type as TokenType;
419
+ if (currentType === 'COMMA') {
420
+ this.advance();
421
+ continue;
422
+ }
423
+ if (currentType === 'RBRACE') {
424
+ this.advance();
425
+ break;
426
+ }
427
+ throw new Error(
428
+ `Expected COMMA or RBRACE but got ${currentType} at position ${this.currentToken.position}`
429
+ );
430
+ }
431
+
432
+ return obj;
433
+ }
434
+
435
+ case 'MAP': {
436
+ this.advance(); // consume 'map'
437
+ this.expect('LBRACE');
438
+
439
+ const map = new Map();
440
+
441
+ if (this.currentToken.type === 'RBRACE') {
442
+ this.advance();
443
+ return map;
444
+ }
445
+
446
+ while (true) {
447
+ const key = this.parseValue();
448
+ this.expect('COLON');
449
+ const value = this.parseValue();
450
+ map.set(key, value);
451
+
452
+ const currentType = this.currentToken.type as TokenType;
453
+ if (currentType === 'COMMA') {
454
+ this.advance();
455
+ continue;
456
+ }
457
+ if (currentType === 'RBRACE') {
458
+ this.advance();
459
+ break;
460
+ }
461
+ throw new Error(
462
+ `Expected COMMA or RBRACE but got ${currentType} at position ${this.currentToken.position}`
463
+ );
464
+ }
465
+
466
+ return map;
467
+ }
468
+
469
+ case 'SET': {
470
+ this.advance(); // consume 'set'
471
+ this.expect('LBRACE');
472
+
473
+ const set = new Set();
474
+
475
+ if (this.currentToken.type === 'RBRACE') {
476
+ this.advance();
477
+ return set;
478
+ }
479
+
480
+ while (true) {
481
+ set.add(this.parseValue());
482
+
483
+ const currentType = this.currentToken.type as TokenType;
484
+ if (currentType === 'COMMA') {
485
+ this.advance();
486
+ continue;
487
+ }
488
+ if (currentType === 'RBRACE') {
489
+ this.advance();
490
+ break;
491
+ }
492
+ throw new Error(
493
+ `Expected COMMA or RBRACE but got ${currentType} at position ${this.currentToken.position}`
494
+ );
495
+ }
496
+
497
+ return set;
498
+ }
499
+
500
+ default:
501
+ throw new Error(
502
+ `Unexpected token ${type} at position ${token.position}`
503
+ );
504
+ }
505
+ }
506
+
507
+ parse(): unknown {
508
+ const value = this.parseValue();
509
+ if (this.currentToken.type !== 'EOF') {
510
+ throw new Error(
511
+ `Expected EOF but got ${this.currentToken.type} at position ${this.currentToken.position}`
512
+ );
513
+ }
514
+ return value;
515
+ }
516
+ }
517
+
518
+ const parser = new Parser(ionExpression);
519
+ return parser.parse();
520
+ },
521
+
522
+ stringify(object: unknown): string {
523
+ const seen = new WeakSet<object>();
524
+
525
+ function stringifyValue(value: unknown): string {
526
+ // Handle null first (most common primitive in many use cases)
527
+ if (value === null) return 'null';
528
+
529
+ const type = typeof value;
530
+
531
+ // Handle primitives (optimized order: most common first)
532
+ if (type === 'string') {
533
+ // Inline simple string escaping for common cases
534
+ const len = (value as string).length;
535
+ let needsEscape = false;
536
+ for (let i = 0; i < len; i++) {
537
+ const ch = (value as string).charCodeAt(i);
538
+ if (ch === 34 || ch === 92 || ch < 32) {
539
+ needsEscape = true;
540
+ break;
541
+ }
542
+ }
543
+ if (!needsEscape) {
544
+ return `"${value}"`;
545
+ }
546
+ return JSON.stringify(value);
547
+ }
548
+
549
+ if (type === 'number') {
550
+ // Fast path for normal numbers
551
+ if (value === (value as number)) { // NaN check (NaN !== NaN)
552
+ if (isFinite(value as number)) {
553
+ return String(value);
554
+ }
555
+ // Handle Infinity
556
+ return value === Infinity ? 'Infinity' : '-Infinity';
557
+ }
558
+ return 'NaN';
559
+ }
560
+
561
+ if (type === 'boolean') {
562
+ return value ? 'true' : 'false';
563
+ }
564
+
565
+ if (value === undefined) {
566
+ throw new Error('Cannot serialize undefined');
567
+ }
568
+
569
+ // Handle unsupported primitives early
570
+ if (type === 'bigint') {
571
+ throw new Error('Cannot serialize BigInt');
572
+ }
573
+ if (type === 'function') {
574
+ throw new Error('Cannot serialize function');
575
+ }
576
+ if (type === 'symbol') {
577
+ throw new Error('Cannot serialize symbol');
578
+ }
579
+
580
+ // Handle objects - type === 'object' at this point
581
+ // Circular reference check
582
+ if (seen.has(value as object)) {
583
+ throw new Error('Circular reference detected');
584
+ }
585
+ seen.add(value as object);
586
+
587
+ try {
588
+ // Order by frequency: Array -> Plain Object -> Date -> Map -> Set -> Unsupported
589
+
590
+ // Handle Array (very common)
591
+ if (Array.isArray(value)) {
592
+ const len = value.length;
593
+ if (len === 0) return '[]';
594
+
595
+ let result = '[';
596
+ for (let i = 0; i < len; i++) {
597
+ if (i > 0) result += ', ';
598
+ result += stringifyValue(value[i]);
599
+ }
600
+ result += ']';
601
+ return result;
602
+ }
603
+
604
+ // Handle Date before checking for plain objects
605
+ if (value instanceof Date) {
606
+ const time = value.getTime();
607
+ if (time !== time) { // isNaN check
608
+ throw new Error('Invalid Date');
609
+ }
610
+ return `date:${value.toISOString()}`;
611
+ }
612
+
613
+ // Handle Map
614
+ if (value instanceof Map) {
615
+ if (value.size === 0) return 'map { }';
616
+
617
+ let result = 'map { ';
618
+ let first = true;
619
+ for (const [k, v] of value) {
620
+ if (!first) result += ', ';
621
+ first = false;
622
+ result += stringifyValue(k);
623
+ result += ': ';
624
+ result += stringifyValue(v);
625
+ }
626
+ result += ' }';
627
+ return result;
628
+ }
629
+
630
+ // Handle Set
631
+ if (value instanceof Set) {
632
+ if (value.size === 0) return 'set { }';
633
+
634
+ let result = 'set { ';
635
+ let first = true;
636
+ for (const v of value) {
637
+ if (!first) result += ', ';
638
+ first = false;
639
+ result += stringifyValue(v);
640
+ }
641
+ result += ' }';
642
+ return result;
643
+ }
644
+
645
+ // Handle WeakMap/WeakSet (must check before plain objects)
646
+ if (value instanceof WeakMap) {
647
+ throw new Error('Cannot serialize WeakMap');
648
+ }
649
+ if (value instanceof WeakSet) {
650
+ throw new Error('Cannot serialize WeakSet');
651
+ }
652
+
653
+ // Handle plain objects (most common object type)
654
+ const keys = Object.keys(value);
655
+ const len = keys.length;
656
+ if (len === 0) return '{}';
657
+
658
+ let result = '{ ';
659
+ let first = true;
660
+ for (let i = 0; i < len; i++) {
661
+ const key = keys[i]!;
662
+ const val = (value as Record<string, unknown>)[key];
663
+
664
+ // Skip undefined properties
665
+ if (val === undefined) continue;
666
+
667
+ if (!first) result += ', ';
668
+ first = false;
669
+
670
+ // Inline string key escaping
671
+ let keyStr: string;
672
+ const keyLen = key.length;
673
+ let needsEscape = false;
674
+ for (let j = 0; j < keyLen; j++) {
675
+ const ch = key.charCodeAt(j);
676
+ if (ch === 34 || ch === 92 || ch < 32) {
677
+ needsEscape = true;
678
+ break;
679
+ }
680
+ }
681
+ keyStr = needsEscape ? JSON.stringify(key) : `"${key}"`;
682
+
683
+ result += keyStr;
684
+ result += ': ';
685
+ result += stringifyValue(val);
686
+ }
687
+ result += ' }';
688
+ return result;
689
+ } finally {
690
+ seen.delete(value as object);
691
+ }
692
+ }
693
+
694
+ return stringifyValue(object);
695
+ },
696
+ }
697
+
698
+ export default ION;
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@covenant-rpc/ion",
3
+ "module": "index.ts",
4
+ "version": "1.0.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "exports": {
10
+ ".": "./index.ts"
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "latest"
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5"
17
+ }
18
+ }