@iadev93/zuno 0.0.3 → 0.0.5
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/README.md +4 -0
- package/dist/core/createZuno.d.ts +77 -0
- package/dist/core/createZuno.d.ts.map +1 -0
- package/dist/core/createZuno.js +250 -0
- package/dist/core/store.d.ts +10 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +27 -0
- package/dist/core/types.d.ts +107 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/universe.d.ts +12 -0
- package/dist/core/universe.d.ts.map +1 -0
- package/dist/core/universe.js +53 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/server/apply-state-event.d.ts +26 -0
- package/dist/server/apply-state-event.d.ts.map +1 -0
- package/dist/server/apply-state-event.js +29 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/server-transport.d.ts +8 -0
- package/dist/server/server-transport.d.ts.map +1 -0
- package/dist/server/server-transport.js +25 -0
- package/dist/server/snapshot-handler.d.ts +9 -0
- package/dist/server/snapshot-handler.d.ts.map +1 -0
- package/dist/server/snapshot-handler.js +18 -0
- package/dist/server/sse-handler.d.ts +24 -0
- package/dist/server/sse-handler.d.ts.map +1 -0
- package/dist/server/sse-handler.js +127 -0
- package/dist/server/state.bus.d.ts +18 -0
- package/dist/server/state.bus.d.ts.map +1 -0
- package/dist/server/state.bus.js +26 -0
- package/dist/server/state.log.d.ts +22 -0
- package/dist/server/state.log.d.ts.map +1 -0
- package/dist/server/state.log.js +37 -0
- package/dist/server/types.d.ts +8 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +1 -0
- package/dist/server/universe-store.d.ts +29 -0
- package/dist/server/universe-store.d.ts.map +1 -0
- package/dist/server/universe-store.js +26 -0
- package/dist/shared/readable.d.ts +21 -0
- package/dist/shared/readable.d.ts.map +1 -0
- package/dist/shared/readable.js +7 -0
- package/dist/sync/apply-incoming-event.d.ts +10 -0
- package/dist/sync/apply-incoming-event.d.ts.map +1 -0
- package/dist/sync/apply-incoming-event.js +28 -0
- package/dist/sync/broadcast-channel.d.ts +12 -0
- package/dist/sync/broadcast-channel.d.ts.map +1 -0
- package/dist/sync/broadcast-channel.js +73 -0
- package/dist/sync/sse-client.d.ts +21 -0
- package/dist/sync/sse-client.d.ts.map +1 -0
- package/dist/sync/sse-client.js +162 -0
- package/dist/sync/sync-types.d.ts +164 -0
- package/dist/sync/sync-types.d.ts.map +1 -0
- package/dist/sync/sync-types.js +1 -0
- package/dist/sync/transport.d.ts +10 -0
- package/dist/sync/transport.d.ts.map +1 -0
- package/dist/sync/transport.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ZunoStateEvent } from "../sync/sync-types";
|
|
2
|
+
import type { Universe, ZunoSnapshot } from "./types";
|
|
3
|
+
/** Store */
|
|
4
|
+
type ZunoStore<T> = {
|
|
5
|
+
get(): T;
|
|
6
|
+
set(next: T): void;
|
|
7
|
+
subscribe(cb: (state: T) => void): () => void;
|
|
8
|
+
};
|
|
9
|
+
export type CreateZunoOptions = {
|
|
10
|
+
/** Universe */
|
|
11
|
+
universe?: Universe;
|
|
12
|
+
/** Optional transports */
|
|
13
|
+
channelName?: string;
|
|
14
|
+
/** SSE */
|
|
15
|
+
sseUrl?: string;
|
|
16
|
+
/** Sync */
|
|
17
|
+
syncUrl?: string;
|
|
18
|
+
/** Behavior */
|
|
19
|
+
optimistic?: boolean;
|
|
20
|
+
/** Client ID */
|
|
21
|
+
clientId?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Creates a Zuno instance, which provides a state management system with optional synchronization
|
|
25
|
+
* capabilities via Server-Sent Events (SSE) and BroadcastChannel.
|
|
26
|
+
*
|
|
27
|
+
* @param opts - Configuration options for the Zuno instance.
|
|
28
|
+
* @param opts.universe - An optional pre-existing ZunoUniverse instance. If not provided, a new one will be created.
|
|
29
|
+
* @param opts.channelName - An optional name for the BroadcastChannel to enable local tab synchronization.
|
|
30
|
+
* @param opts.sseUrl - The URL for the Server-Sent Events endpoint to receive state updates from a server.
|
|
31
|
+
* @param opts.syncUrl - The URL for the synchronization endpoint to send state updates to a server. Required if `sseUrl` is provided.
|
|
32
|
+
* @param opts.optimistic - A boolean indicating whether state updates should be applied optimistically before server confirmation. Defaults to `true`.
|
|
33
|
+
* @param opts.clientId - A unique identifier for the client. If not provided, a random UUID will be generated.
|
|
34
|
+
* @returns An object containing methods to interact with the Zuno instance, including `getStore`, `destroy`, and `broadcast`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const createZuno: (opts?: CreateZunoOptions) => {
|
|
37
|
+
/** Universe */
|
|
38
|
+
universe: Universe;
|
|
39
|
+
/** Client ID */
|
|
40
|
+
clientId: string;
|
|
41
|
+
/** Get store */
|
|
42
|
+
getStore: <T>(storeKey: string, init: () => T) => import("./types").Store<T>;
|
|
43
|
+
/** Create store */
|
|
44
|
+
store: <T>(storeKey: string, init: () => T) => {
|
|
45
|
+
key: string;
|
|
46
|
+
get: () => T;
|
|
47
|
+
set: (next: T | ((prev: T) => T)) => Promise<any>;
|
|
48
|
+
subscribe: (cb: (state: T) => void) => () => void;
|
|
49
|
+
raw: () => ZunoStore<T>;
|
|
50
|
+
};
|
|
51
|
+
/** Get state */
|
|
52
|
+
get: <T>(storeKey: string, init?: () => T) => T;
|
|
53
|
+
/** Set state */
|
|
54
|
+
set: <T>(storeKey: string, next: T | ((prev: T) => T), init?: () => T) => Promise<{
|
|
55
|
+
ok: boolean;
|
|
56
|
+
status: number;
|
|
57
|
+
json: any;
|
|
58
|
+
}>;
|
|
59
|
+
/** Subscribe to store */
|
|
60
|
+
subscribe: <T>(storeKey: string, init: () => T, cb: (state: T) => void) => () => boolean;
|
|
61
|
+
/** Dispatch event */
|
|
62
|
+
dispatch: (event: ZunoStateEvent) => Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
status: number;
|
|
65
|
+
json: any;
|
|
66
|
+
}>;
|
|
67
|
+
/** Stop */
|
|
68
|
+
stop: () => void;
|
|
69
|
+
/** Hydrate snapshot */
|
|
70
|
+
hydrateSnapshot: (snapshot: ZunoSnapshot) => void;
|
|
71
|
+
/** Get last event ID */
|
|
72
|
+
getLastEventId: () => number;
|
|
73
|
+
/** Set last event ID */
|
|
74
|
+
setLastEventId: (id: number) => void;
|
|
75
|
+
};
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=createZuno.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createZuno.d.ts","sourceRoot":"","sources":["../../src/core/createZuno.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEtD,YAAY;AACZ,KAAK,SAAS,CAAC,CAAC,IAAI;IAClB,GAAG,IAAI,CAAC,CAAC;IACT,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACnB,SAAS,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe;IACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,UAAU;IACV,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,WAAW;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,eAAe;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,OAAM,iBAAsB;IAmRnD,eAAe;;IAEf,gBAAgB;;IAIhB,gBAAgB;eAlIA,CAAC,YAAa,MAAM,QAAQ,MAAM,CAAC;IAoInD,mBAAmB;YArBN,CAAC,YAAa,MAAM,QAAQ,MAAM,CAAC;aAb3C,MAAM;;6CAE0B,OAAO,CAAC,GAAG,CAAC;sCACnB,IAAI,KAAK,MAAM,IAAI;;;IAiCjD,gBAAgB;UA7HL,CAAC,YAAa,MAAM,SAAS,MAAM,CAAC,KAAG,CAAC;IA+HnD,gBAAgB;UAhFC,CAAC,YACR,MAAM,QACV,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,SACnB,MAAM,CAAC;;;;;IA+Ed,yBAAyB;gBA1DR,CAAC,YACR,MAAM,QACV,MAAM,CAAC,MACT,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI;IA2DtB,qBAAqB;sBA7HQ,cAAc;;;;;IA+H3C,WAAW;;IAGX,uBAAuB;gCAhRU,YAAY;IAkR7C,wBAAwB;;IAExB,wBAAwB;yBA3PE,MAAM;CA8PnC,CAAC"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createUniverse } from "./universe";
|
|
2
|
+
import { startSSE } from "../sync/sse-client";
|
|
3
|
+
import { startBroadcastChannel } from "../sync/broadcast-channel";
|
|
4
|
+
import { applyIncomingEvent } from "../sync/apply-incoming-event";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Zuno instance, which provides a state management system with optional synchronization
|
|
7
|
+
* capabilities via Server-Sent Events (SSE) and BroadcastChannel.
|
|
8
|
+
*
|
|
9
|
+
* @param opts - Configuration options for the Zuno instance.
|
|
10
|
+
* @param opts.universe - An optional pre-existing ZunoUniverse instance. If not provided, a new one will be created.
|
|
11
|
+
* @param opts.channelName - An optional name for the BroadcastChannel to enable local tab synchronization.
|
|
12
|
+
* @param opts.sseUrl - The URL for the Server-Sent Events endpoint to receive state updates from a server.
|
|
13
|
+
* @param opts.syncUrl - The URL for the synchronization endpoint to send state updates to a server. Required if `sseUrl` is provided.
|
|
14
|
+
* @param opts.optimistic - A boolean indicating whether state updates should be applied optimistically before server confirmation. Defaults to `true`.
|
|
15
|
+
* @param opts.clientId - A unique identifier for the client. If not provided, a random UUID will be generated.
|
|
16
|
+
* @returns An object containing methods to interact with the Zuno instance, including `getStore`, `destroy`, and `broadcast`.
|
|
17
|
+
*/
|
|
18
|
+
export const createZuno = (opts = {}) => {
|
|
19
|
+
/** Local state */
|
|
20
|
+
const localState = new Map();
|
|
21
|
+
/** Local per-store versions (for BC / local ordering) */
|
|
22
|
+
const versions = new Map();
|
|
23
|
+
/** Universe */
|
|
24
|
+
const universe = (opts.universe ?? (createUniverse()));
|
|
25
|
+
/** Unique client ID */
|
|
26
|
+
const clientId = opts.clientId ?? (globalThis.crypto?.randomUUID?.() ?? String(Math.random()));
|
|
27
|
+
/** SSE ready */
|
|
28
|
+
let sseReady = false;
|
|
29
|
+
/** Last event ID */
|
|
30
|
+
let lastEventId = 0;
|
|
31
|
+
/**
|
|
32
|
+
* Hydrate snapshot
|
|
33
|
+
* @param snapshot - The snapshot to hydrate.
|
|
34
|
+
*/
|
|
35
|
+
function hydrateSnapshot(snapshot) {
|
|
36
|
+
const plain = {};
|
|
37
|
+
for (const [k, rec] of Object.entries(snapshot.state)) {
|
|
38
|
+
plain[k] = rec.state;
|
|
39
|
+
versions.set(k, rec.version);
|
|
40
|
+
}
|
|
41
|
+
universe.restore(plain);
|
|
42
|
+
lastEventId = snapshot.lastEventId;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get store base version
|
|
46
|
+
* @param storeKey - The key of the store to get the base version for.
|
|
47
|
+
*/
|
|
48
|
+
function getStoreBaseVersion(storeKey) { return versions.get(storeKey) ?? 0; }
|
|
49
|
+
/**
|
|
50
|
+
* Get last event ID
|
|
51
|
+
*/
|
|
52
|
+
function getLastEventId() { return lastEventId; }
|
|
53
|
+
/**
|
|
54
|
+
* Set last event ID
|
|
55
|
+
* @param id - The new last event ID.
|
|
56
|
+
*/
|
|
57
|
+
function setLastEventId(id) { lastEventId = id; }
|
|
58
|
+
/** SSE Prefer server sync if provided */
|
|
59
|
+
const sse = opts.sseUrl && opts.syncUrl
|
|
60
|
+
? startSSE({
|
|
61
|
+
universe,
|
|
62
|
+
url: opts.sseUrl,
|
|
63
|
+
syncUrl: opts.syncUrl,
|
|
64
|
+
optimistic: opts.optimistic ?? true,
|
|
65
|
+
clientId,
|
|
66
|
+
versions,
|
|
67
|
+
getLastEventId: () => lastEventId,
|
|
68
|
+
onOpen: () => {
|
|
69
|
+
sseReady = true;
|
|
70
|
+
},
|
|
71
|
+
onClose: () => {
|
|
72
|
+
sseReady = false;
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
: null;
|
|
76
|
+
/** Apply event to target */
|
|
77
|
+
const apply = (event) => {
|
|
78
|
+
/** Update last event ID */
|
|
79
|
+
if (typeof event.eventId === "number")
|
|
80
|
+
lastEventId = Math.max(lastEventId, event.eventId);
|
|
81
|
+
applyIncomingEvent(universe, event, { clientId, localState, versions });
|
|
82
|
+
};
|
|
83
|
+
/** Broadcast Channel for local tab sync */
|
|
84
|
+
const bc = opts.channelName
|
|
85
|
+
? startBroadcastChannel({
|
|
86
|
+
/** Channel name for BroadcastChannel */
|
|
87
|
+
channelName: opts.channelName,
|
|
88
|
+
/** Unique client ID */
|
|
89
|
+
clientId,
|
|
90
|
+
/** Apply event to target */
|
|
91
|
+
onEvent: (ev) => {
|
|
92
|
+
apply(ev);
|
|
93
|
+
},
|
|
94
|
+
/** Get snapshot of local state */
|
|
95
|
+
getSnapshot: () => {
|
|
96
|
+
/** Snapshot */
|
|
97
|
+
const snap = universe.snapshot();
|
|
98
|
+
/** Snapshot */
|
|
99
|
+
const out = {};
|
|
100
|
+
/** Iterate local state */
|
|
101
|
+
for (const [storeKey, state] of Object.entries(snap)) {
|
|
102
|
+
/** Add to snapshot */
|
|
103
|
+
out[storeKey] = {
|
|
104
|
+
state,
|
|
105
|
+
version: versions.get(storeKey) ?? 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
},
|
|
110
|
+
/** Apply snapshot to target */
|
|
111
|
+
onSnapshot: (snap) => {
|
|
112
|
+
/** Iterate snapshot with store key and record */
|
|
113
|
+
for (const [storeKey, rec] of Object.entries(snap)) {
|
|
114
|
+
/** Record */
|
|
115
|
+
const record = rec;
|
|
116
|
+
/** Get the universe store state or the store state */
|
|
117
|
+
const state = record?.state ?? record;
|
|
118
|
+
/** Get the universe store version or the store version */
|
|
119
|
+
const version = typeof record?.version === "number" ? record.version : 0;
|
|
120
|
+
/** Set the latest version */
|
|
121
|
+
// versions.set(storeKey, Math.max(versions.get(storeKey) ?? 0, version));
|
|
122
|
+
/** Apply the event */
|
|
123
|
+
apply({ storeKey, state, version });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
: null;
|
|
128
|
+
/** Immediately ask other tabs for snapshot (BC-first) */
|
|
129
|
+
setTimeout(() => bc?.hello(), 0);
|
|
130
|
+
/** Store factory
|
|
131
|
+
* @param storeKey - The key of the store to get.
|
|
132
|
+
* @param init - The initialization function for the store.
|
|
133
|
+
* @returns The store.
|
|
134
|
+
*/
|
|
135
|
+
const getStore = (storeKey, init) => {
|
|
136
|
+
return universe.getStore(storeKey, init);
|
|
137
|
+
};
|
|
138
|
+
/** Get store by store key
|
|
139
|
+
* @param storeKey - The key of the store to get.
|
|
140
|
+
* @param init - The initialization function for the store.
|
|
141
|
+
* @returns The state of the store.
|
|
142
|
+
*/
|
|
143
|
+
const get = (storeKey, init) => {
|
|
144
|
+
return universe.getStore(storeKey, init ?? (() => undefined)).get();
|
|
145
|
+
};
|
|
146
|
+
/** Dispatch event to universe
|
|
147
|
+
* @param event - The event to dispatch.
|
|
148
|
+
* @returns A promise that resolves to the result of the dispatch.
|
|
149
|
+
*/
|
|
150
|
+
const dispatch = async (event) => {
|
|
151
|
+
/** Check if SSE is enabled */
|
|
152
|
+
if (sse && sseReady) {
|
|
153
|
+
/** Payload with origin */
|
|
154
|
+
const payload = { ...event, origin: clientId, baseVersion: getStoreBaseVersion(event.storeKey) };
|
|
155
|
+
/** Dispatch to SSE */
|
|
156
|
+
return await sse.dispatch(payload);
|
|
157
|
+
}
|
|
158
|
+
/** Current version */
|
|
159
|
+
const current = versions.get(event.storeKey) ?? 0;
|
|
160
|
+
/** Next version */
|
|
161
|
+
const nextVersion = current + 1;
|
|
162
|
+
/** Apply event with next version */
|
|
163
|
+
apply({ ...event, version: nextVersion });
|
|
164
|
+
/** Set version */
|
|
165
|
+
versions.set(event.storeKey, nextVersion);
|
|
166
|
+
/** Check if BroadcastChannel is enabled */
|
|
167
|
+
if (bc) {
|
|
168
|
+
/** Publish event */
|
|
169
|
+
bc.publish({ ...event, version: nextVersion, origin: clientId });
|
|
170
|
+
}
|
|
171
|
+
/** Return success */
|
|
172
|
+
return { ok: true, status: 200, json: null };
|
|
173
|
+
};
|
|
174
|
+
/** Set store state
|
|
175
|
+
* @param storeKey - The key of the store to set.
|
|
176
|
+
* @param next - The new state to set.
|
|
177
|
+
* @param init - The initialization function for the store.
|
|
178
|
+
* @returns A promise that resolves to the result of the dispatch.
|
|
179
|
+
*/
|
|
180
|
+
const set = async (storeKey, next, init) => {
|
|
181
|
+
/** Get store */
|
|
182
|
+
const store = universe.getStore(storeKey, init ?? (() => undefined));
|
|
183
|
+
/** Get previous state */
|
|
184
|
+
const prev = store.get();
|
|
185
|
+
/** Get next state */
|
|
186
|
+
const state = typeof next === "function" ? next(prev) : next;
|
|
187
|
+
/** Dispatch event */
|
|
188
|
+
return dispatch({ storeKey, state });
|
|
189
|
+
};
|
|
190
|
+
/** Subscribe to store
|
|
191
|
+
* @param storeKey - The key of the store to subscribe to.
|
|
192
|
+
* @param init - The initialization function for the store.
|
|
193
|
+
* @param cb - The callback function to be called when the store state changes.
|
|
194
|
+
* @returns A function to unsubscribe from the store.
|
|
195
|
+
*/
|
|
196
|
+
const subscribe = (storeKey, init, cb) => {
|
|
197
|
+
/** Get store */
|
|
198
|
+
const store = universe.getStore(storeKey, init);
|
|
199
|
+
return store.subscribe(cb);
|
|
200
|
+
};
|
|
201
|
+
/** Stop cleanup */
|
|
202
|
+
const stop = () => {
|
|
203
|
+
sse?.unsubscribe?.();
|
|
204
|
+
bc?.stop?.();
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Creates a bound store for a specific key.
|
|
208
|
+
* @param storeKey The key of the store to create.
|
|
209
|
+
* @param init The initialization function for the store.
|
|
210
|
+
* @returns A BoundStore object representing the store.
|
|
211
|
+
*/
|
|
212
|
+
const store = (storeKey, init) => {
|
|
213
|
+
const rawStore = getStore(storeKey, init);
|
|
214
|
+
return {
|
|
215
|
+
key: storeKey,
|
|
216
|
+
raw: () => rawStore,
|
|
217
|
+
get: () => rawStore.get(),
|
|
218
|
+
subscribe: (cb) => rawStore.subscribe(cb),
|
|
219
|
+
set: (next) => set(storeKey, next, init),
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
/** Universe */
|
|
224
|
+
universe,
|
|
225
|
+
/** Client ID */
|
|
226
|
+
clientId,
|
|
227
|
+
// ------------ DX ------------ \\
|
|
228
|
+
/** Get store */
|
|
229
|
+
getStore,
|
|
230
|
+
/** Create store */
|
|
231
|
+
store,
|
|
232
|
+
/** Get state */
|
|
233
|
+
get,
|
|
234
|
+
/** Set state */
|
|
235
|
+
set,
|
|
236
|
+
/** Subscribe to store */
|
|
237
|
+
subscribe,
|
|
238
|
+
// ------------ Advanced ------------ \\
|
|
239
|
+
/** Dispatch event */
|
|
240
|
+
dispatch,
|
|
241
|
+
/** Stop */
|
|
242
|
+
stop,
|
|
243
|
+
/** Hydrate snapshot */
|
|
244
|
+
hydrateSnapshot,
|
|
245
|
+
/** Get last event ID */
|
|
246
|
+
getLastEventId,
|
|
247
|
+
/** Set last event ID */
|
|
248
|
+
setLastEventId,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Store } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ZUNO state management store.
|
|
4
|
+
*
|
|
5
|
+
* @template T The type of the state managed by the store.
|
|
6
|
+
* @param {T} initial The initial state value.
|
|
7
|
+
* @returns {Store<T>} An object containing methods to interact with the store.
|
|
8
|
+
*/
|
|
9
|
+
export declare const createStore: <T>(initial: T) => Store<T>;
|
|
10
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,SAAS,CAAC,KAAG,KAAK,CAAC,CAAC,CAsBlD,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a ZUNO state management store.
|
|
3
|
+
*
|
|
4
|
+
* @template T The type of the state managed by the store.
|
|
5
|
+
* @param {T} initial The initial state value.
|
|
6
|
+
* @returns {Store<T>} An object containing methods to interact with the store.
|
|
7
|
+
*/
|
|
8
|
+
export const createStore = (initial) => {
|
|
9
|
+
let state = initial;
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
return {
|
|
12
|
+
get: () => state,
|
|
13
|
+
set: (next) => {
|
|
14
|
+
const value = typeof next === "function"
|
|
15
|
+
? next(state)
|
|
16
|
+
: next;
|
|
17
|
+
if (Object.is(value, state))
|
|
18
|
+
return;
|
|
19
|
+
state = value;
|
|
20
|
+
listeners.forEach((l) => l(state));
|
|
21
|
+
},
|
|
22
|
+
subscribe: (listener) => {
|
|
23
|
+
listeners.add(listener);
|
|
24
|
+
return () => listeners.delete(listener);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a generic store that can hold and manage a state of type T.
|
|
3
|
+
* It provides methods to get, set, and subscribe to changes in the state.
|
|
4
|
+
*/
|
|
5
|
+
export interface Store<T> {
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves the current value of the store's state.
|
|
8
|
+
* @returns The current state.
|
|
9
|
+
*/
|
|
10
|
+
get(): T;
|
|
11
|
+
/**
|
|
12
|
+
* Sets the new state of the store.
|
|
13
|
+
* It can accept either a direct value or a function that receives the previous state
|
|
14
|
+
* and returns the new state.
|
|
15
|
+
* @param next The new state value or a function to derive the new state.
|
|
16
|
+
*/
|
|
17
|
+
set(next: T | ((prev: T) => T)): void;
|
|
18
|
+
/**
|
|
19
|
+
* Subscribes a listener function to state changes.
|
|
20
|
+
* The listener will be called whenever the state updates.
|
|
21
|
+
* @param listener The function to call when the state changes.
|
|
22
|
+
* @returns {boolean} A function to unsubscribe the listener. Calling this function will remove the subscription.
|
|
23
|
+
*/
|
|
24
|
+
subscribe(listener: (state: T) => void): () => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Represents a global container or manager for multiple stores,
|
|
28
|
+
* often referred to as a "universe" in state management patterns.
|
|
29
|
+
* It allows creating, retrieving, snapshotting, and restoring stores.
|
|
30
|
+
*/
|
|
31
|
+
export interface Universe {
|
|
32
|
+
/**
|
|
33
|
+
* Retrieves a store by its unique key. If the store does not exist,
|
|
34
|
+
* it initializes it using the provided `init` function and then returns it.
|
|
35
|
+
* @param key The unique identifier for the store.
|
|
36
|
+
* @param init A function to initialize the store's state if it doesn't already exist.
|
|
37
|
+
* @returns The requested store.
|
|
38
|
+
*/
|
|
39
|
+
getStore<T>(key: string, init: () => T): Store<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Creates a snapshot of the current state of all stores managed by the Universe.
|
|
42
|
+
* The snapshot is a record where keys are store identifiers and values are their states.
|
|
43
|
+
* @returns A record representing the current state of all stores.
|
|
44
|
+
*/
|
|
45
|
+
snapshot(): Record<string, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* Restores the state of all stores in the Universe from a provided data snapshot.
|
|
48
|
+
* This can be used to rehydrate the application's state.
|
|
49
|
+
* @param data A record containing the state to restore for each store.
|
|
50
|
+
*/
|
|
51
|
+
restore(data: Record<string, unknown>): void;
|
|
52
|
+
/**
|
|
53
|
+
* Deletes a store by its unique key.
|
|
54
|
+
* @param key The unique identifier for the store to delete.
|
|
55
|
+
*/
|
|
56
|
+
delete(key: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Clears all stores from the Universe.
|
|
59
|
+
*/
|
|
60
|
+
clear(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Restores the state of all stores in the Universe from a provided data snapshot.
|
|
63
|
+
* This can be used to rehydrate the application's state.
|
|
64
|
+
* @param snapshot A record containing the state to restore for each store.
|
|
65
|
+
*/
|
|
66
|
+
hydrateSnapshot(snapshot: ZunoSnapshot): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Context for incoming events.
|
|
70
|
+
* It is used to prevent loops (don’t re-apply your own broadcast) and to track per-store version tracking (SSE uses it, BC can ignore it).
|
|
71
|
+
*/
|
|
72
|
+
export type IncomingEventContext = {
|
|
73
|
+
/** Used to prevent loops (don’t re-apply your own broadcast) */
|
|
74
|
+
clientId: string;
|
|
75
|
+
/** Per-store version tracking (SSE uses it, BC can ignore it) */
|
|
76
|
+
versions?: Map<string, number>;
|
|
77
|
+
/** Optional in-memory snapshot state for BC “hello/snapshot” */
|
|
78
|
+
localState?: Map<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* A snapshot of the universe state.
|
|
82
|
+
* It is used to rehydrate the application's state.
|
|
83
|
+
*/
|
|
84
|
+
export type ZunoSnapshot = {
|
|
85
|
+
state: Record<string, {
|
|
86
|
+
state: unknown;
|
|
87
|
+
version: number;
|
|
88
|
+
}>;
|
|
89
|
+
lastEventId: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* A contract for adapters to interact with the store in a read-only manner.
|
|
93
|
+
* This provides a "native feel" for consumers preventing them from touching the core directly.
|
|
94
|
+
*/
|
|
95
|
+
export type ZunoReadable<T> = {
|
|
96
|
+
/** Read current value (must be sync) */
|
|
97
|
+
getSnapshot(): T;
|
|
98
|
+
/**
|
|
99
|
+
* Subscribe to changes.
|
|
100
|
+
* Must call `onChange` whenever snapshot may have changed.
|
|
101
|
+
* Returns unsubscribe.
|
|
102
|
+
*/
|
|
103
|
+
subscribe(onChange: () => void): () => void;
|
|
104
|
+
/** Optional: for SSR hydration safety in React */
|
|
105
|
+
getServerSnapshot?: () => T;
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB;;;OAGG;IACH,GAAG,IAAI,CAAC,CAAC;IACT;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,OAAO,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAClD;;;;OAIG;IACH,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAE7C;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B;;OAEG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;CAE/C;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IAEjB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE/B,gEAAgE;IAChE,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1D,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAC5B,wCAAwC;IACxC,WAAW,IAAI,CAAC,CAAC;IAEjB;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAE5C,kDAAkD;IAClD,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;CAC7B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Universe } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ZUNO Universe.
|
|
4
|
+
*
|
|
5
|
+
* A Universe is a simple container that manages multiple named stores.
|
|
6
|
+
* It:
|
|
7
|
+
* - Lazily creates a store when `getStore` is called with a new key
|
|
8
|
+
* - Can snapshot the state of all known stores
|
|
9
|
+
* - Can restore state from a snapshot
|
|
10
|
+
*/
|
|
11
|
+
export declare const createUniverse: () => Universe;
|
|
12
|
+
//# sourceMappingURL=universe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"universe.d.ts","sourceRoot":"","sources":["../../src/core/universe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAG7D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,QAAO,QAoDjC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createStore } from "./store";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ZUNO Universe.
|
|
4
|
+
*
|
|
5
|
+
* A Universe is a simple container that manages multiple named stores.
|
|
6
|
+
* It:
|
|
7
|
+
* - Lazily creates a store when `getStore` is called with a new key
|
|
8
|
+
* - Can snapshot the state of all known stores
|
|
9
|
+
* - Can restore state from a snapshot
|
|
10
|
+
*/
|
|
11
|
+
export const createUniverse = () => {
|
|
12
|
+
const stores = new Map();
|
|
13
|
+
return {
|
|
14
|
+
getStore(key, init) {
|
|
15
|
+
if (!stores.has(key)) {
|
|
16
|
+
const initialState = init();
|
|
17
|
+
stores.set(key, createStore(initialState));
|
|
18
|
+
}
|
|
19
|
+
// TypeScript doesn't know the concrete T stored under this key,
|
|
20
|
+
// but by convention you control init(), so this cast is safe.
|
|
21
|
+
return stores.get(key);
|
|
22
|
+
},
|
|
23
|
+
snapshot() {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [key, store] of stores.entries()) {
|
|
26
|
+
out[key] = store.get();
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
},
|
|
30
|
+
restore(data) {
|
|
31
|
+
for (const [key, value] of Object.entries(data)) {
|
|
32
|
+
const existing = stores.get(key);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.set(value);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// create a new store with this initial value
|
|
38
|
+
const newStore = createStore(value);
|
|
39
|
+
stores.set(key, newStore);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
delete(key) {
|
|
44
|
+
stores.delete(key);
|
|
45
|
+
},
|
|
46
|
+
clear() {
|
|
47
|
+
stores.clear();
|
|
48
|
+
},
|
|
49
|
+
hydrateSnapshot(snapshot) {
|
|
50
|
+
this.restore(snapshot.state);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createZuno } from "./core/createZuno";
|
|
2
|
+
export type { CreateZunoOptions } from "./core/createZuno";
|
|
3
|
+
export type * from "./core/types";
|
|
4
|
+
export { startSSE } from "./sync/sse-client";
|
|
5
|
+
export { startBroadcastChannel } from "./sync/broadcast-channel";
|
|
6
|
+
export type * from "./sync/sync-types";
|
|
7
|
+
export * from "./sync/transport";
|
|
8
|
+
export type { ZunoReadable, ZunoSubscribableStore } from "./shared/readable";
|
|
9
|
+
export { toReadable } from "./shared/readable";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,mBAAmB,cAAc,CAAC;AAGlC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,mBAAmB,mBAAmB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AAGjC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ZunoStateEvent } from "../sync/sync-types";
|
|
2
|
+
/**
|
|
3
|
+
* Result of applying a state event.
|
|
4
|
+
* If the event was applied successfully, `ok` is true and `event` contains the applied event.
|
|
5
|
+
* If there was a version conflict, `ok` is false and `reason` is "VERSION_CONFLICT".
|
|
6
|
+
*/
|
|
7
|
+
export type ApplyResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
event: ZunoStateEvent;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: "VERSION_CONFLICT";
|
|
13
|
+
current: {
|
|
14
|
+
state: any;
|
|
15
|
+
version: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Core sync handler that applies an event to the universe
|
|
20
|
+
* and broadcasts it to all SSE subscribers.
|
|
21
|
+
* This is independent of HTTP / WebSocket / whatever transport.
|
|
22
|
+
* @property {ZunoStateEvent} incoming - The incoming event to apply.
|
|
23
|
+
* @returns {ApplyResult} The result of the application.
|
|
24
|
+
*/
|
|
25
|
+
export declare const applyStateEvent: (incoming: ZunoStateEvent) => ApplyResult;
|
|
26
|
+
//# sourceMappingURL=apply-state-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-state-event.d.ts","sourceRoot":"","sources":["../../src/server/apply-state-event.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAExF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,cAAc,KAAG,WAwB1D,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { publishToStateEvent } from "./state.bus";
|
|
2
|
+
import { appendEvent } from "./state.log";
|
|
3
|
+
import { getUniverseRecord, updateUniverseState } from "./universe-store";
|
|
4
|
+
/**
|
|
5
|
+
* Core sync handler that applies an event to the universe
|
|
6
|
+
* and broadcasts it to all SSE subscribers.
|
|
7
|
+
* This is independent of HTTP / WebSocket / whatever transport.
|
|
8
|
+
* @property {ZunoStateEvent} incoming - The incoming event to apply.
|
|
9
|
+
* @returns {ApplyResult} The result of the application.
|
|
10
|
+
*/
|
|
11
|
+
export const applyStateEvent = (incoming) => {
|
|
12
|
+
/** Get the current state of the store */
|
|
13
|
+
const current = getUniverseRecord(incoming.storeKey) ?? { state: undefined, version: 0 };
|
|
14
|
+
/** Only enforce if client provided baseVersion */
|
|
15
|
+
if (typeof incoming.baseVersion === "number" && incoming.baseVersion !== current.version) {
|
|
16
|
+
return { ok: false, reason: "VERSION_CONFLICT", current };
|
|
17
|
+
}
|
|
18
|
+
/** Create a new event with the next version and current timestamp */
|
|
19
|
+
const event = appendEvent({
|
|
20
|
+
...incoming,
|
|
21
|
+
version: current.version + 1,
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
});
|
|
24
|
+
/** Update the universe state */
|
|
25
|
+
updateUniverseState(event);
|
|
26
|
+
/** Publish the event */
|
|
27
|
+
publishToStateEvent(event);
|
|
28
|
+
return { ok: true, event };
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,mBAAmB,EAAE,gBAAgB,EACtC,MAAM,eAAe,CAAC;AACvB,cAAc,qBAAqB,CAAC"}
|