@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/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;
@@ -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
+ }