@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.
Files changed (62) hide show
  1. package/README.md +4 -0
  2. package/dist/core/createZuno.d.ts +77 -0
  3. package/dist/core/createZuno.d.ts.map +1 -0
  4. package/dist/core/createZuno.js +250 -0
  5. package/dist/core/store.d.ts +10 -0
  6. package/dist/core/store.d.ts.map +1 -0
  7. package/dist/core/store.js +27 -0
  8. package/dist/core/types.d.ts +107 -0
  9. package/dist/core/types.d.ts.map +1 -0
  10. package/dist/core/types.js +1 -0
  11. package/dist/core/universe.d.ts +12 -0
  12. package/dist/core/universe.d.ts.map +1 -0
  13. package/dist/core/universe.js +53 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +6 -0
  17. package/dist/server/apply-state-event.d.ts +26 -0
  18. package/dist/server/apply-state-event.d.ts.map +1 -0
  19. package/dist/server/apply-state-event.js +29 -0
  20. package/dist/server/index.d.ts +4 -0
  21. package/dist/server/index.d.ts.map +1 -0
  22. package/dist/server/index.js +3 -0
  23. package/dist/server/server-transport.d.ts +8 -0
  24. package/dist/server/server-transport.d.ts.map +1 -0
  25. package/dist/server/server-transport.js +25 -0
  26. package/dist/server/snapshot-handler.d.ts +9 -0
  27. package/dist/server/snapshot-handler.d.ts.map +1 -0
  28. package/dist/server/snapshot-handler.js +18 -0
  29. package/dist/server/sse-handler.d.ts +24 -0
  30. package/dist/server/sse-handler.d.ts.map +1 -0
  31. package/dist/server/sse-handler.js +127 -0
  32. package/dist/server/state.bus.d.ts +18 -0
  33. package/dist/server/state.bus.d.ts.map +1 -0
  34. package/dist/server/state.bus.js +26 -0
  35. package/dist/server/state.log.d.ts +22 -0
  36. package/dist/server/state.log.d.ts.map +1 -0
  37. package/dist/server/state.log.js +37 -0
  38. package/dist/server/types.d.ts +8 -0
  39. package/dist/server/types.d.ts.map +1 -0
  40. package/dist/server/types.js +1 -0
  41. package/dist/server/universe-store.d.ts +29 -0
  42. package/dist/server/universe-store.d.ts.map +1 -0
  43. package/dist/server/universe-store.js +26 -0
  44. package/dist/shared/readable.d.ts +21 -0
  45. package/dist/shared/readable.d.ts.map +1 -0
  46. package/dist/shared/readable.js +7 -0
  47. package/dist/sync/apply-incoming-event.d.ts +10 -0
  48. package/dist/sync/apply-incoming-event.d.ts.map +1 -0
  49. package/dist/sync/apply-incoming-event.js +28 -0
  50. package/dist/sync/broadcast-channel.d.ts +12 -0
  51. package/dist/sync/broadcast-channel.d.ts.map +1 -0
  52. package/dist/sync/broadcast-channel.js +73 -0
  53. package/dist/sync/sse-client.d.ts +21 -0
  54. package/dist/sync/sse-client.d.ts.map +1 -0
  55. package/dist/sync/sse-client.js +162 -0
  56. package/dist/sync/sync-types.d.ts +164 -0
  57. package/dist/sync/sync-types.d.ts.map +1 -0
  58. package/dist/sync/sync-types.js +1 -0
  59. package/dist/sync/transport.d.ts +10 -0
  60. package/dist/sync/transport.d.ts.map +1 -0
  61. package/dist/sync/transport.js +26 -0
  62. package/package.json +1 -1
