@iadev93/zuno 0.0.10 → 0.0.12
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 +28 -0
- package/dist/{index-BU6hvVGv.d.cts → index-3uqI2fJ2.d.cts} +9 -0
- package/dist/{index-BU6hvVGv.d.ts → index-3uqI2fJ2.d.ts} +9 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +9 -8
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +75 -27
- package/dist/server/index.d.ts +75 -27
- package/dist/server/index.js +9 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
|
-
import { f as ZunoStateEvent } from '../index-
|
|
1
|
+
import { f as ZunoStateEvent } from '../index-3uqI2fJ2.js';
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
3
|
|
|
4
|
+
type UniverseRecord = {
|
|
5
|
+
state: unknown;
|
|
6
|
+
version: number;
|
|
7
|
+
};
|
|
8
|
+
type ZunoStateListener = (event: ZunoStateEvent) => void;
|
|
9
|
+
type CreateZunoServerStateOptions = {
|
|
10
|
+
/** Maximum number of authoritative events retained for SSE replay. */
|
|
11
|
+
maxEvents?: number;
|
|
12
|
+
/** Maximum serialized state size accepted per event. */
|
|
13
|
+
maxStateBytes?: number;
|
|
14
|
+
/** Maximum events buffered for a slow SSE subscriber. */
|
|
15
|
+
maxSubscriberBuffer?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Isolated authoritative state used by server adapters.
|
|
19
|
+
* Create one instance per application, namespace, or tenant boundary.
|
|
20
|
+
*/
|
|
21
|
+
declare class ZunoServerState {
|
|
22
|
+
readonly maxEvents: number;
|
|
23
|
+
readonly maxStateBytes: number;
|
|
24
|
+
readonly maxSubscriberBuffer: number;
|
|
25
|
+
private readonly universeState;
|
|
26
|
+
private readonly eventLog;
|
|
27
|
+
private readonly listeners;
|
|
28
|
+
private nextEventId;
|
|
29
|
+
constructor(options?: CreateZunoServerStateOptions);
|
|
30
|
+
getUniverseRecord(storeKey: string): UniverseRecord | undefined;
|
|
31
|
+
updateUniverseState(event: ZunoStateEvent): void;
|
|
32
|
+
getUniverseState(): Record<string, UniverseRecord>;
|
|
33
|
+
appendEvent(event: ZunoStateEvent): ZunoStateEvent;
|
|
34
|
+
getEventsAfter(lastEventId: number): ZunoStateEvent[];
|
|
35
|
+
canReplayAfter(lastEventId: number): boolean;
|
|
36
|
+
getLastEventId(): number;
|
|
37
|
+
subscribeToStateEvents(listener: ZunoStateListener): () => void;
|
|
38
|
+
publishToStateEvent(event: ZunoStateEvent): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
declare const createZunoServerState: (options?: CreateZunoServerStateOptions) => ZunoServerState;
|
|
42
|
+
/** Lazily creates isolated server states for application namespaces or tenants. */
|
|
43
|
+
declare class ZunoServerRegistry {
|
|
44
|
+
private readonly serverOptions;
|
|
45
|
+
private readonly servers;
|
|
46
|
+
constructor(serverOptions?: CreateZunoServerStateOptions);
|
|
47
|
+
get(namespace: string): ZunoServerState;
|
|
48
|
+
delete(namespace: string): boolean;
|
|
49
|
+
clear(): void;
|
|
50
|
+
}
|
|
51
|
+
declare const createZunoServerRegistry: (options?: CreateZunoServerStateOptions) => ZunoServerRegistry;
|
|
52
|
+
/** Backward-compatible singleton used by the original module-level helpers. */
|
|
53
|
+
declare const defaultZunoServerState: ZunoServerState;
|
|
54
|
+
declare const getUniverseRecord: (storeKey: string) => UniverseRecord | undefined;
|
|
55
|
+
declare const updateUniverseState: (event: ZunoStateEvent) => void;
|
|
56
|
+
declare const getUniverseState: () => Record<string, UniverseRecord>;
|
|
57
|
+
declare const appendEvent: (event: ZunoStateEvent) => ZunoStateEvent;
|
|
58
|
+
declare const getEventsAfter: (lastEventId: number) => ZunoStateEvent[];
|
|
59
|
+
declare const getLastEventId: () => number;
|
|
60
|
+
declare const subscribeToStateEvents: (listener: ZunoStateListener) => () => void;
|
|
61
|
+
declare const publishToStateEvent: (event: ZunoStateEvent) => void;
|
|
62
|
+
|
|
63
|
+
type EventValidationError = {
|
|
64
|
+
field: string;
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
4
67
|
type ApplyResult = {
|
|
5
68
|
ok: true;
|
|
6
69
|
event: ZunoStateEvent;
|
|
@@ -11,47 +74,32 @@ type ApplyResult = {
|
|
|
11
74
|
state: unknown;
|
|
12
75
|
version: number;
|
|
13
76
|
};
|
|
77
|
+
errors?: never;
|
|
14
78
|
} | {
|
|
15
79
|
ok: false;
|
|
16
|
-
reason:
|
|
80
|
+
reason: "INVALID_EVENT";
|
|
81
|
+
errors: EventValidationError[];
|
|
17
82
|
current?: never;
|
|
18
83
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
declare function applyStateEvent(incoming: ZunoStateEvent): ApplyResult;
|
|
23
|
-
|
|
24
|
-
type UniverseRecord = {
|
|
25
|
-
state: unknown;
|
|
26
|
-
version: number;
|
|
27
|
-
};
|
|
28
|
-
type ZunoStateListener = (event: ZunoStateEvent) => void;
|
|
29
|
-
declare const getUniverseRecord: (storeKey: string) => UniverseRecord | undefined;
|
|
30
|
-
declare const updateUniverseState: (event: ZunoStateEvent) => void;
|
|
31
|
-
declare const getUniverseState: () => {
|
|
32
|
-
[k: string]: UniverseRecord;
|
|
33
|
-
};
|
|
34
|
-
declare const appendEvent: (event: ZunoStateEvent) => ZunoStateEvent;
|
|
35
|
-
declare const getEventsAfter: (lastEventId: number) => ZunoStateEvent[];
|
|
36
|
-
declare const getLastEventId: () => number;
|
|
37
|
-
declare const subscribeToStateEvents: (listener: ZunoStateListener) => () => void;
|
|
38
|
-
declare const publishToStateEvent: (event: ZunoStateEvent) => void;
|
|
84
|
+
declare function validateStateEvent(input: unknown, maxStateBytes?: number): EventValidationError[];
|
|
85
|
+
/** Validates and applies a state event to an isolated authoritative server. */
|
|
86
|
+
declare function applyStateEvent(incoming: unknown, server?: ZunoServerState): ApplyResult;
|
|
39
87
|
|
|
40
88
|
/**
|
|
41
89
|
* Sends a snapshot of the current universe state to the response.
|
|
42
90
|
* Compatible with both Express and raw Node.js http.
|
|
43
91
|
*/
|
|
44
|
-
declare function sendSnapshot(_req: IncomingMessage, res: ServerResponse): void;
|
|
92
|
+
declare function sendSnapshot(_req: IncomingMessage, res: ServerResponse, server?: ZunoServerState): void;
|
|
45
93
|
|
|
46
94
|
type IncomingHeaders = IncomingMessage["headers"];
|
|
47
95
|
/**
|
|
48
96
|
* Creates a Server-Sent Events (SSE) connection for Zuno state updates.
|
|
49
97
|
*/
|
|
50
|
-
declare const createSSEConnection: (req: IncomingMessage, res: ServerResponse, headers: IncomingHeaders) => void;
|
|
98
|
+
declare const createSSEConnection: (req: IncomingMessage, res: ServerResponse, headers: IncomingHeaders, server?: ZunoServerState) => void;
|
|
51
99
|
/**
|
|
52
100
|
* Synchronizes the Zuno universe state by applying an incoming event.
|
|
53
101
|
*/
|
|
54
|
-
declare const syncUniverseState: (req: IncomingMessage, res: ServerResponse) => void;
|
|
55
|
-
declare const setUniverseState: (req: IncomingMessage, res: ServerResponse) => void;
|
|
102
|
+
declare const syncUniverseState: (req: IncomingMessage, res: ServerResponse, server?: ZunoServerState) => void;
|
|
103
|
+
declare const setUniverseState: (req: IncomingMessage, res: ServerResponse, server?: ZunoServerState) => void;
|
|
56
104
|
|
|
57
|
-
export { type ApplyResult, type UniverseRecord, type ZunoStateListener, appendEvent, applyStateEvent, createSSEConnection, getEventsAfter, getLastEventId, getUniverseRecord, getUniverseState, publishToStateEvent, sendSnapshot, setUniverseState, subscribeToStateEvents, syncUniverseState, updateUniverseState };
|
|
105
|
+
export { type ApplyResult, type CreateZunoServerStateOptions, type EventValidationError, type UniverseRecord, ZunoServerRegistry, ZunoServerState, type ZunoStateListener, appendEvent, applyStateEvent, createSSEConnection, createZunoServerRegistry, createZunoServerState, defaultZunoServerState, getEventsAfter, getLastEventId, getUniverseRecord, getUniverseState, publishToStateEvent, sendSnapshot, setUniverseState, subscribeToStateEvents, syncUniverseState, updateUniverseState, validateStateEvent };
|
package/dist/server/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
var l=class{constructor(e={}){this.universeState=new Map;this.eventLog=[];this.listeners=new Set;this.nextEventId=1;let n=e.maxEvents??1e3,r=e.maxStateBytes??512*1024,s=e.maxSubscriberBuffer??1e3;if(!Number.isInteger(n)||n<1)throw new TypeError("maxEvents must be a positive integer");if(!Number.isInteger(r)||r<1)throw new TypeError("maxStateBytes must be a positive integer");if(!Number.isInteger(s)||s<1)throw new TypeError("maxSubscriberBuffer must be a positive integer");this.maxEvents=n,this.maxStateBytes=r,this.maxSubscriberBuffer=s;}getUniverseRecord(e){return this.universeState.get(e)}updateUniverseState(e){let n=this.universeState.get(e.storeKey)??{version:0},r=typeof e.version=="number"?e.version:n.version+1;this.universeState.set(e.storeKey,{state:e.state,version:r});}getUniverseState(){return Object.fromEntries(this.universeState)}appendEvent(e){return e.eventId=this.nextEventId++,this.eventLog.push(e),this.eventLog.length>this.maxEvents&&this.eventLog.shift(),e}getEventsAfter(e){return this.eventLog.filter(n=>(n.eventId??0)>e)}canReplayAfter(e){let n=this.getLastEventId();if(e===n)return true;let r=this.eventLog[0]?.eventId;return typeof r=="number"&&e>=r-1&&e<n}getLastEventId(){return this.eventLog[this.eventLog.length-1]?.eventId??0}subscribeToStateEvents(e){return this.listeners.add(e),()=>{this.listeners.delete(e);}}publishToStateEvent(e){this.listeners.forEach(n=>{n(e);});}clear(){this.universeState.clear(),this.eventLog.length=0,this.nextEventId=1;}},b=(t={})=>new l(t),g=class{constructor(e={}){this.serverOptions=e;this.servers=new Map;}get(e){if(e.trim().length===0)throw new TypeError("namespace must be a non-empty string");let n=this.servers.get(e);return n||(n=b(this.serverOptions),this.servers.set(e,n)),n}delete(e){return this.servers.delete(e)}clear(){this.servers.clear();}},L=(t={})=>new g(t),a=b(),T=t=>a.getUniverseRecord(t),R=t=>a.updateUniverseState(t),C=()=>a.getUniverseState(),k=t=>a.appendEvent(t),A=t=>a.getEventsAfter(t),B=()=>a.getLastEventId(),U=t=>a.subscribeToStateEvents(t),V=t=>a.publishToStateEvent(t);var x=t=>typeof t=="object"&&t!==null&&!Array.isArray(t);function O(t,e=512*1024){if(!x(t))return [{field:"event",message:"Event must be an object"}];let n=[];if((typeof t.storeKey!="string"||t.storeKey.trim().length===0||t.storeKey.length>256)&&n.push({field:"storeKey",message:"storeKey must be a non-empty string of at most 256 characters"}),!("state"in t))n.push({field:"state",message:"state is required"});else try{let r=JSON.stringify(t.state);r===void 0?n.push({field:"state",message:"state must be JSON-serializable"}):new TextEncoder().encode(r).byteLength>e&&n.push({field:"state",message:`state exceeds the ${e} byte limit`});}catch{n.push({field:"state",message:"state must be JSON-serializable"});}for(let r of ["version","baseVersion","eventId"]){let s=t[r];s!==void 0&&(!Number.isInteger(s)||s<0)&&n.push({field:r,message:`${r} must be a non-negative integer when provided`});}for(let r of ["origin"]){let s=t[r];s!==void 0&&typeof s!="string"&&n.push({field:r,message:`${r} must be a string when provided`});}return t.ts!==void 0&&!Number.isFinite(t.ts)&&n.push({field:"ts",message:"ts must be a finite number when provided"}),t.intent!==void 0&&(!x(t.intent)||typeof t.intent.type!="string"||t.intent.type.trim().length===0)&&n.push({field:"intent",message:"intent must contain a non-empty string type"}),n}function I(t,e=a){let n=O(t,e.maxStateBytes);if(n.length>0)return {ok:false,reason:"INVALID_EVENT",errors:n};let r=t,s=e.getUniverseRecord(r.storeKey)??{state:void 0,version:0};if(typeof r.baseVersion=="number"&&r.baseVersion!==s.version)return {ok:false,reason:"VERSION_CONFLICT",current:s};let i={...r,version:s.version+1};return e.updateUniverseState(i),e.appendEvent(i),e.publishToStateEvent(i),{ok:true,event:i}}function H(t,e,n=a){let r={state:n.getUniverseState(),lastEventId:n.getLastEventId()};"json"in e&&typeof e.json=="function"?e.json(r):(e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify(r)));}var F=(t,e,n,r=a)=>{e.writeHead(200,{"Cache-Control":"no-cache, no-transform","Content-Type":"text/event-stream; charset=utf-8",Connection:"keep-alive","X-Accel-Buffering":"no",...n}),e.flushHeaders?.();let s=t.headers["last-event-id"]||new URL(t.url||"","http://localhost").searchParams.get("lastEventId"),i=Number.parseInt(Array.isArray(s)?s[0]:s??"0",10)||0,u=[],f=[],m=true,c=false,v=false,p=null,y=()=>{},d=()=>{v||(v=true,p&&clearInterval(p),y(),e.end());},h=()=>{for(c=false;!v&&f.length>0;){let o=f.shift();if(o&&!e.write(E(o))){c=true,e.once("drain",h);break}}},E=o=>`id: ${o.eventId}
|
|
2
|
+
event: state
|
|
3
|
+
data: ${JSON.stringify(o)}
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`),e.write(`
|
|
5
|
+
`,S=o=>{if(!v){if(c){if(f.length>=r.maxSubscriberBuffer){d();return}f.push(o);return}e.write(E(o))||(c=true,e.once("drain",h));}},w=()=>{e.write(`id: ${r.getLastEventId()}
|
|
6
|
+
`),e.write(`event: snapshot
|
|
7
|
+
`),e.write(`data: ${JSON.stringify(r.getUniverseState())}
|
|
7
8
|
|
|
8
|
-
`);for(
|
|
9
|
+
`);};if(y=r.subscribeToStateEvents(o=>{if(m){if(u.length>=r.maxSubscriberBuffer){d();return}u.push(o);}else S(o);}),i>0&&r.canReplayAfter(i)){let o=r.getEventsAfter(i);for(let Z of o)S(Z);}else w();for(m=false;u.length>0;){let o=u.shift();o&&S(o);}v||(p=setInterval(()=>{v||e.write(`: ping ${Date.now()}
|
|
9
10
|
|
|
10
|
-
`);},15e3)
|
|
11
|
+
`);},15e3),e.write(`: connected
|
|
11
12
|
|
|
12
|
-
`),t.on("close",()=>{
|
|
13
|
+
`),t.on("close",()=>{d();}));},N=(t,e,n=a)=>{let s="";t.on("data",i=>{s+=i.toString("utf8"),s.length>524288&&(e.writeHead(413,{"Content-Type":"application/json"}),e.end(JSON.stringify({ok:false,reason:"PAYLOAD_TOO_LARGE"})),t.destroy());}),t.on("end",()=>{try{let i=JSON.parse(s||"{}"),u=I(i,n);if(!u.ok){u.reason==="VERSION_CONFLICT"?(e.writeHead(409,{"Content-Type":"application/json"}),e.end(JSON.stringify({ok:!1,reason:"VERSION_CONFLICT",current:u.current}))):(e.writeHead(400,{"Content-Type":"application/json"}),e.end(JSON.stringify({ok:!1,reason:u.reason,errors:u.errors})));return}e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify({ok:!0,event:u.event}));}catch{e.writeHead(400,{"Content-Type":"application/json"}),e.end(JSON.stringify({ok:false,reason:"INVALID_JSON"}));}});},Y=(t,e,n=a)=>N(t,e,n);export{g as ZunoServerRegistry,l as ZunoServerState,k as appendEvent,I as applyStateEvent,F as createSSEConnection,L as createZunoServerRegistry,b as createZunoServerState,a as defaultZunoServerState,A as getEventsAfter,B as getLastEventId,T as getUniverseRecord,C as getUniverseState,V as publishToStateEvent,H as sendSnapshot,Y as setUniverseState,U as subscribeToStateEvents,N as syncUniverseState,R as updateUniverseState,O as validateStateEvent};//# sourceMappingURL=index.js.map
|
|
13
14
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/core.ts","../../src/server/apply-state-event.ts","../../src/server/snapshot-handler.ts","../../src/server/sse-handler.ts"],"names":["universeState","getUniverseRecord","storeKey","updateUniverseState","event","current","nextVersion","getUniverseState","MAX_EVENTS","nextEventId","eventLog","appendEvent","getEventsAfter","lastEventId","getLastEventId","listeners","subscribeToStateEvents","listener","publishToStateEvent","applyStateEvent","incoming","sendSnapshot","_req","res","snapshot","createSSEConnection","req","headers","raw","buffer","isSyncing","writeEvent","unsubscribe","missed","heartbeat","syncUniverseState","body","chunk","result","setUniverseState"],"mappings":"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","file":"index.js","sourcesContent":["import type { ZunoStateEvent } from \"../sync\";\n\n// --- Types ---\n\nexport type UniverseRecord = {\n\tstate: unknown; // Was 'any'\n\tversion: number;\n};\n\nexport type ZunoStateListener = (event: ZunoStateEvent) => void;\n\n// --- State Store ---\n\nconst universeState = new Map<string, UniverseRecord>();\n\nexport const getUniverseRecord = (\n\tstoreKey: string,\n): UniverseRecord | undefined => {\n\treturn universeState.get(storeKey);\n};\n\nexport const updateUniverseState = (event: ZunoStateEvent) => {\n\tconst current = universeState.get(event.storeKey) ?? {\n\t\tstate: undefined,\n\t\tversion: 0,\n\t};\n\tconst nextVersion =\n\t\ttypeof event.version === \"number\" ? event.version : current.version + 1;\n\tuniverseState.set(event.storeKey, {\n\t\tstate: event.state,\n\t\tversion: nextVersion,\n\t});\n};\n\nexport const getUniverseState = () => {\n\treturn Object.fromEntries(universeState);\n};\n\n// --- Event Log ---\n\nconst MAX_EVENTS = 1000;\nlet nextEventId = 1;\nconst eventLog: ZunoStateEvent[] = [];\n\nexport const appendEvent = (event: ZunoStateEvent) => {\n\tevent.eventId = nextEventId++;\n\teventLog.push(event);\n\tif (eventLog.length > MAX_EVENTS) {\n\t\teventLog.shift();\n\t}\n\treturn event;\n};\n\nexport const getEventsAfter = (lastEventId: number) => {\n\treturn eventLog.filter((event) => (event?.eventId ?? 0) > lastEventId);\n};\n\nexport const getLastEventId = () => {\n\treturn eventLog[eventLog.length - 1]?.eventId ?? 0;\n};\n\n// --- State Bus (Events) ---\n\nconst listeners = new Set<ZunoStateListener>();\n\nexport const subscribeToStateEvents = (listener: ZunoStateListener) => {\n\tlisteners.add(listener);\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t};\n};\n\nexport const publishToStateEvent = (event: ZunoStateEvent) => {\n\tlisteners.forEach((listener) => {\n\t\tlistener(event);\n\t});\n};\n","import type { ZunoStateEvent } from \"../sync\";\nimport {\n\tappendEvent,\n\tgetUniverseRecord,\n\tpublishToStateEvent,\n\tupdateUniverseState,\n} from \"./core\";\n\nexport type ApplyResult =\n\t| { ok: true; event: ZunoStateEvent }\n\t| {\n\t\t\tok: false;\n\t\t\treason: \"VERSION_CONFLICT\";\n\t\t\tcurrent: { state: unknown; version: number };\n\t }\n\t| { ok: false; reason: string; current?: never };\n\n/**\n * Validates and applies a state event to the server universe.\n */\nexport function applyStateEvent(incoming: ZunoStateEvent): ApplyResult {\n\tconst current = getUniverseRecord(incoming.storeKey) ?? {\n\t\tstate: undefined,\n\t\tversion: 0,\n\t};\n\n\t// Strict version check\n\tif (\n\t\ttypeof incoming.baseVersion === \"number\" &&\n\t\tincoming.baseVersion !== current.version\n\t) {\n\t\treturn { ok: false, reason: \"VERSION_CONFLICT\", current };\n\t}\n\n\t// Increment version\n\tconst nextVersion = current.version + 1;\n\tconst event = { ...incoming, version: nextVersion };\n\n\t// Persistence\n\tupdateUniverseState(event);\n\tappendEvent(event);\n\n\t// Notify SSE subscribers\n\tpublishToStateEvent(event);\n\n\treturn { ok: true, event };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { getLastEventId, getUniverseState } from \"./core\";\n\n/**\n * Sends a snapshot of the current universe state to the response.\n * Compatible with both Express and raw Node.js http.\n */\nexport function sendSnapshot(_req: IncomingMessage, res: ServerResponse) {\n\tconst snapshot = {\n\t\tstate: getUniverseState(),\n\t\tlastEventId: getLastEventId(),\n\t};\n\n\t// Check for Express-like .json() method\n\t// biome-ignore lint/suspicious/noExplicitAny: Checking for dynamic .json() method on response object\n\tif (\"json\" in res && typeof (res as any).json === \"function\") {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Calling dynamic .json() method\n\t\t(res as any).json(snapshot);\n\t} else {\n\t\tres.writeHead(200, { \"Content-Type\": \"application/json\" });\n\t\tres.end(JSON.stringify(snapshot));\n\t}\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ZunoStateEvent } from \"../sync\";\nimport { applyStateEvent } from \"./apply-state-event\";\nimport {\n\tgetEventsAfter,\n\tgetUniverseState,\n\tsubscribeToStateEvents,\n} from \"./core\";\n\ntype IncomingHeaders = IncomingMessage[\"headers\"];\n\n/**\n * Creates a Server-Sent Events (SSE) connection for Zuno state updates.\n */\nexport const createSSEConnection = (\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\theaders: IncomingHeaders,\n) => {\n\tres.writeHead(200, {\n\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\"Content-Type\": \"text/event-stream; charset=utf-8\",\n\t\tConnection: \"keep-alive\",\n\t\t\"X-Accel-Buffering\": \"no\",\n\t\t...headers,\n\t});\n\n\tres.flushHeaders?.();\n\n\tconst raw =\n\t\treq.headers[\"last-event-id\"] ||\n\t\tnew URL(req.url || \"\", \"http://localhost\").searchParams.get(\"lastEventId\");\n\tconst lastEventId =\n\t\tNumber.parseInt(Array.isArray(raw) ? raw[0] : (raw ?? \"0\"), 10) || 0;\n\n\t// 1. Subscribe FIRST and buffer events until snapshot/missed-events are sent\n\tconst buffer: ZunoStateEvent[] = [];\n\tlet isSyncing = true;\n\n\tconst writeEvent = (event: ZunoStateEvent) => {\n\t\tres.write(`id: ${event.eventId}\\n`);\n\t\tres.write(`event: state\\n`);\n\t\tres.write(`data: ${JSON.stringify(event)}\\n\\n`);\n\t};\n\n\tconst unsubscribe = subscribeToStateEvents((event: ZunoStateEvent) => {\n\t\tif (isSyncing) {\n\t\t\tbuffer.push(event);\n\t\t} else {\n\t\t\twriteEvent(event);\n\t\t}\n\t});\n\n\t// 2. Send missed events or snapshot\n\tif (lastEventId > 0) {\n\t\tconst missed = getEventsAfter(lastEventId);\n\t\tfor (const event of missed) {\n\t\t\twriteEvent(event);\n\t\t}\n\t} else {\n\t\tres.write(`event: snapshot\\n`);\n\t\tres.write(`data: ${JSON.stringify(getUniverseState())}\\n\\n`);\n\t}\n\n\t// 3. Flush buffer and switch to live mode\n\tisSyncing = false;\n\twhile (buffer.length > 0) {\n\t\tconst event = buffer.shift();\n\t\tif (event) writeEvent(event);\n\t}\n\n\tconst heartbeat = setInterval(() => {\n\t\tres.write(`: ping ${Date.now()}\\n\\n`);\n\t}, 15000);\n\n\tres.write(\": connected \\n\\n\");\n\n\treq.on(\"close\", () => {\n\t\tclearInterval(heartbeat);\n\t\tunsubscribe();\n\t\tres.end();\n\t});\n};\n\n/**\n * Synchronizes the Zuno universe state by applying an incoming event.\n */\nexport const syncUniverseState = (\n\treq: IncomingMessage,\n\tres: ServerResponse,\n) => {\n\tconst MAX_BODY_BYTES = 512 * 1024; // 512KB safety\n\tlet body = \"\";\n\n\treq.on(\"data\", (chunk: Buffer) => {\n\t\tbody += chunk.toString(\"utf8\");\n\t\tif (body.length > MAX_BODY_BYTES) {\n\t\t\tres.writeHead(413, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: false, reason: \"PAYLOAD_TOO_LARGE\" }));\n\t\t\treq.destroy();\n\t\t}\n\t});\n\n\treq.on(\"end\", () => {\n\t\ttry {\n\t\t\tconst incoming: ZunoStateEvent = JSON.parse(\n\t\t\t\tbody || \"{}\",\n\t\t\t) as unknown as ZunoStateEvent;\n\t\t\tconst result = applyStateEvent(incoming);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tif (result.reason === \"VERSION_CONFLICT\") {\n\t\t\t\t\tres.writeHead(409, { \"Content-Type\": \"application/json\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\treason: \"VERSION_CONFLICT\",\n\t\t\t\t\t\t\tcurrent: result.current,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tres.writeHead(200, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: true, event: result.event }));\n\t\t} catch {\n\t\t\tres.writeHead(400, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: false, reason: \"INVALID_JSON\" }));\n\t\t}\n\t});\n};\n\nexport const setUniverseState = (req: IncomingMessage, res: ServerResponse) => {\n\treturn syncUniverseState(req, res);\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/core.ts","../../src/server/apply-state-event.ts","../../src/server/snapshot-handler.ts","../../src/server/sse-handler.ts"],"names":["ZunoServerState","options","maxEvents","maxStateBytes","maxSubscriberBuffer","storeKey","event","current","nextVersion","lastEventId","latest","first","listener","createZunoServerState","ZunoServerRegistry","serverOptions","namespace","server","createZunoServerRegistry","defaultZunoServerState","getUniverseRecord","updateUniverseState","getUniverseState","appendEvent","getEventsAfter","getLastEventId","subscribeToStateEvents","publishToStateEvent","isRecord","value","validateStateEvent","input","errors","serialized","field","applyStateEvent","incoming","authoritativeEvent","sendSnapshot","_req","res","snapshot","createSSEConnection","req","headers","raw","buffer","pendingWrites","isSyncing","backpressured","closed","heartbeat","unsubscribe","closeConnection","flushPendingWrites","formatStateEvent","writeEvent","writeSnapshot","missed","syncUniverseState","body","chunk","result","setUniverseState"],"mappings":"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","file":"index.js","sourcesContent":["import type { ZunoStateEvent } from \"../sync\";\n\nexport type UniverseRecord = {\n\tstate: unknown;\n\tversion: number;\n};\n\nexport type ZunoStateListener = (event: ZunoStateEvent) => void;\n\nexport type CreateZunoServerStateOptions = {\n\t/** Maximum number of authoritative events retained for SSE replay. */\n\tmaxEvents?: number;\n\t/** Maximum serialized state size accepted per event. */\n\tmaxStateBytes?: number;\n\t/** Maximum events buffered for a slow SSE subscriber. */\n\tmaxSubscriberBuffer?: number;\n};\n\n/**\n * Isolated authoritative state used by server adapters.\n * Create one instance per application, namespace, or tenant boundary.\n */\nexport class ZunoServerState {\n\treadonly maxEvents: number;\n\treadonly maxStateBytes: number;\n\treadonly maxSubscriberBuffer: number;\n\tprivate readonly universeState = new Map<string, UniverseRecord>();\n\tprivate readonly eventLog: ZunoStateEvent[] = [];\n\tprivate readonly listeners = new Set<ZunoStateListener>();\n\tprivate nextEventId = 1;\n\n\tconstructor(options: CreateZunoServerStateOptions = {}) {\n\t\tconst maxEvents = options.maxEvents ?? 1000;\n\t\tconst maxStateBytes = options.maxStateBytes ?? 512 * 1024;\n\t\tconst maxSubscriberBuffer = options.maxSubscriberBuffer ?? 1000;\n\t\tif (!Number.isInteger(maxEvents) || maxEvents < 1) {\n\t\t\tthrow new TypeError(\"maxEvents must be a positive integer\");\n\t\t}\n\t\tif (!Number.isInteger(maxStateBytes) || maxStateBytes < 1) {\n\t\t\tthrow new TypeError(\"maxStateBytes must be a positive integer\");\n\t\t}\n\t\tif (!Number.isInteger(maxSubscriberBuffer) || maxSubscriberBuffer < 1) {\n\t\t\tthrow new TypeError(\"maxSubscriberBuffer must be a positive integer\");\n\t\t}\n\t\tthis.maxEvents = maxEvents;\n\t\tthis.maxStateBytes = maxStateBytes;\n\t\tthis.maxSubscriberBuffer = maxSubscriberBuffer;\n\t}\n\n\tgetUniverseRecord(storeKey: string): UniverseRecord | undefined {\n\t\treturn this.universeState.get(storeKey);\n\t}\n\n\tupdateUniverseState(event: ZunoStateEvent): void {\n\t\tconst current = this.universeState.get(event.storeKey) ?? {\n\t\t\tstate: undefined,\n\t\t\tversion: 0,\n\t\t};\n\t\tconst nextVersion =\n\t\t\ttypeof event.version === \"number\" ? event.version : current.version + 1;\n\t\tthis.universeState.set(event.storeKey, {\n\t\t\tstate: event.state,\n\t\t\tversion: nextVersion,\n\t\t});\n\t}\n\n\tgetUniverseState(): Record<string, UniverseRecord> {\n\t\treturn Object.fromEntries(this.universeState);\n\t}\n\n\tappendEvent(event: ZunoStateEvent): ZunoStateEvent {\n\t\tevent.eventId = this.nextEventId++;\n\t\tthis.eventLog.push(event);\n\t\tif (this.eventLog.length > this.maxEvents) {\n\t\t\tthis.eventLog.shift();\n\t\t}\n\t\treturn event;\n\t}\n\n\tgetEventsAfter(lastEventId: number): ZunoStateEvent[] {\n\t\treturn this.eventLog.filter((event) => (event.eventId ?? 0) > lastEventId);\n\t}\n\n\tcanReplayAfter(lastEventId: number): boolean {\n\t\tconst latest = this.getLastEventId();\n\t\tif (lastEventId === latest) return true;\n\t\tconst first = this.eventLog[0]?.eventId;\n\t\treturn (\n\t\t\ttypeof first === \"number\" &&\n\t\t\tlastEventId >= first - 1 &&\n\t\t\tlastEventId < latest\n\t\t);\n\t}\n\n\tgetLastEventId(): number {\n\t\treturn this.eventLog[this.eventLog.length - 1]?.eventId ?? 0;\n\t}\n\n\tsubscribeToStateEvents(listener: ZunoStateListener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tpublishToStateEvent(event: ZunoStateEvent): void {\n\t\tthis.listeners.forEach((listener) => {\n\t\t\tlistener(event);\n\t\t});\n\t}\n\n\tclear(): void {\n\t\tthis.universeState.clear();\n\t\tthis.eventLog.length = 0;\n\t\tthis.nextEventId = 1;\n\t}\n}\n\nexport const createZunoServerState = (\n\toptions: CreateZunoServerStateOptions = {},\n) => new ZunoServerState(options);\n\n/** Lazily creates isolated server states for application namespaces or tenants. */\nexport class ZunoServerRegistry {\n\tprivate readonly servers = new Map<string, ZunoServerState>();\n\n\tconstructor(\n\t\tprivate readonly serverOptions: CreateZunoServerStateOptions = {},\n\t) {}\n\n\tget(namespace: string): ZunoServerState {\n\t\tif (namespace.trim().length === 0) {\n\t\t\tthrow new TypeError(\"namespace must be a non-empty string\");\n\t\t}\n\t\tlet server = this.servers.get(namespace);\n\t\tif (!server) {\n\t\t\tserver = createZunoServerState(this.serverOptions);\n\t\t\tthis.servers.set(namespace, server);\n\t\t}\n\t\treturn server;\n\t}\n\n\tdelete(namespace: string): boolean {\n\t\treturn this.servers.delete(namespace);\n\t}\n\n\tclear(): void {\n\t\tthis.servers.clear();\n\t}\n}\n\nexport const createZunoServerRegistry = (\n\toptions: CreateZunoServerStateOptions = {},\n) => new ZunoServerRegistry(options);\n\n/** Backward-compatible singleton used by the original module-level helpers. */\nexport const defaultZunoServerState = createZunoServerState();\n\nexport const getUniverseRecord = (storeKey: string) =>\n\tdefaultZunoServerState.getUniverseRecord(storeKey);\nexport const updateUniverseState = (event: ZunoStateEvent) =>\n\tdefaultZunoServerState.updateUniverseState(event);\nexport const getUniverseState = () => defaultZunoServerState.getUniverseState();\nexport const appendEvent = (event: ZunoStateEvent) =>\n\tdefaultZunoServerState.appendEvent(event);\nexport const getEventsAfter = (lastEventId: number) =>\n\tdefaultZunoServerState.getEventsAfter(lastEventId);\nexport const getLastEventId = () => defaultZunoServerState.getLastEventId();\nexport const subscribeToStateEvents = (listener: ZunoStateListener) =>\n\tdefaultZunoServerState.subscribeToStateEvents(listener);\nexport const publishToStateEvent = (event: ZunoStateEvent) =>\n\tdefaultZunoServerState.publishToStateEvent(event);\n","import type { ZunoStateEvent } from \"../sync\";\nimport { defaultZunoServerState, type ZunoServerState } from \"./core\";\n\nexport type EventValidationError = {\n\tfield: string;\n\tmessage: string;\n};\n\nexport type ApplyResult =\n\t| { ok: true; event: ZunoStateEvent }\n\t| {\n\t\t\tok: false;\n\t\t\treason: \"VERSION_CONFLICT\";\n\t\t\tcurrent: { state: unknown; version: number };\n\t\t\terrors?: never;\n\t }\n\t| {\n\t\t\tok: false;\n\t\t\treason: \"INVALID_EVENT\";\n\t\t\terrors: EventValidationError[];\n\t\t\tcurrent?: never;\n\t };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === \"object\" && value !== null && !Array.isArray(value);\n\nexport function validateStateEvent(\n\tinput: unknown,\n\tmaxStateBytes = 512 * 1024,\n): EventValidationError[] {\n\tif (!isRecord(input)) {\n\t\treturn [{ field: \"event\", message: \"Event must be an object\" }];\n\t}\n\n\tconst errors: EventValidationError[] = [];\n\tif (\n\t\ttypeof input.storeKey !== \"string\" ||\n\t\tinput.storeKey.trim().length === 0 ||\n\t\tinput.storeKey.length > 256\n\t) {\n\t\terrors.push({\n\t\t\tfield: \"storeKey\",\n\t\t\tmessage: \"storeKey must be a non-empty string of at most 256 characters\",\n\t\t});\n\t}\n\n\tif (!(\"state\" in input)) {\n\t\terrors.push({ field: \"state\", message: \"state is required\" });\n\t} else {\n\t\ttry {\n\t\t\tconst serialized = JSON.stringify(input.state);\n\t\t\tif (serialized === undefined) {\n\t\t\t\terrors.push({\n\t\t\t\t\tfield: \"state\",\n\t\t\t\t\tmessage: \"state must be JSON-serializable\",\n\t\t\t\t});\n\t\t\t} else if (\n\t\t\t\tnew TextEncoder().encode(serialized).byteLength > maxStateBytes\n\t\t\t) {\n\t\t\t\terrors.push({\n\t\t\t\t\tfield: \"state\",\n\t\t\t\t\tmessage: `state exceeds the ${maxStateBytes} byte limit`,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch {\n\t\t\terrors.push({\n\t\t\t\tfield: \"state\",\n\t\t\t\tmessage: \"state must be JSON-serializable\",\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const field of [\"version\", \"baseVersion\", \"eventId\"] as const) {\n\t\tconst value = input[field];\n\t\tif (\n\t\t\tvalue !== undefined &&\n\t\t\t(!Number.isInteger(value) || (value as number) < 0)\n\t\t) {\n\t\t\terrors.push({\n\t\t\t\tfield,\n\t\t\t\tmessage: `${field} must be a non-negative integer when provided`,\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const field of [\"origin\"] as const) {\n\t\tconst value = input[field];\n\t\tif (value !== undefined && typeof value !== \"string\") {\n\t\t\terrors.push({\n\t\t\t\tfield,\n\t\t\t\tmessage: `${field} must be a string when provided`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (input.ts !== undefined && !Number.isFinite(input.ts)) {\n\t\terrors.push({\n\t\t\tfield: \"ts\",\n\t\t\tmessage: \"ts must be a finite number when provided\",\n\t\t});\n\t}\n\n\tif (input.intent !== undefined) {\n\t\tif (\n\t\t\t!isRecord(input.intent) ||\n\t\t\ttypeof input.intent.type !== \"string\" ||\n\t\t\tinput.intent.type.trim().length === 0\n\t\t) {\n\t\t\terrors.push({\n\t\t\t\tfield: \"intent\",\n\t\t\t\tmessage: \"intent must contain a non-empty string type\",\n\t\t\t});\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n/** Validates and applies a state event to an isolated authoritative server. */\nexport function applyStateEvent(\n\tincoming: unknown,\n\tserver: ZunoServerState = defaultZunoServerState,\n): ApplyResult {\n\tconst errors = validateStateEvent(incoming, server.maxStateBytes);\n\tif (errors.length > 0) {\n\t\treturn { ok: false, reason: \"INVALID_EVENT\", errors };\n\t}\n\n\tconst event = incoming as ZunoStateEvent;\n\tconst current = server.getUniverseRecord(event.storeKey) ?? {\n\t\tstate: undefined,\n\t\tversion: 0,\n\t};\n\n\tif (\n\t\ttypeof event.baseVersion === \"number\" &&\n\t\tevent.baseVersion !== current.version\n\t) {\n\t\treturn { ok: false, reason: \"VERSION_CONFLICT\", current };\n\t}\n\n\tconst authoritativeEvent = { ...event, version: current.version + 1 };\n\tserver.updateUniverseState(authoritativeEvent);\n\tserver.appendEvent(authoritativeEvent);\n\tserver.publishToStateEvent(authoritativeEvent);\n\n\treturn { ok: true, event: authoritativeEvent };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { defaultZunoServerState, type ZunoServerState } from \"./core\";\n\n/**\n * Sends a snapshot of the current universe state to the response.\n * Compatible with both Express and raw Node.js http.\n */\nexport function sendSnapshot(\n\t_req: IncomingMessage,\n\tres: ServerResponse,\n\tserver: ZunoServerState = defaultZunoServerState,\n) {\n\tconst snapshot = {\n\t\tstate: server.getUniverseState(),\n\t\tlastEventId: server.getLastEventId(),\n\t};\n\n\t// Check for Express-like .json() method\n\t// biome-ignore lint/suspicious/noExplicitAny: Checking for dynamic .json() method on response object\n\tif (\"json\" in res && typeof (res as any).json === \"function\") {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Calling dynamic .json() method\n\t\t(res as any).json(snapshot);\n\t} else {\n\t\tres.writeHead(200, { \"Content-Type\": \"application/json\" });\n\t\tres.end(JSON.stringify(snapshot));\n\t}\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ZunoStateEvent } from \"../sync\";\nimport { applyStateEvent } from \"./apply-state-event\";\nimport { defaultZunoServerState, type ZunoServerState } from \"./core\";\n\ntype IncomingHeaders = IncomingMessage[\"headers\"];\n\n/**\n * Creates a Server-Sent Events (SSE) connection for Zuno state updates.\n */\nexport const createSSEConnection = (\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\theaders: IncomingHeaders,\n\tserver: ZunoServerState = defaultZunoServerState,\n) => {\n\tres.writeHead(200, {\n\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\"Content-Type\": \"text/event-stream; charset=utf-8\",\n\t\tConnection: \"keep-alive\",\n\t\t\"X-Accel-Buffering\": \"no\",\n\t\t...headers,\n\t});\n\n\tres.flushHeaders?.();\n\n\tconst raw =\n\t\treq.headers[\"last-event-id\"] ||\n\t\tnew URL(req.url || \"\", \"http://localhost\").searchParams.get(\"lastEventId\");\n\tconst lastEventId =\n\t\tNumber.parseInt(Array.isArray(raw) ? raw[0] : (raw ?? \"0\"), 10) || 0;\n\n\t// 1. Subscribe FIRST and buffer events until snapshot/missed-events are sent\n\tconst buffer: ZunoStateEvent[] = [];\n\tconst pendingWrites: ZunoStateEvent[] = [];\n\tlet isSyncing = true;\n\tlet backpressured = false;\n\tlet closed = false;\n\tlet heartbeat: ReturnType<typeof setInterval> | null = null;\n\tlet unsubscribe = () => {};\n\n\tconst closeConnection = () => {\n\t\tif (closed) return;\n\t\tclosed = true;\n\t\tif (heartbeat) clearInterval(heartbeat);\n\t\tunsubscribe();\n\t\tres.end();\n\t};\n\tconst flushPendingWrites = () => {\n\t\tbackpressured = false;\n\t\twhile (!closed && pendingWrites.length > 0) {\n\t\t\tconst event = pendingWrites.shift();\n\t\t\tif (!event) continue;\n\t\t\tif (!res.write(formatStateEvent(event))) {\n\t\t\t\tbackpressured = true;\n\t\t\t\tres.once(\"drain\", flushPendingWrites);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\tconst formatStateEvent = (event: ZunoStateEvent) =>\n\t\t`id: ${event.eventId}\\nevent: state\\ndata: ${JSON.stringify(event)}\\n\\n`;\n\tconst writeEvent = (event: ZunoStateEvent) => {\n\t\tif (closed) return;\n\t\tif (backpressured) {\n\t\t\tif (pendingWrites.length >= server.maxSubscriberBuffer) {\n\t\t\t\tcloseConnection();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpendingWrites.push(event);\n\t\t\treturn;\n\t\t}\n\t\tif (!res.write(formatStateEvent(event))) {\n\t\t\tbackpressured = true;\n\t\t\tres.once(\"drain\", flushPendingWrites);\n\t\t}\n\t};\n\tconst writeSnapshot = () => {\n\t\tres.write(`id: ${server.getLastEventId()}\\n`);\n\t\tres.write(\"event: snapshot\\n\");\n\t\tres.write(`data: ${JSON.stringify(server.getUniverseState())}\\n\\n`);\n\t};\n\n\tunsubscribe = server.subscribeToStateEvents((event: ZunoStateEvent) => {\n\t\tif (isSyncing) {\n\t\t\tif (buffer.length >= server.maxSubscriberBuffer) {\n\t\t\t\tcloseConnection();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbuffer.push(event);\n\t\t} else {\n\t\t\twriteEvent(event);\n\t\t}\n\t});\n\n\t// 2. Send missed events or snapshot\n\tif (lastEventId > 0 && server.canReplayAfter(lastEventId)) {\n\t\tconst missed = server.getEventsAfter(lastEventId);\n\t\tfor (const event of missed) {\n\t\t\twriteEvent(event);\n\t\t}\n\t} else {\n\t\twriteSnapshot();\n\t}\n\n\t// 3. Flush buffer and switch to live mode\n\tisSyncing = false;\n\twhile (buffer.length > 0) {\n\t\tconst event = buffer.shift();\n\t\tif (event) writeEvent(event);\n\t}\n\tif (closed) return;\n\n\theartbeat = setInterval(() => {\n\t\tif (closed) return;\n\t\tres.write(`: ping ${Date.now()}\\n\\n`);\n\t}, 15000);\n\n\tres.write(\": connected \\n\\n\");\n\n\treq.on(\"close\", () => {\n\t\tcloseConnection();\n\t});\n};\n\n/**\n * Synchronizes the Zuno universe state by applying an incoming event.\n */\nexport const syncUniverseState = (\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\tserver: ZunoServerState = defaultZunoServerState,\n) => {\n\tconst MAX_BODY_BYTES = 512 * 1024; // 512KB safety\n\tlet body = \"\";\n\n\treq.on(\"data\", (chunk: Buffer) => {\n\t\tbody += chunk.toString(\"utf8\");\n\t\tif (body.length > MAX_BODY_BYTES) {\n\t\t\tres.writeHead(413, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: false, reason: \"PAYLOAD_TOO_LARGE\" }));\n\t\t\treq.destroy();\n\t\t}\n\t});\n\n\treq.on(\"end\", () => {\n\t\ttry {\n\t\t\tconst incoming: ZunoStateEvent = JSON.parse(\n\t\t\t\tbody || \"{}\",\n\t\t\t) as unknown as ZunoStateEvent;\n\t\t\tconst result = applyStateEvent(incoming, server);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tif (result.reason === \"VERSION_CONFLICT\") {\n\t\t\t\t\tres.writeHead(409, { \"Content-Type\": \"application/json\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\treason: \"VERSION_CONFLICT\",\n\t\t\t\t\t\t\tcurrent: result.current,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"application/json\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\treason: result.reason,\n\t\t\t\t\t\t\terrors: result.errors,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tres.writeHead(200, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: true, event: result.event }));\n\t\t} catch {\n\t\t\tres.writeHead(400, { \"Content-Type\": \"application/json\" });\n\t\t\tres.end(JSON.stringify({ ok: false, reason: \"INVALID_JSON\" }));\n\t\t}\n\t});\n};\n\nexport const setUniverseState = (\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\tserver: ZunoServerState = defaultZunoServerState,\n) => {\n\treturn syncUniverseState(req, res, server);\n};\n"]}
|