@jucie.io/state 1.0.1
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 +61 -0
- package/core/README.md +635 -0
- package/dist/Plugin.js +36 -0
- package/dist/State.d.ts +44 -0
- package/dist/State.js +243 -0
- package/dist/admin/binary.js +90 -0
- package/dist/admin/buffer.js +174 -0
- package/dist/admin/pack.js +67 -0
- package/dist/admin/unpack.js +88 -0
- package/dist/lib/TOKENS.js +18 -0
- package/dist/lib/change.js +94 -0
- package/dist/lib/global.js +42 -0
- package/dist/lib/gsru.js +125 -0
- package/dist/lib/marker.js +233 -0
- package/dist/lib/pathEncoder.js +89 -0
- package/dist/lib/tree/mutate.js +193 -0
- package/dist/lib/tree/seek.js +66 -0
- package/dist/lib/tree/traverse.js +38 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +7 -0
- package/dist/plugins/history.js +2 -0
- package/dist/plugins/history.js.map +7 -0
- package/dist/plugins/matcher.js +2 -0
- package/dist/plugins/matcher.js.map +7 -0
- package/dist/plugins/on-change.js +2 -0
- package/dist/plugins/on-change.js.map +7 -0
- package/dist/utils/clone.js +7 -0
- package/dist/utils/convertStringToExpression.js +23 -0
- package/dist/utils/convertStringToFunction.js +17 -0
- package/dist/utils/defer.js +24 -0
- package/dist/utils/isAsync.js +4 -0
- package/dist/utils/isPrimitive.js +12 -0
- package/dist/utils/isPromise.js +1 -0
- package/dist/utils/nextIdleTick.js +23 -0
- package/package.json +81 -0
- package/plugins/history/README.md +320 -0
- package/plugins/matcher/README.md +402 -0
- package/plugins/on-change/README.md +444 -0
package/dist/Plugin.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class Plugin {
|
|
2
|
+
|
|
3
|
+
static name = null;
|
|
4
|
+
static options = {};
|
|
5
|
+
|
|
6
|
+
static configure(options) {
|
|
7
|
+
options = {...this.options, ...options};
|
|
8
|
+
return {
|
|
9
|
+
install: (state) => this.install(state, options),
|
|
10
|
+
name: this.name,
|
|
11
|
+
options
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static install(state, options) {
|
|
16
|
+
options = {...this.options, ...options};
|
|
17
|
+
const pluginInstance = new this(state, options);
|
|
18
|
+
|
|
19
|
+
Object.defineProperty(pluginInstance, 'state', {
|
|
20
|
+
value: state,
|
|
21
|
+
writable: false,
|
|
22
|
+
configurable: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
Object.defineProperty(pluginInstance, 'options', {
|
|
26
|
+
value: options,
|
|
27
|
+
writable: false,
|
|
28
|
+
configurable: false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
return pluginInstance;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Plugin;
|
package/dist/State.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jucie-state/state - State management module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface StateValue {
|
|
6
|
+
get(path: string[]): any;
|
|
7
|
+
set(path: string[], value: any): void;
|
|
8
|
+
update(path: string[], fn: (value: any) => any): void;
|
|
9
|
+
delete(path: string[]): void;
|
|
10
|
+
keys(path?: string[]): string[];
|
|
11
|
+
has(path: string[]): boolean;
|
|
12
|
+
subscribe(path: string[], callback: (value: any) => void): () => void;
|
|
13
|
+
unsubscribe(path: string[], callback: (value: any) => void): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StateExport {
|
|
17
|
+
exportState(path?: string[]): any;
|
|
18
|
+
importState(data: any, path?: string[]): void;
|
|
19
|
+
clearState(path?: string[]): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class State {
|
|
22
|
+
static create(config?: any): State;
|
|
23
|
+
static createStateExport(config?: any): StateExport;
|
|
24
|
+
|
|
25
|
+
get(path: string[]): any;
|
|
26
|
+
set(path: string[], value: any): void;
|
|
27
|
+
update(path: string[], fn: (value: any) => any): void;
|
|
28
|
+
delete(path: string[]): void;
|
|
29
|
+
keys(path?: string[]): string[];
|
|
30
|
+
has(path: string[]): boolean;
|
|
31
|
+
subscribe(path: string[], callback: (value: any) => void): () => void;
|
|
32
|
+
unsubscribe(path: string[], callback: (value: any) => void): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Global state utilities
|
|
37
|
+
export declare function provideState(state: State): void;
|
|
38
|
+
export declare function hasState(): boolean;
|
|
39
|
+
|
|
40
|
+
export declare function createState(config?: any): State;
|
|
41
|
+
export declare function createStateExport(config?: any): StateExport;
|
|
42
|
+
export declare function useState(path: string[]): any;
|
|
43
|
+
export declare function forceState(path: string[], value: any): void;
|
|
44
|
+
export declare function clearState(path: string[]): void;
|
package/dist/State.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { get, set, remove, update } from './lib/gsru.js';
|
|
2
|
+
import { createMarker, createChildMarker, dispatch } from './lib/marker.js';
|
|
3
|
+
import { pack } from './admin/pack.js';
|
|
4
|
+
import { unpack } from './admin/unpack.js';
|
|
5
|
+
import { clear } from './lib/tree/mutate.js';
|
|
6
|
+
import { findFirstNode, findAllNodes } from './lib/tree/seek.js';
|
|
7
|
+
import { CREATED, DELETED, UPDATED } from './lib/TOKENS.js';
|
|
8
|
+
import { createChange } from './lib/change.js';
|
|
9
|
+
|
|
10
|
+
export class State {
|
|
11
|
+
#state;
|
|
12
|
+
#marker;
|
|
13
|
+
#batchDepth = 0;
|
|
14
|
+
#changeHandler;
|
|
15
|
+
#accessHandler;
|
|
16
|
+
#plugins = new Map();
|
|
17
|
+
|
|
18
|
+
static create (state = {}) {
|
|
19
|
+
const instance = new State(state);
|
|
20
|
+
|
|
21
|
+
return instance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
constructor(state, marker = null, changeHandler = null, accessHandler = null) {
|
|
25
|
+
this.#state = state;
|
|
26
|
+
this.#changeHandler = changeHandler || this.#createChangeHandler();
|
|
27
|
+
this.#accessHandler = accessHandler || this.#createAccessHandler();
|
|
28
|
+
this.#marker = marker || createMarker();
|
|
29
|
+
this.#batchDepth = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
install(...plugins) {
|
|
33
|
+
const initializeFunctions = new Map();
|
|
34
|
+
|
|
35
|
+
for (const plugin of plugins) {
|
|
36
|
+
if (this.#plugins.has(plugin.name)) {
|
|
37
|
+
console.warn(`Plugin "${plugin.name}" already installed`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!plugin.install) {
|
|
42
|
+
console.warn(`Plugin "${plugin.name}" does not have an install function`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pluginInstance = plugin.install(this);
|
|
47
|
+
this.#plugins.set(plugin.name, pluginInstance);
|
|
48
|
+
|
|
49
|
+
if (pluginInstance.initialize) {
|
|
50
|
+
initializeFunctions.set(plugin.name, () => pluginInstance.initialize(this, pluginInstance.options));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (pluginInstance.actions) {
|
|
54
|
+
const actions = pluginInstance.actions(this, pluginInstance.options);
|
|
55
|
+
Object.defineProperty(this, plugin.name, {
|
|
56
|
+
value: actions,
|
|
57
|
+
writable: false,
|
|
58
|
+
enumerable: true,
|
|
59
|
+
configurable: false
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
initializeFunctions.forEach(initialize => initialize());
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get batching() {
|
|
69
|
+
return this.#batchDepth > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Core State Accessors
|
|
73
|
+
get(...args) {
|
|
74
|
+
return get(this.#state, createChildMarker(this.#marker, args), this.#accessHandler);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
has(...args) {
|
|
78
|
+
const marker = createChildMarker(this.#marker, args);
|
|
79
|
+
return dispatch(marker, {
|
|
80
|
+
global: () => undefined,
|
|
81
|
+
path: (marker) => get(this.#state, marker, this.#accessHandler) !== undefined,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
keys(...args) {
|
|
86
|
+
const marker = createChildMarker(this.#marker, args);
|
|
87
|
+
return dispatch(marker, {
|
|
88
|
+
global: () => Object.keys(this.#state),
|
|
89
|
+
path: (marker) => Object.keys(get(this.#state, marker, this.#accessHandler) || {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
typeof(...args) {
|
|
94
|
+
const marker = createChildMarker(this.#marker, args);
|
|
95
|
+
const value = dispatch(marker, {
|
|
96
|
+
global: () => undefined,
|
|
97
|
+
path: ({ path }) => typeof get(this.#state, path, this.#accessHandler),
|
|
98
|
+
});
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Queries
|
|
103
|
+
findWhere(key, matcher = 'is', value) {
|
|
104
|
+
const state = get(this.#state, this.#marker, this.#accessHandler);
|
|
105
|
+
return findFirstNode(state, key, matcher, value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
findAllWhere(key, matcher = 'is', value) {
|
|
109
|
+
const state = get(this.#state, this.#marker, this.#accessHandler);
|
|
110
|
+
return findAllNodes(state, key, matcher, value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mutations
|
|
114
|
+
set(...args) {
|
|
115
|
+
const value = args.pop();
|
|
116
|
+
const marker = createChildMarker(this.#marker, args);
|
|
117
|
+
// No sync tombstone check - let async processing handle conflicts
|
|
118
|
+
return set(this.#state, marker, value, 'set', this.#changeHandler);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
update(...args) {
|
|
122
|
+
const fn = args.pop();
|
|
123
|
+
const marker = createChildMarker(this.#marker, args);
|
|
124
|
+
|
|
125
|
+
// No sync tombstone check - let async processing handle conflicts
|
|
126
|
+
return update(this.#state, marker, fn, 'update', this.#changeHandler);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
apply(changeEntries) {
|
|
130
|
+
for (const changeEntry of changeEntries) {
|
|
131
|
+
const { path, operation, to } = changeEntry;
|
|
132
|
+
|
|
133
|
+
const marker = createChildMarker(this.#marker, path);
|
|
134
|
+
switch (operation) {
|
|
135
|
+
case CREATED:
|
|
136
|
+
case UPDATED:
|
|
137
|
+
set(this.#state, marker, to, 'apply', this.#changeHandler);
|
|
138
|
+
break;
|
|
139
|
+
case DELETED:
|
|
140
|
+
remove(this.#state, marker, 'apply', this.#changeHandler);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
load(value) {
|
|
147
|
+
set(this.#state, this.#marker, value, 'load', this.#changeHandler);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
remove(...args) {
|
|
152
|
+
const marker = createChildMarker(this.#marker, args);
|
|
153
|
+
return remove(this.#state, marker, 'remove', this.#changeHandler);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Lifecycle / Admin
|
|
157
|
+
async export(...args) {
|
|
158
|
+
try {
|
|
159
|
+
const marker = createChildMarker(this.#marker, args);
|
|
160
|
+
const promises = dispatch(marker, {
|
|
161
|
+
global: async () => await pack(this.#state),
|
|
162
|
+
path: async (path) => await pack(get(this.#state, path, this.#accessHandler)),
|
|
163
|
+
});
|
|
164
|
+
if (!Array.isArray(promises)) {
|
|
165
|
+
return await promises;
|
|
166
|
+
}
|
|
167
|
+
return await Promise.all(promises);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.trace(`Failed to export state: ${err.message}`);
|
|
170
|
+
throw err
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async import(exportedData) {
|
|
175
|
+
try {
|
|
176
|
+
const { data } = await unpack(exportedData);
|
|
177
|
+
this.load(data);
|
|
178
|
+
return this;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.trace(`Failed to import state: ${err.message}`);
|
|
181
|
+
throw err
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
reset() {
|
|
186
|
+
clear(this.#state);
|
|
187
|
+
for (const plugin of this.#plugins.values()) {
|
|
188
|
+
if (plugin.reset) {
|
|
189
|
+
plugin.reset();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
this.#changeHandler = this.#createChangeHandler();
|
|
193
|
+
this.#marker = createMarker([], this);
|
|
194
|
+
this.#batchDepth = 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// Reactivity Utilities
|
|
199
|
+
ref(...path) {
|
|
200
|
+
const marker = createChildMarker(this.#marker, path);
|
|
201
|
+
return new State(this.#state, marker, this.#changeHandler, this.#accessHandler);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
batch(fn) {
|
|
205
|
+
this.#batchDepth++;
|
|
206
|
+
this.#notifyPlugins('onBatchStart', this.batching);
|
|
207
|
+
if (fn) {
|
|
208
|
+
fn(this);
|
|
209
|
+
return this.endBatch();
|
|
210
|
+
}
|
|
211
|
+
return () => this.endBatch();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
endBatch() {
|
|
215
|
+
this.#batchDepth--;
|
|
216
|
+
if (this.#batchDepth === 0) {
|
|
217
|
+
this.#notifyPlugins('onBatchEnd');
|
|
218
|
+
}
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Private change handler
|
|
223
|
+
#createChangeHandler() {
|
|
224
|
+
return (marker, method, to = undefined, from = undefined) => {
|
|
225
|
+
this.#notifyPlugins('onStateChange', marker, createChange(marker, method, to, from), this.batching);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#createAccessHandler() {
|
|
230
|
+
return (marker) => this.#notifyPlugins('onStateAccess', marker);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#notifyPlugins(event, ...args) {
|
|
234
|
+
for (const plugin of this.#plugins.values()) {
|
|
235
|
+
if (typeof plugin[event] === 'function') {
|
|
236
|
+
plugin[event](...args);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const createState = (...args) => State.create(...args);
|
|
243
|
+
export const isState = (target) => target instanceof State;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { encode, decode } from 'cbor-x';
|
|
2
|
+
|
|
3
|
+
// Magic header for binary capsules
|
|
4
|
+
export const MAGIC = new Uint8Array([0x4A, 0x53, 0x43, 0x32]); // "JSC2" (Object Capsule v2)
|
|
5
|
+
export const VERSION = 2;
|
|
6
|
+
|
|
7
|
+
// Base64url encoding/decoding utilities
|
|
8
|
+
export function base64urlEncode(buffer) {
|
|
9
|
+
// Process in chunks to avoid call stack overflow for large buffers
|
|
10
|
+
const chunkSize = 8192; // Process 8KB at a time
|
|
11
|
+
let binaryString = '';
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < buffer.length; i += chunkSize) {
|
|
14
|
+
const chunk = buffer.slice(i, i + chunkSize);
|
|
15
|
+
binaryString += String.fromCharCode(...chunk);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return btoa(binaryString)
|
|
19
|
+
.replace(/\+/g, '-')
|
|
20
|
+
.replace(/\//g, '_')
|
|
21
|
+
.replace(/=/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function base64urlDecode(str) {
|
|
25
|
+
const base64 = str
|
|
26
|
+
.replace(/-/g, '+')
|
|
27
|
+
.replace(/_/g, '/');
|
|
28
|
+
|
|
29
|
+
// Add padding if needed
|
|
30
|
+
const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
|
|
31
|
+
const binaryString = atob(padded);
|
|
32
|
+
|
|
33
|
+
// Convert to Uint8Array efficiently for large strings
|
|
34
|
+
const result = new Uint8Array(binaryString.length);
|
|
35
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
36
|
+
result[i] = binaryString.charCodeAt(i);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Simple checksum calculation
|
|
42
|
+
export function calculateChecksum(buffer) {
|
|
43
|
+
let checksum = 0;
|
|
44
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
45
|
+
checksum = ((checksum << 5) - checksum + buffer[i]) >>> 0; // Ensure positive 32-bit
|
|
46
|
+
}
|
|
47
|
+
return checksum;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Binary encoding utilities for capsule header
|
|
51
|
+
export function writeUint32(value, buffer, offset = 0) {
|
|
52
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
|
|
53
|
+
view.setUint32(0, value, true); // little-endian
|
|
54
|
+
return offset + 4;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readUint32(buffer, offset = 0) {
|
|
58
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
|
|
59
|
+
return view.getUint32(0, true); // little-endian
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeUint16(value, buffer, offset = 0) {
|
|
63
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 2);
|
|
64
|
+
view.setUint16(0, value, true); // little-endian
|
|
65
|
+
return offset + 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readUint16(buffer, offset = 0) {
|
|
69
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 2);
|
|
70
|
+
return view.getUint16(0, true); // little-endian
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function writeUint8(value, buffer, offset = 0) {
|
|
74
|
+
buffer[offset] = value;
|
|
75
|
+
return offset + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readUint8(buffer, offset = 0) {
|
|
79
|
+
return buffer[offset];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Object encoding using cbor-x
|
|
83
|
+
export function encodeObject(obj) {
|
|
84
|
+
return encode(obj);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function decodeObject(buffer) {
|
|
88
|
+
const obj = decode(buffer);
|
|
89
|
+
return [obj, buffer.length];
|
|
90
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export function isBrowser() {
|
|
2
|
+
return typeof window !== "undefined" && typeof window.crypto !== "undefined" && !!window.crypto.subtle;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getSubtle() {
|
|
6
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
7
|
+
// Node.js
|
|
8
|
+
const { webcrypto } = require("node:crypto");
|
|
9
|
+
return webcrypto.subtle;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// base64url encode/decode for both envs
|
|
13
|
+
export function base64urlEncode(bytes) {
|
|
14
|
+
if (typeof Buffer !== "undefined" && Buffer.from) {
|
|
15
|
+
// Node
|
|
16
|
+
return Buffer.from(bytes).toString("base64")
|
|
17
|
+
.replace(/=/g, "")
|
|
18
|
+
.replace(/\+/g, "-")
|
|
19
|
+
.replace(/\//g, "_");
|
|
20
|
+
} else {
|
|
21
|
+
// Browser
|
|
22
|
+
// Convert bytes -> binary string -> base64 -> base64url
|
|
23
|
+
let bin = "";
|
|
24
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
25
|
+
const b64 = btoa(bin);
|
|
26
|
+
return b64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function base64urlDecode(str) {
|
|
31
|
+
// restore padding
|
|
32
|
+
const pad = str.length % 4 === 2 ? "=="
|
|
33
|
+
: str.length % 4 === 3 ? "="
|
|
34
|
+
: str.length % 4 === 0 ? ""
|
|
35
|
+
: "=".repeat(4 - (str.length % 4));
|
|
36
|
+
const b64 = str.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
37
|
+
|
|
38
|
+
if (typeof Buffer !== "undefined" && Buffer.from) {
|
|
39
|
+
const buf = Buffer.from(b64, "base64");
|
|
40
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
41
|
+
} else {
|
|
42
|
+
// Browser
|
|
43
|
+
const bin = atob(b64);
|
|
44
|
+
const out = new Uint8Array(bin.length);
|
|
45
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Canonical JSON (stable ordering) to avoid hash drift due to key order
|
|
51
|
+
export function stableStringify(value) {
|
|
52
|
+
if (value === null || typeof value !== "object") {
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
const items = value.map(v => stableStringify(v)).join(",");
|
|
57
|
+
return `[${items}]`;
|
|
58
|
+
}
|
|
59
|
+
const keys = Object.keys(value).sort();
|
|
60
|
+
const props = keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",");
|
|
61
|
+
return `{${props}}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Optional replacer to omit ephemeral keys
|
|
65
|
+
export function defaultReplacer(key, value) {
|
|
66
|
+
// drop keys starting with '.' or exactly 'ephemeral'
|
|
67
|
+
if (key && key[0] === ".") return undefined;
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function applyReplacerDeep(input, replacer = defaultReplacer, parentKey = "") {
|
|
72
|
+
if (input === null || typeof input !== "object") return input;
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(input)) {
|
|
75
|
+
return input.map(v => applyReplacerDeep(v, replacer));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const k of Object.keys(input)) {
|
|
80
|
+
const v = replacer(k, input[k]);
|
|
81
|
+
if (typeof v !== "undefined") {
|
|
82
|
+
out[k] = applyReplacerDeep(v, replacer, k);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Convert path IDs with null chars to arrays for JSON serialization
|
|
89
|
+
export function pathIdsToArrays(input) {
|
|
90
|
+
if (input === null || typeof input !== "object") return input;
|
|
91
|
+
|
|
92
|
+
if (Array.isArray(input)) {
|
|
93
|
+
return input.map(v => {
|
|
94
|
+
// Check if array element is a string with null character (path ID)
|
|
95
|
+
if (typeof v === 'string' && v.includes('\0')) {
|
|
96
|
+
const pathArray = v.split('\0').map(segment => {
|
|
97
|
+
const [type, value] = segment.split(':');
|
|
98
|
+
return type === 'n' ? parseInt(value) : value;
|
|
99
|
+
});
|
|
100
|
+
return `__pathArray:${JSON.stringify(pathArray)}`;
|
|
101
|
+
}
|
|
102
|
+
return pathIdsToArrays(v);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const k of Object.keys(input)) {
|
|
108
|
+
const v = input[k];
|
|
109
|
+
// Check if key contains null character (path ID)
|
|
110
|
+
if (typeof k === 'string' && k.includes('\0')) {
|
|
111
|
+
// Convert path ID to array format
|
|
112
|
+
const pathArray = k.split('\0').map(segment => {
|
|
113
|
+
const [type, value] = segment.split(':');
|
|
114
|
+
return type === 'n' ? parseInt(value) : value;
|
|
115
|
+
});
|
|
116
|
+
out[`__pathArray:${JSON.stringify(pathArray)}`] = pathIdsToArrays(v);
|
|
117
|
+
} else {
|
|
118
|
+
out[k] = pathIdsToArrays(v);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Convert arrays back to path IDs with null chars after JSON parsing
|
|
125
|
+
export function arraysToPathIds(input) {
|
|
126
|
+
if (input === null || typeof input !== "object") return input;
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(input)) {
|
|
129
|
+
return input.map(v => {
|
|
130
|
+
// Check if array element is a path array
|
|
131
|
+
if (typeof v === 'string' && v.startsWith('__pathArray:')) {
|
|
132
|
+
const pathArray = JSON.parse(v.substring(12)); // Remove '__pathArray:' prefix
|
|
133
|
+
// Convert array back to path ID with null chars
|
|
134
|
+
const pathId = pathArray.map(segment => {
|
|
135
|
+
const type = typeof segment === 'number' ? 'n' : 's';
|
|
136
|
+
return `${type}:${segment}`;
|
|
137
|
+
}).join('\0');
|
|
138
|
+
return pathId;
|
|
139
|
+
}
|
|
140
|
+
return arraysToPathIds(v);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const out = {};
|
|
145
|
+
for (const k of Object.keys(input)) {
|
|
146
|
+
const v = input[k];
|
|
147
|
+
// Check if key is a path array
|
|
148
|
+
if (typeof k === 'string' && k.startsWith('__pathArray:')) {
|
|
149
|
+
const pathArray = JSON.parse(k.substring(12)); // Remove '__pathArray:' prefix
|
|
150
|
+
// Convert array back to path ID with null chars
|
|
151
|
+
const pathId = pathArray.map(segment => {
|
|
152
|
+
const type = typeof segment === 'number' ? 'n' : 's';
|
|
153
|
+
return `${type}:${segment}`;
|
|
154
|
+
}).join('\0');
|
|
155
|
+
out[pathId] = arraysToPathIds(v);
|
|
156
|
+
} else {
|
|
157
|
+
out[k] = arraysToPathIds(v);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function sha256(bytes) {
|
|
164
|
+
const subtle = getSubtle();
|
|
165
|
+
const digest = await subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
166
|
+
return new Uint8Array(digest);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function constantTimeEqual(a, b) {
|
|
170
|
+
if (a.length !== b.length) return false;
|
|
171
|
+
let acc = 0;
|
|
172
|
+
for (let i = 0; i < a.length; i++) acc |= (a[i] ^ b[i]);
|
|
173
|
+
return acc === 0;
|
|
174
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defaultReplacer, applyReplacerDeep } from './buffer.js';
|
|
2
|
+
import {
|
|
3
|
+
MAGIC, VERSION,
|
|
4
|
+
writeUint32, writeUint16, writeUint8,
|
|
5
|
+
encodeObject,
|
|
6
|
+
calculateChecksum, base64urlEncode
|
|
7
|
+
} from './binary.js';
|
|
8
|
+
|
|
9
|
+
// ---------- Binary Capsule Format ----------
|
|
10
|
+
// [ 0..3 ] Magic "JSC2" (Jucie State Capsule v2)
|
|
11
|
+
// [ 4..5 ] u16 version (big-endian) = 2
|
|
12
|
+
// [ 6 ] u8 flags (reserved for future use)
|
|
13
|
+
// [ 7..10 ] u32 checksum (simple checksum)
|
|
14
|
+
// [11..14 ] u32 payloadLength (big-endian)
|
|
15
|
+
// [15.. ] payload bytes (binary encoded data)
|
|
16
|
+
|
|
17
|
+
// ---------- Public API ----------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Export any data object as a resilient binary capsule string (base64url).
|
|
21
|
+
* Reciprocal with importState.
|
|
22
|
+
* @param {Object} data - The data object to export
|
|
23
|
+
* @param {Object} options - Export options
|
|
24
|
+
*/
|
|
25
|
+
export async function pack(data, { replacer = defaultReplacer } = {}) {
|
|
26
|
+
// 1) Clean ephemeral/transient fields
|
|
27
|
+
const clean = applyReplacerDeep(data, replacer);
|
|
28
|
+
|
|
29
|
+
// 2) Encode data to binary
|
|
30
|
+
const dataBuffer = encodeObject(clean);
|
|
31
|
+
|
|
32
|
+
// 3) Create payload: [dataLength:4][data:varies]
|
|
33
|
+
const payloadLength = 4 + dataBuffer.length;
|
|
34
|
+
const payload = new Uint8Array(payloadLength);
|
|
35
|
+
|
|
36
|
+
let offset = 0;
|
|
37
|
+
offset = writeUint32(dataBuffer.length, payload, offset);
|
|
38
|
+
payload.set(dataBuffer, offset);
|
|
39
|
+
|
|
40
|
+
// 4) Calculate checksum
|
|
41
|
+
const checksum = calculateChecksum(payload);
|
|
42
|
+
|
|
43
|
+
// 5) Build capsule
|
|
44
|
+
const headerLen = 4 + 2 + 1 + 4 + 4; // 15 bytes
|
|
45
|
+
const capsule = new Uint8Array(headerLen + payload.length);
|
|
46
|
+
|
|
47
|
+
// Magic
|
|
48
|
+
capsule.set(MAGIC, 0);
|
|
49
|
+
|
|
50
|
+
// Version u16 LE
|
|
51
|
+
writeUint16(VERSION, capsule, 4);
|
|
52
|
+
|
|
53
|
+
// Flags u8
|
|
54
|
+
writeUint8(0, capsule, 6);
|
|
55
|
+
|
|
56
|
+
// Checksum u32 LE
|
|
57
|
+
writeUint32(checksum, capsule, 7);
|
|
58
|
+
|
|
59
|
+
// Payload length u32 LE
|
|
60
|
+
writeUint32(payload.length, capsule, 11);
|
|
61
|
+
|
|
62
|
+
// Payload
|
|
63
|
+
capsule.set(payload, 15);
|
|
64
|
+
|
|
65
|
+
// 6) Base64url encode (single portable string)
|
|
66
|
+
return base64urlEncode(capsule);
|
|
67
|
+
}
|