@@ -0,0 +1,162 @@
1
+ import { createTransport } from "./transport";
2
+ import { applyIncomingEvent } from "./apply-incoming-event";
3
+ /**
4
+ * Starts a Server-Sent Events (SSE) connection to synchronize state from a server.
5
+ * It can synchronize either a Zuno universe (collection of stores) or a single Zuno store.
6
+ *
7
+ * @param options - Configuration options for the SSE connection, including the URL,
8
+ * and either a Zuno universe or a specific Zuno store to update.
9
+ */
10
+ export const startSSE = (options) => {
11
+ if (!options?.url)
12
+ throw new Error("startSSE: 'url' is required");
13
+ if (!options.universe && !options.store)
14
+ throw new Error("startSSE: provide either 'universe' or 'store'");
15
+ const clientId = options.clientId ?? (crypto?.randomUUID?.() ?? String(Math.random()));
16
+ // const eventSource = new EventSource(options.url);
17
+ const lastEventId = options.getLastEventId?.() ?? 0;
18
+ const url = lastEventId > 0
19
+ ? `${options.url}?lastEventId=${encodeURIComponent(String(lastEventId))}`
20
+ : options.url;
21
+ const eventSource = new EventSource(url);
22
+ const transport = createTransport(options.syncUrl);
23
+ /**
24
+ * Applies an incoming state event to the target Zuno universe or store.
25
+ * If a universe is provided, it updates the specific store identified by `storeKey`.
26
+ * If a single store is provided, it updates that store directly.
27
+ * @param eventState - The state event received from the SSE connection, containing `storeKey` and `state`.
28
+ */
29
+ const applyEventToTarget = (eventState) => {
30
+ if (options.universe) {
31
+ const store = options.universe.getStore(eventState.storeKey, () => eventState.state);
32
+ store.set(eventState.state);
33
+ }
34
+ else if (options.store) {
35
+ options.store.set(eventState.state);
36
+ }
37
+ };
38
+ /**
39
+ * Tracks the version of each store to handle conflicts.
40
+ */
41
+ const versions = options.versions;
42
+ /**
43
+ * Applies an incoming snapshot to the target Zuno universe or store.
44
+ * If a universe is provided, it updates all stores in the snapshot.
45
+ * If a single store is provided, it updates that store directly.
46
+ * @param snapshot - The snapshot received from the SSE connection, containing a map of store keys to states.
47
+ */
48
+ const applySnapshotToTarget = (snapshot) => {
49
+ if (options.universe) {
50
+ const snap = (snapshot ?? {});
51
+ for (const [storeKey, rec] of Object.entries(snap)) {
52
+ versions.set(storeKey, rec?.version ?? 0);
53
+ const store = options.universe.getStore(storeKey, () => rec.state);
54
+ store.set(rec.state);
55
+ }
56
+ options.getSnapshot?.(options.universe);
57
+ }
58
+ else if (options.store) {
59
+ options.store.set(snapshot);
60
+ options.getSnapshot?.(options.store);
61
+ }
62
+ };
63
+ /**
64
+ * Handles incoming snapshot events from the SSE connection.
65
+ * Parses the snapshot data and applies it to the target Zuno universe or store.
66
+ * @param event - The MessageEvent containing the snapshot data.
67
+ */
68
+ const onSnapshot = (event) => {
69
+ try {
70
+ const snapshotState = JSON.parse(event.data);
71
+ applySnapshotToTarget(snapshotState);
72
+ }
73
+ catch {
74
+ console.error("Zuno SSE: failed to parse snapshot payload");
75
+ }
76
+ };
77
+ /**
78
+ * Handles incoming state events from the SSE connection.
79
+ * Parses the state data and applies it to the target Zuno universe or store.
80
+ * @param event - The MessageEvent containing the state data.
81
+ */
82
+ const onState = (event) => {
83
+ try {
84
+ const eventState = JSON.parse(event.data);
85
+ /**
86
+ * Apply incoming event to universe or store
87
+ */
88
+ if (options.universe) {
89
+ applyIncomingEvent(options.universe, eventState, {
90
+ clientId,
91
+ versions,
92
+ });
93
+ }
94
+ /**
95
+ * Apply incoming event to store
96
+ */
97
+ else if (options.store) {
98
+ // Minimal support for single-store subscriptions.
99
+ if (typeof eventState.version === "number") {
100
+ const currentVersion = versions.get(eventState.storeKey) ?? 0;
101
+ if (eventState.version <= currentVersion)
102
+ return;
103
+ versions.set(eventState.storeKey, eventState.version);
104
+ }
105
+ options.store.set(eventState.state);
106
+ }
107
+ }
108
+ catch {
109
+ console.error("Zuno SSE: failed to parse state payload");
110
+ }
111
+ };
112
+ eventSource.addEventListener("snapshot", onSnapshot);
113
+ eventSource.addEventListener("state", onState);
114
+ eventSource.onopen = () => {
115
+ options.onOpen?.();
116
+ };
117
+ eventSource.onerror = () => {
118
+ options.onClose?.();
119
+ };
120
+ /**
121
+ * Unsubscribes from the SSE connection and removes the event listeners.
122
+ */
123
+ const unsubscribe = () => {
124
+ eventSource.removeEventListener("snapshot", onSnapshot);
125
+ eventSource.removeEventListener("state", onState);
126
+ eventSource.close();
127
+ };
128
+ /**
129
+ * Dispatches a state event to the server.
130
+ * If optimistic = true, also applies the event locally immediately.
131
+ * @param event - The state event to dispatch.
132
+ */
133
+ const dispatch = async (event) => {
134
+ const baseVersion = versions.get(event.storeKey) ?? 0;
135
+ /** Payload to send to server with event metadata, version and origin */
136
+ const payload = {
137
+ ...event,
138
+ origin: clientId
139
+ };
140
+ /**
141
+ * Optimistic logic applied locally
142
+ */
143
+ if (options.optimistic) {
144
+ applyEventToTarget(payload);
145
+ versions.set(event.storeKey, baseVersion + 1);
146
+ }
147
+ /**
148
+ * AUTHORITATIVE: send to server
149
+ */
150
+ const result = await transport.publish(payload);
151
+ /**
152
+ * If server rejects, reconcile (optional but good)
153
+ */
154
+ if (!result.ok && result.status === 409 && result.json?.current && options.universe) {
155
+ const { state, version } = result.json.current;
156
+ versions.set(event.storeKey, version);
157
+ options.universe.getStore(event.storeKey, () => state).set(state);
158
+ }
159
+ return result;
160
+ };
161
+ return { unsubscribe, dispatch };
162
+ };
@@ -0,0 +1,164 @@
1
+ import type { Store, Universe } from "../core/types";
2
+ /**
3
+ * Represents a state event received from a Zuno SSE stream.
4
+ */
5
+ export interface ZunoStateEvent {
6
+ /** The kind of the event. */
7
+ kind?: "state";
8
+ /** The key of the store that emitted the state change. */
9
+ storeKey: string;
10
+ /** The new state value. */
11
+ state: unknown;
12
+ /** The version of the store before the state change to resolve conflicts and ordering. */
13
+ baseVersion?: number;
14
+ /** The authoritative version after server applies. */
15
+ version?: number;
16
+ /** The origin of the state change. */
17
+ origin?: string;
18
+ /** The timestamp of the state change. */
19
+ ts?: number;
20
+ /** The global monotonic id of the event. */
21
+ eventId?: number;
22
+ /** The transport layer that published the event. */
23
+ via?: "http" | "sse" | "bc";
24
+ }
25
+ export type ZunoSSEOptionsDefault = {
26
+ /**
27
+ * The URL of the SSE endpoint.
28
+ * */
29
+ url: string;
30
+ /**
31
+ * The URL of the Sync endpoint.
32
+ */
33
+ syncUrl: string;
34
+ /**
35
+ * Get the snapshot of the universe or store.
36
+ */
37
+ getSnapshot?: (data: Universe | Store<unknown>) => void;
38
+ /**
39
+ * Whether to enable optimistic updates.
40
+ * If true, the client will update its state optimistically before receiving the server's response.
41
+ */
42
+ optimistic?: boolean;
43
+ /**
44
+ * The client id for the Broadcast Channel.
45
+ */
46
+ clientId?: string;
47
+ /**
48
+ * The name of the Broadcast Channel.
49
+ */
50
+ channelName?: string;
51
+ /**
52
+ * The versions of the stores.
53
+ */
54
+ versions: Map<string, number>;
55
+ /**
56
+ * Callback when SSE connection is opened.
57
+ */
58
+ onOpen?: () => void;
59
+ /**
60
+ * Callback when SSE connection is closed.
61
+ */
62
+ onClose?: () => void;
63
+ /**
64
+ * The last event ID received from the server.
65
+ */
66
+ getLastEventId?: () => number;
67
+ /**
68
+ * Callback when an event is received from the server.
69
+ */
70
+ onEvent?: (event: ZunoStateEvent) => void;
71
+ };
72
+ /**
73
+ * Options for configuring a Zuno Server-Sent Events (SSE) connection.
74
+ * It can be configured either with a `Universe` or a specific `Store`.
75
+ */
76
+ export type ZunoSSEOptions = ZunoSSEOptionsDefault & ({
77
+ /** The universe to subscribe to. */
78
+ universe: Universe;
79
+ store?: never;
80
+ } | {
81
+ /** The specific store to subscribe to. */
82
+ store: Store<any>;
83
+ universe?: never;
84
+ });
85
+ /**
86
+ * Defines the interface for a Zuno transport layer, responsible for
87
+ * publishing and subscribing to state events.
88
+ */
89
+ export interface ZunoTransport {
90
+ /**
91
+ * Publishes a state event to the transport.
92
+ * @param event The state event to publish.
93
+ */
94
+ publish(event: ZunoStateEvent): {
95
+ ok: boolean;
96
+ status: number;
97
+ json: unknown;
98
+ } | Promise<{
99
+ ok: boolean;
100
+ status: number;
101
+ json: any;
102
+ }>;
103
+ /**
104
+ * Subscribes to state events from the transport.
105
+ * @param handler The function to call when a new state event is received.
106
+ */
107
+ subscribe(handler: (event: ZunoStateEvent) => void): () => void;
108
+ }
109
+ /**
110
+ * A record of a store's state and version.
111
+ * @property {unknown} state - The state of the store.
112
+ * @property {number} version - The version of the store.
113
+ */
114
+ export type ZunoSnapshotRecord = {
115
+ /**
116
+ * The state of the store.
117
+ */
118
+ state: unknown;
119
+ /**
120
+ * The version of the store.
121
+ */
122
+ version: number;
123
+ };
124
+ /**
125
+ * A snapshot of the universe or store.
126
+ * @property {Record<string, ZunoSnapshotRecord>} state - The state of the store.
127
+ */
128
+ export type ZunoSnapshotState = Record<string, ZunoSnapshotRecord>;
129
+ /**
130
+ * A message sent over the broadcast channel.
131
+ * @property {"zuno:hello" | "zuno:snapshot" | "zuno:event"} type - The type of the message.
132
+ * @property {string} origin - The origin of the message.
133
+ * @property {string} target - The target of the message.
134
+ * @property {ZunoSnapshotState} snapshot - The snapshot of the universe or store.
135
+ * @property {ZunoStateEvent} event - The state event.
136
+ */
137
+ export type BCMsg = {
138
+ type: "zuno:hello";
139
+ origin: string;
140
+ } | {
141
+ type: "zuno:snapshot";
142
+ origin: string;
143
+ target: string;
144
+ snapshot: ZunoSnapshotState;
145
+ } | {
146
+ type: "zuno:event";
147
+ origin: string;
148
+ event: ZunoStateEvent;
149
+ };
150
+ /**
151
+ * Options for starting a broadcast channel.
152
+ * @property {string} channelName - The name of the broadcast channel.
153
+ * @property {string} clientId - The client id for the broadcast channel.
154
+ * @property {(event: ZunoStateEvent) => void} onEvent - The function to call when a new state event is received.
155
+ * @property {() => ZunoSnapshotState} getSnapshot - The function to get the snapshot of the universe or store.
156
+ */
157
+ export type ZunoBCOptions = {
158
+ channelName: string;
159
+ clientId: string;
160
+ onEvent: (event: ZunoStateEvent) => void;
161
+ getSnapshot?: () => ZunoSnapshotState;
162
+ onSnapshot?: (snapshot: ZunoSnapshotState) => void;
163
+ };
164
+ //# sourceMappingURL=sync-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-types.d.ts","sourceRoot":"","sources":["../../src/sync/sync-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { ZunoTransport } from "./sync-types";
2
+ /**
3
+ * Creates a transport for publishing Zuno state events.
4
+ *
5
+ * @param url The URL where events will be published.
6
+ * @param headers Additional headers to include in the HTTP request.
7
+ * @returns A ZunoTransport object with a publish method.
8
+ */
9
+ export declare const createTransport: (url: string, headers?: HeadersInit) => ZunoTransport;
10
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/sync/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlE;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,UAAU,WAAW,KAAG,aAsBpE,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Creates a transport for publishing Zuno state events.
3
+ *
4
+ * @param url The URL where events will be published.
5
+ * @param headers Additional headers to include in the HTTP request.
6
+ * @returns A ZunoTransport object with a publish method.
7
+ */
8
+ export const createTransport = (url, headers) => {
9
+ /** A set of subscribers to state events. */
10
+ const subs = new Set();
11
+ return {
12
+ async publish(event) {
13
+ const res = await fetch(url, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json", ...headers },
16
+ body: JSON.stringify(event),
17
+ });
18
+ const json = await res.json().catch(() => null);
19
+ return { ok: res.ok, status: res.status, json };
20
+ },
21
+ subscribe(cb) {
22
+ subs.add(cb);
23
+ return () => subs.delete(cb);
24
+ },
25
+ };
26
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iadev93/zuno",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "author": "Ibrahim Aftab",
5
5
  "license": "MIT",
6
6
  "type": "module",