@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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @covenant-rpc/ion
2
+
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8061179: Initial publish
8
+
9
+ ## 1.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix all packages
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+
2
+
3
+ # `ION`
4
+
5
+ `ION` - Isomorphic Object Notation
6
+
7
+ ## Purpose
8
+
9
+ `ION` is a `JSON` like object serialization format. If given the following code:
10
+
11
+ ```ts
12
+ function isomorphicSerialize(obj: any) {
13
+ const serialized = ION.stringify(obj);
14
+ const deserialized = ION.parse(obj);
15
+ }
16
+ ```
17
+
18
+ It guarantees that either:
19
+ - `ION.stringify` throws an error
20
+ - `deserialized` and `obj` are identical objects
21
+
22
+
23
+ `JSON` fails in various circumstances at this:
24
+ - `Date`s become strings
25
+ - `undefined` becomes null
26
+ - `Symbol`s are omitted
27
+ - `NaN`, `Infinity`, and `-Infinity` become null
28
+ - Sets and maps become `{}`
29
+
30
+ In cases where something can't be perfectly recreated from parsing, ION will throw an error.
31
+
32
+ ## Performance
33
+
34
+ ION is optimized for performance while maintaining type safety guarantees:
35
+
36
+ **Stringify Performance:**
37
+ - Small objects: ~3.2x slower than JSON
38
+ - Medium objects: ~3.5x slower than JSON
39
+ - Large arrays (100 items): ~1.8x slower than JSON
40
+ - String-heavy data: ~2.1x slower than JSON
41
+
42
+ **Parse Performance:**
43
+ - Small objects: ~8.6x slower than JSON
44
+ - Medium objects: ~5.6x slower than JSON
45
+ - Large arrays (100 items): ~5.5x slower than JSON
46
+ - String-heavy data: ~2.4x slower than JSON
47
+
48
+ **Average:** ION stringify is ~2.2x slower and parse is ~5.7x slower than JSON, while providing complete type preservation and safety guarantees that JSON cannot offer.
49
+
50
+
51
+ ## Syntax
52
+
53
+
54
+ `ION` is a superset of `JSON`. Any valid `JSON` is valid `ION`, although not all objects are translated exactly the same.
55
+
56
+
57
+ ```ion
58
+ {
59
+ "string": "hello",
60
+ "number": 123,
61
+ "array": [
62
+ {
63
+ "name": null,
64
+ "boolean": true,
65
+ }
66
+ ]
67
+ }
68
+ ```
69
+
70
+ `JSON` has support for several types: `boolean`, `number`, `string`, `object`, and `array`. `ION` adds a couple more:
71
+ - `Infinity`
72
+ - `-Infinity`
73
+ - `NaN`
74
+ - `Date`
75
+ - `Map`
76
+ - `Set`
77
+
78
+ Note: `WeakMap` and `WeakSet` cannot be serialized and will throw an error if encountered.
79
+
80
+
81
+ If an object has any `undefined` properties, they are simply omitted. These advanced properties look like:
82
+
83
+ ```ion
84
+ {
85
+ "date": date:2026-01-27T15:30:00Z,
86
+ "not-a-number": NaN,
87
+ "infinity": Infinity,
88
+ "negativeInf": -Infinity,
89
+ "someMap": map {
90
+ "property": 1,
91
+ "another": 2
92
+ },
93
+ "someSet": set {
94
+ "a",
95
+ "b"
96
+ }
97
+ }
98
+ ```
99
+
100
+
101
+
@@ -0,0 +1,184 @@
1
+ import ION from './index.ts';
2
+
3
+ console.log('='.repeat(100));
4
+ console.log('ION vs JSON: Type Preservation Comparison');
5
+ console.log('='.repeat(100));
6
+
7
+ // Test 1: Dates
8
+ console.log('\n1. Date Handling');
9
+ console.log('-'.repeat(100));
10
+ const dateObj = { timestamp: new Date('2026-01-27T15:30:00Z'), name: 'Event' };
11
+
12
+ const jsonDate = JSON.parse(JSON.stringify(dateObj));
13
+ const ionDate = ION.parse(ION.stringify(dateObj)) as typeof dateObj;
14
+
15
+ console.log('Original:', dateObj);
16
+ console.log('JSON: ', jsonDate);
17
+ console.log(' - timestamp type:', typeof jsonDate.timestamp);
18
+ console.log(' - timestamp value:', jsonDate.timestamp);
19
+ console.log('ION: ', ionDate);
20
+ console.log(' - timestamp type:', typeof ionDate.timestamp);
21
+ console.log(' - timestamp instanceof Date:', ionDate.timestamp instanceof Date);
22
+
23
+ // Test 2: NaN and Infinity
24
+ console.log('\n2. Special Numbers (NaN, Infinity)');
25
+ console.log('-'.repeat(100));
26
+ const specialNumbers = {
27
+ nan: NaN,
28
+ infinity: Infinity,
29
+ negInfinity: -Infinity,
30
+ normal: 42,
31
+ };
32
+
33
+ const jsonSpecial = JSON.parse(JSON.stringify(specialNumbers));
34
+ const ionSpecial = ION.parse(ION.stringify(specialNumbers)) as typeof specialNumbers;
35
+
36
+ console.log('Original:', specialNumbers);
37
+ console.log('JSON: ', jsonSpecial);
38
+ console.log(' - nan is NaN:', Number.isNaN(jsonSpecial.nan));
39
+ console.log(' - infinity is Infinity:', jsonSpecial.infinity === Infinity);
40
+ console.log('ION: ', ionSpecial);
41
+ console.log(' - nan is NaN:', Number.isNaN(ionSpecial.nan));
42
+ console.log(' - infinity is Infinity:', ionSpecial.infinity === Infinity);
43
+ console.log(' - negInfinity is -Infinity:', ionSpecial.negInfinity === -Infinity);
44
+
45
+ // Test 3: Maps
46
+ console.log('\n3. Map Handling');
47
+ console.log('-'.repeat(100));
48
+ const mapObj = {
49
+ config: new Map<string | number, string | number>([
50
+ ['theme', 'dark'],
51
+ ['fontSize', 14],
52
+ [123, 'numeric key'],
53
+ ]),
54
+ };
55
+
56
+ const jsonMap = JSON.parse(JSON.stringify(mapObj));
57
+ const ionMap = ION.parse(ION.stringify(mapObj)) as typeof mapObj;
58
+
59
+ console.log('Original:', mapObj);
60
+ console.log(' - config instanceof Map:', mapObj.config instanceof Map);
61
+ console.log(' - config.get("theme"):', mapObj.config.get('theme'));
62
+ console.log('JSON: ', jsonMap);
63
+ console.log(' - config instanceof Map:', jsonMap.config instanceof Map);
64
+ console.log(' - config type:', typeof jsonMap.config);
65
+ console.log('ION: ', ionMap);
66
+ console.log(' - config instanceof Map:', ionMap.config instanceof Map);
67
+ console.log(' - config.get("theme"):', ionMap.config.get('theme'));
68
+ console.log(' - config.get(123):', ionMap.config.get(123));
69
+
70
+ // Test 4: Sets
71
+ console.log('\n4. Set Handling');
72
+ console.log('-'.repeat(100));
73
+ const setObj = {
74
+ tags: new Set(['javascript', 'typescript', 'bun']),
75
+ count: 3,
76
+ };
77
+
78
+ const jsonSet = JSON.parse(JSON.stringify(setObj));
79
+ const ionSet = ION.parse(ION.stringify(setObj)) as typeof setObj;
80
+
81
+ console.log('Original:', setObj);
82
+ console.log(' - tags instanceof Set:', setObj.tags instanceof Set);
83
+ console.log(' - tags.has("typescript"):', setObj.tags.has('typescript'));
84
+ console.log('JSON: ', jsonSet);
85
+ console.log(' - tags instanceof Set:', jsonSet.tags instanceof Set);
86
+ console.log(' - tags type:', typeof jsonSet.tags);
87
+ console.log('ION: ', ionSet);
88
+ console.log(' - tags instanceof Set:', ionSet.tags instanceof Set);
89
+ console.log(' - tags.has("typescript"):', ionSet.tags.has('typescript'));
90
+
91
+ // Test 5: undefined properties
92
+ console.log('\n5. undefined Handling');
93
+ console.log('-'.repeat(100));
94
+ const undefinedObj = {
95
+ name: 'Alice',
96
+ age: undefined,
97
+ email: 'alice@example.com',
98
+ phone: undefined,
99
+ };
100
+
101
+ const jsonUndefined = JSON.parse(JSON.stringify(undefinedObj));
102
+ const ionUndefined = ION.parse(ION.stringify(undefinedObj)) as Record<string, unknown>;
103
+
104
+ console.log('Original:', undefinedObj);
105
+ console.log(' - has "age" property:', 'age' in undefinedObj);
106
+ console.log(' - has "phone" property:', 'phone' in undefinedObj);
107
+ console.log('JSON: ', jsonUndefined);
108
+ console.log(' - has "age" property:', 'age' in jsonUndefined);
109
+ console.log(' - has "phone" property:', 'phone' in jsonUndefined);
110
+ console.log('ION: ', ionUndefined);
111
+ console.log(' - has "age" property:', 'age' in ionUndefined);
112
+ console.log(' - has "phone" property:', 'phone' in ionUndefined);
113
+
114
+ // Test 6: Complex real-world scenario
115
+ console.log('\n6. Real-World Scenario: API Response with Mixed Types');
116
+ console.log('-'.repeat(100));
117
+ const apiResponse = {
118
+ user: {
119
+ id: 123,
120
+ name: 'Alice',
121
+ lastLogin: new Date('2026-01-27T15:30:00Z'),
122
+ score: NaN, // not yet calculated
123
+ preferences: new Map<string, string | boolean>([
124
+ ['theme', 'dark'],
125
+ ['notifications', true],
126
+ ]),
127
+ roles: new Set(['admin', 'user']),
128
+ optionalField: undefined,
129
+ },
130
+ metadata: {
131
+ requestTime: new Date('2026-01-27T15:35:00Z'),
132
+ version: '1.0.0',
133
+ },
134
+ };
135
+
136
+ console.log('\nOriginal object:');
137
+ console.log(' - user.lastLogin instanceof Date:', apiResponse.user.lastLogin instanceof Date);
138
+ console.log(' - user.score is NaN:', Number.isNaN(apiResponse.user.score));
139
+ console.log(' - user.preferences instanceof Map:', apiResponse.user.preferences instanceof Map);
140
+ console.log(' - user.roles instanceof Set:', apiResponse.user.roles instanceof Set);
141
+ console.log(' - user has "optionalField":', 'optionalField' in apiResponse.user);
142
+
143
+ const jsonApi = JSON.parse(JSON.stringify(apiResponse));
144
+ console.log('\nJSON round-trip:');
145
+ console.log(' - user.lastLogin instanceof Date:', jsonApi.user.lastLogin instanceof Date);
146
+ console.log(' - user.lastLogin type:', typeof jsonApi.user.lastLogin);
147
+ console.log(' - user.score is NaN:', Number.isNaN(jsonApi.user.score));
148
+ console.log(' - user.score value:', jsonApi.user.score);
149
+ console.log(' - user.preferences instanceof Map:', jsonApi.user.preferences instanceof Map);
150
+ console.log(' - user.roles instanceof Set:', jsonApi.user.roles instanceof Set);
151
+ console.log(' - user has "optionalField":', 'optionalField' in jsonApi.user);
152
+
153
+ const ionApi = ION.parse(ION.stringify(apiResponse)) as typeof apiResponse;
154
+ console.log('\nION round-trip:');
155
+ console.log(' - user.lastLogin instanceof Date:', ionApi.user.lastLogin instanceof Date);
156
+ console.log(' - user.score is NaN:', Number.isNaN(ionApi.user.score));
157
+ console.log(' - user.preferences instanceof Map:', ionApi.user.preferences instanceof Map);
158
+ console.log(' - user.preferences.get("theme"):', ionApi.user.preferences.get('theme'));
159
+ console.log(' - user.roles instanceof Set:', ionApi.user.roles instanceof Set);
160
+ console.log(' - user.roles.has("admin"):', ionApi.user.roles.has('admin'));
161
+ console.log(' - user has "optionalField":', 'optionalField' in ionApi.user);
162
+
163
+ // Summary
164
+ console.log('\n' + '='.repeat(100));
165
+ console.log('Summary');
166
+ console.log('='.repeat(100));
167
+ console.log('\nJSON Issues:');
168
+ console.log(' ✗ Dates become strings (need manual conversion)');
169
+ console.log(' ✗ NaN becomes null (information loss)');
170
+ console.log(' ✗ Infinity becomes null (information loss)');
171
+ console.log(' ✗ Maps become empty objects (data loss)');
172
+ console.log(' ✗ Sets become empty objects (data loss)');
173
+ console.log(' ✓ undefined properties are removed');
174
+
175
+ console.log('\nION Guarantees:');
176
+ console.log(' ✓ Dates preserved as Date objects');
177
+ console.log(' ✓ NaN preserved as NaN');
178
+ console.log(' ✓ Infinity preserved as Infinity');
179
+ console.log(' ✓ Maps preserved with all entries and types');
180
+ console.log(' ✓ Sets preserved with all values');
181
+ console.log(' ✓ undefined properties are removed');
182
+ console.log(' ✓ Throws errors for unsupported types (WeakMap, functions, etc.)');
183
+
184
+ console.log('\n' + '='.repeat(100) + '\n');
package/benchmark.ts ADDED
@@ -0,0 +1,183 @@
1
+ import ION from './index.ts';
2
+
3
+ interface BenchmarkResult {
4
+ name: string;
5
+ jsonStringify: number;
6
+ ionStringify: number;
7
+ jsonParse: number;
8
+ ionParse: number;
9
+ jsonSize: number;
10
+ ionSize: number;
11
+ }
12
+
13
+ function benchmark(name: string, data: unknown, iterations = 10000): BenchmarkResult {
14
+ // Warmup
15
+ for (let i = 0; i < 100; i++) {
16
+ JSON.stringify(data);
17
+ ION.stringify(data);
18
+ }
19
+
20
+ // Benchmark JSON.stringify
21
+ const jsonStringifyStart = performance.now();
22
+ let jsonStr = '';
23
+ for (let i = 0; i < iterations; i++) {
24
+ jsonStr = JSON.stringify(data);
25
+ }
26
+ const jsonStringifyTime = performance.now() - jsonStringifyStart;
27
+
28
+ // Benchmark ION.stringify
29
+ const ionStringifyStart = performance.now();
30
+ let ionStr = '';
31
+ for (let i = 0; i < iterations; i++) {
32
+ ionStr = ION.stringify(data);
33
+ }
34
+ const ionStringifyTime = performance.now() - ionStringifyStart;
35
+
36
+ // Benchmark JSON.parse
37
+ const jsonParseStart = performance.now();
38
+ for (let i = 0; i < iterations; i++) {
39
+ JSON.parse(jsonStr);
40
+ }
41
+ const jsonParseTime = performance.now() - jsonParseStart;
42
+
43
+ // Benchmark ION.parse
44
+ const ionParseStart = performance.now();
45
+ for (let i = 0; i < iterations; i++) {
46
+ ION.parse(ionStr);
47
+ }
48
+ const ionParseTime = performance.now() - ionParseStart;
49
+
50
+ return {
51
+ name,
52
+ jsonStringify: jsonStringifyTime,
53
+ ionStringify: ionStringifyTime,
54
+ jsonParse: jsonParseTime,
55
+ ionParse: ionParseTime,
56
+ jsonSize: jsonStr.length,
57
+ ionSize: ionStr.length,
58
+ };
59
+ }
60
+
61
+ function formatTime(ms: number): string {
62
+ if (ms < 1) {
63
+ return `${(ms * 1000).toFixed(2)}μs`;
64
+ }
65
+ return `${ms.toFixed(2)}ms`;
66
+ }
67
+
68
+ function formatRatio(ion: number, json: number): string {
69
+ const ratio = ion / json;
70
+ if (ratio < 1) {
71
+ return `${(1 / ratio).toFixed(2)}x faster`;
72
+ } else {
73
+ return `${ratio.toFixed(2)}x slower`;
74
+ }
75
+ }
76
+
77
+ function printResults(results: BenchmarkResult[]): void {
78
+ console.log('\n' + '='.repeat(100));
79
+ console.log('ION vs JSON Benchmark Results');
80
+ console.log('='.repeat(100));
81
+
82
+ for (const result of results) {
83
+ console.log(`\n${result.name}`);
84
+ console.log('-'.repeat(100));
85
+
86
+ console.log('\nStringify:');
87
+ console.log(` JSON: ${formatTime(result.jsonStringify)}`);
88
+ console.log(` ION: ${formatTime(result.ionStringify)} (${formatRatio(result.ionStringify, result.jsonStringify)})`);
89
+
90
+ console.log('\nParse:');
91
+ console.log(` JSON: ${formatTime(result.jsonParse)}`);
92
+ console.log(` ION: ${formatTime(result.ionParse)} (${formatRatio(result.ionParse, result.jsonParse)})`);
93
+
94
+ console.log('\nSize:');
95
+ console.log(` JSON: ${result.jsonSize} bytes`);
96
+ console.log(` ION: ${result.ionSize} bytes (${result.ionSize > result.jsonSize ? '+' : ''}${result.ionSize - result.jsonSize} bytes)`);
97
+ }
98
+
99
+ console.log('\n' + '='.repeat(100));
100
+ console.log('Summary');
101
+ console.log('='.repeat(100));
102
+
103
+ const avgJsonStringify = results.reduce((sum, r) => sum + r.jsonStringify, 0) / results.length;
104
+ const avgIonStringify = results.reduce((sum, r) => sum + r.ionStringify, 0) / results.length;
105
+ const avgJsonParse = results.reduce((sum, r) => sum + r.jsonParse, 0) / results.length;
106
+ const avgIonParse = results.reduce((sum, r) => sum + r.ionParse, 0) / results.length;
107
+
108
+ console.log(`\nAverage Stringify: JSON ${formatTime(avgJsonStringify)}, ION ${formatTime(avgIonStringify)}`);
109
+ console.log(`Average Parse: JSON ${formatTime(avgJsonParse)}, ION ${formatTime(avgIonParse)}`);
110
+ console.log(`\nION is ${formatRatio(avgIonStringify, avgJsonStringify)} for stringify`);
111
+ console.log(`ION is ${formatRatio(avgIonParse, avgJsonParse)} for parse`);
112
+ console.log('='.repeat(100) + '\n');
113
+ }
114
+
115
+ // Test data
116
+ const smallObject = {
117
+ name: 'Alice',
118
+ age: 30,
119
+ active: true,
120
+ };
121
+
122
+ const mediumObject = {
123
+ users: [
124
+ { id: 1, name: 'Alice', email: 'alice@example.com', active: true },
125
+ { id: 2, name: 'Bob', email: 'bob@example.com', active: false },
126
+ { id: 3, name: 'Charlie', email: 'charlie@example.com', active: true },
127
+ ],
128
+ metadata: {
129
+ version: '1.0.0',
130
+ created: '2026-01-27',
131
+ count: 3,
132
+ },
133
+ };
134
+
135
+ const largeArray = Array.from({ length: 100 }, (_, i) => ({
136
+ id: i,
137
+ name: `User ${i}`,
138
+ email: `user${i}@example.com`,
139
+ score: Math.random() * 100,
140
+ active: i % 2 === 0,
141
+ }));
142
+
143
+ const deepNesting = {
144
+ level1: {
145
+ level2: {
146
+ level3: {
147
+ level4: {
148
+ level5: {
149
+ data: 'deep value',
150
+ numbers: [1, 2, 3, 4, 5],
151
+ },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ };
157
+
158
+ const stringHeavy = {
159
+ description: 'This is a longer description with multiple sentences. '.repeat(10),
160
+ comments: Array.from({ length: 20 }, (_, i) => `Comment number ${i} with some text content`),
161
+ };
162
+
163
+ // Run benchmarks
164
+ console.log('Running benchmarks...\n');
165
+
166
+ const results: BenchmarkResult[] = [];
167
+
168
+ console.log('[1/5] Small object...');
169
+ results.push(benchmark('Small Object (3 properties)', smallObject, 50000));
170
+
171
+ console.log('[2/5] Medium object...');
172
+ results.push(benchmark('Medium Object (users array + metadata)', mediumObject, 20000));
173
+
174
+ console.log('[3/5] Large array...');
175
+ results.push(benchmark('Large Array (100 objects)', largeArray, 5000));
176
+
177
+ console.log('[4/5] Deep nesting...');
178
+ results.push(benchmark('Deep Nesting (5 levels)', deepNesting, 50000));
179
+
180
+ console.log('[5/5] String heavy...');
181
+ results.push(benchmark('String Heavy (long strings + array)', stringHeavy, 10000));
182
+
183
+ printResults(results);