@iadev93/zuno-elysia 0.0.8 → 0.0.10

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 CHANGED
@@ -53,6 +53,9 @@ An async generator handler for Server-Sent Events. It automatically handles:
53
53
  - Connection keep-alive and heartbeats.
54
54
  - Reconnection logic via `last-event-id`.
55
55
  - Initial state snapshots for fresh connections.
56
+ - Retained-event replay after short interruptions.
57
+ - Authoritative snapshot fallback when the requested replay range has expired.
58
+ - Bounded per-subscriber buffering through the selected server state's `maxSubscriberBuffer` option.
56
59
 
57
60
  #### `sync` (POST)
58
61
  Validates and applies incoming state events to the Zuno universe. Handles version conflicts and broadcasts updates to all connected SSE clients.
@@ -62,6 +65,8 @@ Returns the current full state of the universe, the current version, and the las
62
65
 
63
66
  ## Features
64
67
  - **Native SSE**: Uses Elysia's optimized streaming capabilities with async generators.
68
+ - **Resilient Recovery**: Replays missed events when retained and falls back to an event-ID-bearing snapshot after a replay gap.
69
+ - **Backpressure Protection**: Disconnects subscribers that exceed the configured pending-message buffer so they can recover cleanly on reconnect.
65
70
  - **Lightweight**: Zero runtime dependencies on Elysia itself (uses structural typing).
66
71
  - **Type Safe**: Fully written in TypeScript with comprehensive docstrings.
67
72
 
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var server=require('@iadev93/zuno/server'),elysia=require('elysia');function w(d={}){let t=d.server??server.createZunoServerState(),l=d.authorize??(()=>true);return {sse:async function*(r){let{set:a,headers:o,query:i}=r;if(!await l({action:"read",request:r})){a.status=403;return}a.headers["Content-Type"]="text/event-stream",a.headers["Cache-Control"]="no-cache",a.headers.Connection="keep-alive",yield elysia.sse({data:": connected"});let e=[],c=false,u=null,f=n=>{e.length>=t.maxSubscriberBuffer?c=true:e.push(n),u&&(u(),u=null);},S=t.subscribeToStateEvents(n=>{f(elysia.sse({id:String(n.eventId),event:"state",data:JSON.stringify(n)}));}),h=o["last-event-id"]||i?.lastEventId,v=Number.parseInt(h||"0",10)||0;if(v>0&&t.canReplayAfter(v)){let n=t.getEventsAfter(v);for(let y of n)yield elysia.sse({id:String(y.eventId),event:"state",data:JSON.stringify(y)});}else {let n=t.getUniverseState();yield elysia.sse({id:String(t.getLastEventId()),event:"snapshot",data:JSON.stringify(n)});}let p=setInterval(()=>{f(elysia.sse({data:`: ping ${Date.now()}`}));},15e3);try{for(;;){if(c||(e.length===0&&await new Promise(n=>{u=n;}),c))return;for(;e.length>0;)yield e.shift();}}finally{clearInterval(p),S();}},sync:async r=>{let{body:a,set:o}=r,i=a;if(!await l({action:"write",request:r,event:i}))return o.status=403,{ok:false,reason:"FORBIDDEN"};let e=server.applyStateEvent(i,t);return e.ok?{ok:true,event:e.event}:(o.status=e.reason==="VERSION_CONFLICT"?409:400,{ok:false,reason:e.reason,...e.reason==="VERSION_CONFLICT"?{current:e.current}:{errors:e.errors}})},snapshot:async r=>await l({action:"read",request:r})?{state:t.getUniverseState(),lastEventId:t.getLastEventId()}:(r.set.status=403,{ok:false,reason:"FORBIDDEN"}),server:t}}exports.createZunoElysia=w;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["createZunoElysia","options","server","createZunoServerState","authorize","request","set","headers","query","sse","queue","overflowed","resolve","enqueue","message","unsubscribe","event","rawLastEventId","lastEventId","missed","snapshot","heartbeat","r","body","incoming","result","applyStateEvent"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ZunoStateEvent } from \"@iadev93/zuno\";\nimport {\n\tapplyStateEvent,\n\tcreateZunoServerState,\n\ttype ZunoServerState,\n} from \"@iadev93/zuno/server\";\nimport { sse } from \"elysia\";\n\n/**\n * Creates a Zuno Elysia instance.\n * @returns {Object} An object with the following properties:\n * - sse: An async generator function that handles SSE connections for Elysia.\n * @property {Object} set - Elysia response object.\n * @property {Object} headers - Elysia request headers.\n * @property {Object} query - Elysia request query parameters.\n * - sync: A function that handles sync POST requests for Elysia.\n * @property {Object} body - Elysia request body.\n * @property {Object} set - Elysia response object.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n */\nexport type CreateZunoElysiaOptions = {\n\t/** Isolated authoritative server state. A new instance is created by default. */\n\tserver?: ZunoServerState;\n\t/** Provider-agnostic authorization hook for snapshots, SSE, and mutations. */\n\tauthorize?: (context: {\n\t\taction: \"read\" | \"write\";\n\t\trequest: unknown;\n\t\tevent?: unknown;\n\t}) => boolean | Promise<boolean>;\n};\n\nexport function createZunoElysia(options: CreateZunoElysiaOptions = {}) {\n\tconst server = options.server ?? createZunoServerState();\n\tconst authorize = options.authorize ?? (() => true);\n\treturn {\n\t\t/**\n\t\t * Handles SSE connections for Elysia using an async generator.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @param {Object} param.headers - Elysia request headers.\n\t\t * @param {Object} param.query - Elysia request query parameters.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request object\n\t\tsse: async function* (request: any) {\n\t\t\tconst { set, headers, query } = request;\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tset.headers[\"Content-Type\"] = \"text/event-stream\";\n\t\t\tset.headers[\"Cache-Control\"] = \"no-cache\";\n\t\t\tset.headers.Connection = \"keep-alive\";\n\n\t\t\tyield sse({ data: \": connected\" });\n\n\t\t\t// 1. Subscribe FIRST to avoid missing events during snapshot/missed-events retrieval\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: queue of any SSE events\n\t\t\tconst queue: any[] = [];\n\t\t\tlet overflowed = false;\n\t\t\tlet resolve: ((value: void | PromiseLike<void>) => void) | null = null;\n\t\t\tconst enqueue = (message: unknown) => {\n\t\t\t\tif (queue.length >= server.maxSubscriberBuffer) {\n\t\t\t\t\toverflowed = true;\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push(message);\n\t\t\t\t}\n\t\t\t\tif (resolve) {\n\t\t\t\t\tresolve();\n\t\t\t\t\tresolve = null;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst unsubscribe = server.subscribeToStateEvents(\n\t\t\t\t(event: ZunoStateEvent) => {\n\t\t\t\t\tenqueue(\n\t\t\t\t\t\tsse({\n\t\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t);\n\n\t\t\t// 2. Decide if we send snapshot or missed events\n\t\t\tconst rawLastEventId = headers[\"last-event-id\"] || query?.lastEventId;\n\t\t\tconst lastEventId = Number.parseInt(rawLastEventId || \"0\", 10) || 0;\n\n\t\t\tif (lastEventId > 0 && server.canReplayAfter(lastEventId)) {\n\t\t\t\tconst missed = server.getEventsAfter(lastEventId);\n\t\t\t\tfor (const event of missed) {\n\t\t\t\t\tyield sse({\n\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst snapshot = server.getUniverseState();\n\t\t\t\tyield sse({\n\t\t\t\t\tid: String(server.getLastEventId()),\n\t\t\t\t\tevent: \"snapshot\",\n\t\t\t\t\tdata: JSON.stringify(snapshot),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// 3. Heartbeat interval\n\t\t\tconst heartbeat = setInterval(() => {\n\t\t\t\tenqueue(sse({ data: `: ping ${Date.now()}` }));\n\t\t\t}, 15000);\n\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (overflowed) return;\n\t\t\t\t\tif (queue.length === 0) {\n\t\t\t\t\t\tawait new Promise<void>((r) => {\n\t\t\t\t\t\t\tresolve = r;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (overflowed) return;\n\n\t\t\t\t\twhile (queue.length > 0) {\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: queue is checked for length > 0\n\t\t\t\t\t\tyield queue.shift()!;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tclearInterval(heartbeat);\n\t\t\t\tunsubscribe();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handles sync POST requests for Elysia.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.body - Elysia request body.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - ok: A boolean indicating whether the sync was successful.\n\t\t * - event: The event that was applied to the universe.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request body\n\t\tsync: async (request: any) => {\n\t\t\tconst { body, set } = request;\n\t\t\tconst incoming = body as ZunoStateEvent;\n\t\t\tif (!(await authorize({ action: \"write\", request, event: incoming }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\tconst result = applyStateEvent(incoming, server);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tset.status = result.reason === \"VERSION_CONFLICT\" ? 409 : 400;\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\treason: result.reason,\n\t\t\t\t\t...(result.reason === \"VERSION_CONFLICT\"\n\t\t\t\t\t\t? { current: result.current }\n\t\t\t\t\t\t: { errors: result.errors }),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { ok: true, event: result.event };\n\t\t},\n\n\t\t/**\n\t\t * Handles snapshot GET requests for Elysia.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - state: The current state of the universe.\n\t\t * - version: The version of the universe.\n\t\t * - lastEventId: The ID of the last event in the universe.\n\t\t */\n\t\tsnapshot: async (request: { set: { status?: number } }) => {\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\trequest.set.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstate: server.getUniverseState(),\n\t\t\t\tlastEventId: server.getLastEventId(),\n\t\t\t};\n\t\t},\n\t\tserver,\n\t};\n}\n"]}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- 'use strict';var server=require('@iadev93/zuno/server'),elysia=require('elysia');function m(v={}){let a=v.server??server.createZunoServerState(),l=v.authorize??(()=>true);return {sse:async function*(t){let{set:r,headers:i,query:u}=t;if(!await l({action:"read",request:t})){r.status=403;return}r.headers["Content-Type"]="text/event-stream",r.headers["Cache-Control"]="no-cache",r.headers.Connection="keep-alive",yield elysia.sse({data:": connected"});let e=[],s=null,y=a.subscribeToStateEvents(n=>{e.push(elysia.sse({id:String(n.eventId),event:"state",data:JSON.stringify(n)})),s&&(s(),s=null);}),f=i["last-event-id"]||u?.lastEventId,c=Number.parseInt(f||"0",10)||0;if(c>0){let n=a.getEventsAfter(c);for(let d of n)yield elysia.sse({id:String(d.eventId),event:"state",data:JSON.stringify(d)});}else {let n=a.getUniverseState();yield elysia.sse({event:"snapshot",data:JSON.stringify(n)});}let h=setInterval(()=>{e.push(elysia.sse({data:`: ping ${Date.now()}`})),s&&(s(),s=null);},15e3);try{for(;;)for(e.length===0&&await new Promise(n=>{s=n;});e.length>0;)yield e.shift();}finally{clearInterval(h),y();}},sync:async t=>{let{body:r,set:i}=t,u=r;if(!await l({action:"write",request:t,event:u}))return i.status=403,{ok:false,reason:"FORBIDDEN"};let e=server.applyStateEvent(u,a);return e.ok?{ok:true,event:e.event}:(i.status=e.reason==="VERSION_CONFLICT"?409:400,{ok:false,reason:e.reason,...e.reason==="VERSION_CONFLICT"?{current:e.current}:{errors:e.errors}})},snapshot:async t=>await l({action:"read",request:t})?{state:a.getUniverseState(),lastEventId:a.getLastEventId()}:(t.set.status=403,{ok:false,reason:"FORBIDDEN"}),server:a}}exports.createZunoElysia=m;//# sourceMappingURL=index.js.map
1
+ import {createZunoServerState,applyStateEvent}from'@iadev93/zuno/server';import {sse}from'elysia';function w(d={}){let t=d.server??createZunoServerState(),l=d.authorize??(()=>true);return {sse:async function*(r){let{set:a,headers:o,query:i}=r;if(!await l({action:"read",request:r})){a.status=403;return}a.headers["Content-Type"]="text/event-stream",a.headers["Cache-Control"]="no-cache",a.headers.Connection="keep-alive",yield sse({data:": connected"});let e=[],c=false,u=null,f=n=>{e.length>=t.maxSubscriberBuffer?c=true:e.push(n),u&&(u(),u=null);},S=t.subscribeToStateEvents(n=>{f(sse({id:String(n.eventId),event:"state",data:JSON.stringify(n)}));}),h=o["last-event-id"]||i?.lastEventId,v=Number.parseInt(h||"0",10)||0;if(v>0&&t.canReplayAfter(v)){let n=t.getEventsAfter(v);for(let y of n)yield sse({id:String(y.eventId),event:"state",data:JSON.stringify(y)});}else {let n=t.getUniverseState();yield sse({id:String(t.getLastEventId()),event:"snapshot",data:JSON.stringify(n)});}let p=setInterval(()=>{f(sse({data:`: ping ${Date.now()}`}));},15e3);try{for(;;){if(c||(e.length===0&&await new Promise(n=>{u=n;}),c))return;for(;e.length>0;)yield e.shift();}}finally{clearInterval(p),S();}},sync:async r=>{let{body:a,set:o}=r,i=a;if(!await l({action:"write",request:r,event:i}))return o.status=403,{ok:false,reason:"FORBIDDEN"};let e=applyStateEvent(i,t);return e.ok?{ok:true,event:e.event}:(o.status=e.reason==="VERSION_CONFLICT"?409:400,{ok:false,reason:e.reason,...e.reason==="VERSION_CONFLICT"?{current:e.current}:{errors:e.errors}})},snapshot:async r=>await l({action:"read",request:r})?{state:t.getUniverseState(),lastEventId:t.getLastEventId()}:(r.set.status=403,{ok:false,reason:"FORBIDDEN"}),server:t}}export{w as createZunoElysia};//# sourceMappingURL=index.js.map
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["createZunoElysia","options","server","createZunoServerState","authorize","request","set","headers","query","sse","queue","resolve","unsubscribe","event","rawLastEventId","lastEventId","missed","snapshot","heartbeat","r","body","incoming","result","applyStateEvent"],"mappings":"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","file":"index.js","sourcesContent":["import type { ZunoStateEvent } from \"@iadev93/zuno\";\nimport {\n\tapplyStateEvent,\n\tcreateZunoServerState,\n\ttype ZunoServerState,\n} from \"@iadev93/zuno/server\";\nimport { sse } from \"elysia\";\n\n/**\n * Creates a Zuno Elysia instance.\n * @returns {Object} An object with the following properties:\n * - sse: An async generator function that handles SSE connections for Elysia.\n * @property {Object} set - Elysia response object.\n * @property {Object} headers - Elysia request headers.\n * @property {Object} query - Elysia request query parameters.\n * - sync: A function that handles sync POST requests for Elysia.\n * @property {Object} body - Elysia request body.\n * @property {Object} set - Elysia response object.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n */\nexport type CreateZunoElysiaOptions = {\n\t/** Isolated authoritative server state. A new instance is created by default. */\n\tserver?: ZunoServerState;\n\t/** Provider-agnostic authorization hook for snapshots, SSE, and mutations. */\n\tauthorize?: (context: {\n\t\taction: \"read\" | \"write\";\n\t\trequest: unknown;\n\t\tevent?: unknown;\n\t}) => boolean | Promise<boolean>;\n};\n\nexport function createZunoElysia(options: CreateZunoElysiaOptions = {}) {\n\tconst server = options.server ?? createZunoServerState();\n\tconst authorize = options.authorize ?? (() => true);\n\treturn {\n\t\t/**\n\t\t * Handles SSE connections for Elysia using an async generator.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @param {Object} param.headers - Elysia request headers.\n\t\t * @param {Object} param.query - Elysia request query parameters.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request object\n\t\tsse: async function* (request: any) {\n\t\t\tconst { set, headers, query } = request;\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tset.headers[\"Content-Type\"] = \"text/event-stream\";\n\t\t\tset.headers[\"Cache-Control\"] = \"no-cache\";\n\t\t\tset.headers.Connection = \"keep-alive\";\n\n\t\t\tyield sse({ data: \": connected\" });\n\n\t\t\t// 1. Subscribe FIRST to avoid missing events during snapshot/missed-events retrieval\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: queue of any SSE events\n\t\t\tconst queue: any[] = [];\n\t\t\tlet resolve: ((value: void | PromiseLike<void>) => void) | null = null;\n\n\t\t\tconst unsubscribe = server.subscribeToStateEvents(\n\t\t\t\t(event: ZunoStateEvent) => {\n\t\t\t\t\tqueue.push(\n\t\t\t\t\t\tsse({\n\t\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tif (resolve) {\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t\tresolve = null;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\n\t\t\t// 2. Decide if we send snapshot or missed events\n\t\t\tconst rawLastEventId = headers[\"last-event-id\"] || query?.lastEventId;\n\t\t\tconst lastEventId = Number.parseInt(rawLastEventId || \"0\", 10) || 0;\n\n\t\t\tif (lastEventId > 0) {\n\t\t\t\tconst missed = server.getEventsAfter(lastEventId);\n\t\t\t\tfor (const event of missed) {\n\t\t\t\t\tyield sse({\n\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst snapshot = server.getUniverseState();\n\t\t\t\tyield sse({\n\t\t\t\t\tevent: \"snapshot\",\n\t\t\t\t\tdata: JSON.stringify(snapshot),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// 3. Heartbeat interval\n\t\t\tconst heartbeat = setInterval(() => {\n\t\t\t\tqueue.push(sse({ data: `: ping ${Date.now()}` }));\n\t\t\t\tif (resolve) {\n\t\t\t\t\tresolve();\n\t\t\t\t\tresolve = null;\n\t\t\t\t}\n\t\t\t}, 15000);\n\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (queue.length === 0) {\n\t\t\t\t\t\tawait new Promise<void>((r) => {\n\t\t\t\t\t\t\tresolve = r;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\twhile (queue.length > 0) {\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: queue is checked for length > 0\n\t\t\t\t\t\tyield queue.shift()!;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tclearInterval(heartbeat);\n\t\t\t\tunsubscribe();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handles sync POST requests for Elysia.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.body - Elysia request body.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - ok: A boolean indicating whether the sync was successful.\n\t\t * - event: The event that was applied to the universe.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request body\n\t\tsync: async (request: any) => {\n\t\t\tconst { body, set } = request;\n\t\t\tconst incoming = body as ZunoStateEvent;\n\t\t\tif (!(await authorize({ action: \"write\", request, event: incoming }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\tconst result = applyStateEvent(incoming, server);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tset.status = result.reason === \"VERSION_CONFLICT\" ? 409 : 400;\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\treason: result.reason,\n\t\t\t\t\t...(result.reason === \"VERSION_CONFLICT\"\n\t\t\t\t\t\t? { current: result.current }\n\t\t\t\t\t\t: { errors: result.errors }),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { ok: true, event: result.event };\n\t\t},\n\n\t\t/**\n\t\t * Handles snapshot GET requests for Elysia.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - state: The current state of the universe.\n\t\t * - version: The version of the universe.\n\t\t * - lastEventId: The ID of the last event in the universe.\n\t\t */\n\t\tsnapshot: async (request: { set: { status?: number } }) => {\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\trequest.set.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstate: server.getUniverseState(),\n\t\t\t\tlastEventId: server.getLastEventId(),\n\t\t\t};\n\t\t},\n\t\tserver,\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["createZunoElysia","options","server","createZunoServerState","authorize","request","set","headers","query","sse","queue","overflowed","resolve","enqueue","message","unsubscribe","event","rawLastEventId","lastEventId","missed","snapshot","heartbeat","r","body","incoming","result","applyStateEvent"],"mappings":"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","file":"index.js","sourcesContent":["import type { ZunoStateEvent } from \"@iadev93/zuno\";\nimport {\n\tapplyStateEvent,\n\tcreateZunoServerState,\n\ttype ZunoServerState,\n} from \"@iadev93/zuno/server\";\nimport { sse } from \"elysia\";\n\n/**\n * Creates a Zuno Elysia instance.\n * @returns {Object} An object with the following properties:\n * - sse: An async generator function that handles SSE connections for Elysia.\n * @property {Object} set - Elysia response object.\n * @property {Object} headers - Elysia request headers.\n * @property {Object} query - Elysia request query parameters.\n * - sync: A function that handles sync POST requests for Elysia.\n * @property {Object} body - Elysia request body.\n * @property {Object} set - Elysia response object.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n */\nexport type CreateZunoElysiaOptions = {\n\t/** Isolated authoritative server state. A new instance is created by default. */\n\tserver?: ZunoServerState;\n\t/** Provider-agnostic authorization hook for snapshots, SSE, and mutations. */\n\tauthorize?: (context: {\n\t\taction: \"read\" | \"write\";\n\t\trequest: unknown;\n\t\tevent?: unknown;\n\t}) => boolean | Promise<boolean>;\n};\n\nexport function createZunoElysia(options: CreateZunoElysiaOptions = {}) {\n\tconst server = options.server ?? createZunoServerState();\n\tconst authorize = options.authorize ?? (() => true);\n\treturn {\n\t\t/**\n\t\t * Handles SSE connections for Elysia using an async generator.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @param {Object} param.headers - Elysia request headers.\n\t\t * @param {Object} param.query - Elysia request query parameters.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request object\n\t\tsse: async function* (request: any) {\n\t\t\tconst { set, headers, query } = request;\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tset.headers[\"Content-Type\"] = \"text/event-stream\";\n\t\t\tset.headers[\"Cache-Control\"] = \"no-cache\";\n\t\t\tset.headers.Connection = \"keep-alive\";\n\n\t\t\tyield sse({ data: \": connected\" });\n\n\t\t\t// 1. Subscribe FIRST to avoid missing events during snapshot/missed-events retrieval\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: queue of any SSE events\n\t\t\tconst queue: any[] = [];\n\t\t\tlet overflowed = false;\n\t\t\tlet resolve: ((value: void | PromiseLike<void>) => void) | null = null;\n\t\t\tconst enqueue = (message: unknown) => {\n\t\t\t\tif (queue.length >= server.maxSubscriberBuffer) {\n\t\t\t\t\toverflowed = true;\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push(message);\n\t\t\t\t}\n\t\t\t\tif (resolve) {\n\t\t\t\t\tresolve();\n\t\t\t\t\tresolve = null;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst unsubscribe = server.subscribeToStateEvents(\n\t\t\t\t(event: ZunoStateEvent) => {\n\t\t\t\t\tenqueue(\n\t\t\t\t\t\tsse({\n\t\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t);\n\n\t\t\t// 2. Decide if we send snapshot or missed events\n\t\t\tconst rawLastEventId = headers[\"last-event-id\"] || query?.lastEventId;\n\t\t\tconst lastEventId = Number.parseInt(rawLastEventId || \"0\", 10) || 0;\n\n\t\t\tif (lastEventId > 0 && server.canReplayAfter(lastEventId)) {\n\t\t\t\tconst missed = server.getEventsAfter(lastEventId);\n\t\t\t\tfor (const event of missed) {\n\t\t\t\t\tyield sse({\n\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst snapshot = server.getUniverseState();\n\t\t\t\tyield sse({\n\t\t\t\t\tid: String(server.getLastEventId()),\n\t\t\t\t\tevent: \"snapshot\",\n\t\t\t\t\tdata: JSON.stringify(snapshot),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// 3. Heartbeat interval\n\t\t\tconst heartbeat = setInterval(() => {\n\t\t\t\tenqueue(sse({ data: `: ping ${Date.now()}` }));\n\t\t\t}, 15000);\n\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (overflowed) return;\n\t\t\t\t\tif (queue.length === 0) {\n\t\t\t\t\t\tawait new Promise<void>((r) => {\n\t\t\t\t\t\t\tresolve = r;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (overflowed) return;\n\n\t\t\t\t\twhile (queue.length > 0) {\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: queue is checked for length > 0\n\t\t\t\t\t\tyield queue.shift()!;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tclearInterval(heartbeat);\n\t\t\t\tunsubscribe();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handles sync POST requests for Elysia.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.body - Elysia request body.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - ok: A boolean indicating whether the sync was successful.\n\t\t * - event: The event that was applied to the universe.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request body\n\t\tsync: async (request: any) => {\n\t\t\tconst { body, set } = request;\n\t\t\tconst incoming = body as ZunoStateEvent;\n\t\t\tif (!(await authorize({ action: \"write\", request, event: incoming }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\tconst result = applyStateEvent(incoming, server);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tset.status = result.reason === \"VERSION_CONFLICT\" ? 409 : 400;\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\treason: result.reason,\n\t\t\t\t\t...(result.reason === \"VERSION_CONFLICT\"\n\t\t\t\t\t\t? { current: result.current }\n\t\t\t\t\t\t: { errors: result.errors }),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { ok: true, event: result.event };\n\t\t},\n\n\t\t/**\n\t\t * Handles snapshot GET requests for Elysia.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - state: The current state of the universe.\n\t\t * - version: The version of the universe.\n\t\t * - lastEventId: The ID of the last event in the universe.\n\t\t */\n\t\tsnapshot: async (request: { set: { status?: number } }) => {\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\trequest.set.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstate: server.getUniverseState(),\n\t\t\t\tlastEventId: server.getLastEventId(),\n\t\t\t};\n\t\t},\n\t\tserver,\n\t};\n}\n"]}
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@iadev93/zuno-elysia",
3
- "version": "0.0.8",
4
- "main": "./dist/index.js",
3
+ "version": "0.0.10",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
5
7
  "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
6
12
  "files": [
7
13
  "dist",
8
14
  "README.md",
@@ -11,19 +17,19 @@
11
17
  "exports": {
12
18
  ".": {
13
19
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.mjs",
15
- "require": "./dist/index.js"
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
16
22
  }
17
23
  },
18
24
  "peerDependencies": {
19
25
  "elysia": ">=1.0.0",
20
- "@iadev93/zuno": "0.0.11"
26
+ "@iadev93/zuno": "0.0.15"
21
27
  },
22
28
  "devDependencies": {
23
- "elysia": "^1.2.10",
29
+ "elysia": "^1.4.30",
24
30
  "tsup": "^8.5.1",
25
31
  "typescript": "^5.9.3",
26
- "@iadev93/zuno": "0.0.11"
32
+ "@iadev93/zuno": "0.0.15"
27
33
  },
28
34
  "author": "Ibrahim Aftab",
29
35
  "license": "MIT",
package/dist/index.mjs DELETED
@@ -1,2 +0,0 @@
1
- import {createZunoServerState,applyStateEvent}from'@iadev93/zuno/server';import {sse}from'elysia';function m(v={}){let a=v.server??createZunoServerState(),l=v.authorize??(()=>true);return {sse:async function*(t){let{set:r,headers:i,query:u}=t;if(!await l({action:"read",request:t})){r.status=403;return}r.headers["Content-Type"]="text/event-stream",r.headers["Cache-Control"]="no-cache",r.headers.Connection="keep-alive",yield sse({data:": connected"});let e=[],s=null,y=a.subscribeToStateEvents(n=>{e.push(sse({id:String(n.eventId),event:"state",data:JSON.stringify(n)})),s&&(s(),s=null);}),f=i["last-event-id"]||u?.lastEventId,c=Number.parseInt(f||"0",10)||0;if(c>0){let n=a.getEventsAfter(c);for(let d of n)yield sse({id:String(d.eventId),event:"state",data:JSON.stringify(d)});}else {let n=a.getUniverseState();yield sse({event:"snapshot",data:JSON.stringify(n)});}let h=setInterval(()=>{e.push(sse({data:`: ping ${Date.now()}`})),s&&(s(),s=null);},15e3);try{for(;;)for(e.length===0&&await new Promise(n=>{s=n;});e.length>0;)yield e.shift();}finally{clearInterval(h),y();}},sync:async t=>{let{body:r,set:i}=t,u=r;if(!await l({action:"write",request:t,event:u}))return i.status=403,{ok:false,reason:"FORBIDDEN"};let e=applyStateEvent(u,a);return e.ok?{ok:true,event:e.event}:(i.status=e.reason==="VERSION_CONFLICT"?409:400,{ok:false,reason:e.reason,...e.reason==="VERSION_CONFLICT"?{current:e.current}:{errors:e.errors}})},snapshot:async t=>await l({action:"read",request:t})?{state:a.getUniverseState(),lastEventId:a.getLastEventId()}:(t.set.status=403,{ok:false,reason:"FORBIDDEN"}),server:a}}export{m as createZunoElysia};//# sourceMappingURL=index.mjs.map
2
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["createZunoElysia","options","server","createZunoServerState","authorize","request","set","headers","query","sse","queue","resolve","unsubscribe","event","rawLastEventId","lastEventId","missed","snapshot","heartbeat","r","body","incoming","result","applyStateEvent"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { ZunoStateEvent } from \"@iadev93/zuno\";\nimport {\n\tapplyStateEvent,\n\tcreateZunoServerState,\n\ttype ZunoServerState,\n} from \"@iadev93/zuno/server\";\nimport { sse } from \"elysia\";\n\n/**\n * Creates a Zuno Elysia instance.\n * @returns {Object} An object with the following properties:\n * - sse: An async generator function that handles SSE connections for Elysia.\n * @property {Object} set - Elysia response object.\n * @property {Object} headers - Elysia request headers.\n * @property {Object} query - Elysia request query parameters.\n * - sync: A function that handles sync POST requests for Elysia.\n * @property {Object} body - Elysia request body.\n * @property {Object} set - Elysia response object.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n * - snapshot: A function that handles snapshot GET requests for Elysia.\n */\nexport type CreateZunoElysiaOptions = {\n\t/** Isolated authoritative server state. A new instance is created by default. */\n\tserver?: ZunoServerState;\n\t/** Provider-agnostic authorization hook for snapshots, SSE, and mutations. */\n\tauthorize?: (context: {\n\t\taction: \"read\" | \"write\";\n\t\trequest: unknown;\n\t\tevent?: unknown;\n\t}) => boolean | Promise<boolean>;\n};\n\nexport function createZunoElysia(options: CreateZunoElysiaOptions = {}) {\n\tconst server = options.server ?? createZunoServerState();\n\tconst authorize = options.authorize ?? (() => true);\n\treturn {\n\t\t/**\n\t\t * Handles SSE connections for Elysia using an async generator.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @param {Object} param.headers - Elysia request headers.\n\t\t * @param {Object} param.query - Elysia request query parameters.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request object\n\t\tsse: async function* (request: any) {\n\t\t\tconst { set, headers, query } = request;\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tset.headers[\"Content-Type\"] = \"text/event-stream\";\n\t\t\tset.headers[\"Cache-Control\"] = \"no-cache\";\n\t\t\tset.headers.Connection = \"keep-alive\";\n\n\t\t\tyield sse({ data: \": connected\" });\n\n\t\t\t// 1. Subscribe FIRST to avoid missing events during snapshot/missed-events retrieval\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: queue of any SSE events\n\t\t\tconst queue: any[] = [];\n\t\t\tlet resolve: ((value: void | PromiseLike<void>) => void) | null = null;\n\n\t\t\tconst unsubscribe = server.subscribeToStateEvents(\n\t\t\t\t(event: ZunoStateEvent) => {\n\t\t\t\t\tqueue.push(\n\t\t\t\t\t\tsse({\n\t\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tif (resolve) {\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t\tresolve = null;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\n\t\t\t// 2. Decide if we send snapshot or missed events\n\t\t\tconst rawLastEventId = headers[\"last-event-id\"] || query?.lastEventId;\n\t\t\tconst lastEventId = Number.parseInt(rawLastEventId || \"0\", 10) || 0;\n\n\t\t\tif (lastEventId > 0) {\n\t\t\t\tconst missed = server.getEventsAfter(lastEventId);\n\t\t\t\tfor (const event of missed) {\n\t\t\t\t\tyield sse({\n\t\t\t\t\t\tid: String(event.eventId),\n\t\t\t\t\t\tevent: \"state\",\n\t\t\t\t\t\tdata: JSON.stringify(event),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst snapshot = server.getUniverseState();\n\t\t\t\tyield sse({\n\t\t\t\t\tevent: \"snapshot\",\n\t\t\t\t\tdata: JSON.stringify(snapshot),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// 3. Heartbeat interval\n\t\t\tconst heartbeat = setInterval(() => {\n\t\t\t\tqueue.push(sse({ data: `: ping ${Date.now()}` }));\n\t\t\t\tif (resolve) {\n\t\t\t\t\tresolve();\n\t\t\t\t\tresolve = null;\n\t\t\t\t}\n\t\t\t}, 15000);\n\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (queue.length === 0) {\n\t\t\t\t\t\tawait new Promise<void>((r) => {\n\t\t\t\t\t\t\tresolve = r;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\twhile (queue.length > 0) {\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: queue is checked for length > 0\n\t\t\t\t\t\tyield queue.shift()!;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tclearInterval(heartbeat);\n\t\t\t\tunsubscribe();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handles sync POST requests for Elysia.\n\t\t * @param {Object} param - Elysia request object.\n\t\t * @param {Object} param.body - Elysia request body.\n\t\t * @param {Object} param.set - Elysia response object.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - ok: A boolean indicating whether the sync was successful.\n\t\t * - event: The event that was applied to the universe.\n\t\t */\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Elysia request body\n\t\tsync: async (request: any) => {\n\t\t\tconst { body, set } = request;\n\t\t\tconst incoming = body as ZunoStateEvent;\n\t\t\tif (!(await authorize({ action: \"write\", request, event: incoming }))) {\n\t\t\t\tset.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\tconst result = applyStateEvent(incoming, server);\n\n\t\t\tif (!result.ok) {\n\t\t\t\tset.status = result.reason === \"VERSION_CONFLICT\" ? 409 : 400;\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\treason: result.reason,\n\t\t\t\t\t...(result.reason === \"VERSION_CONFLICT\"\n\t\t\t\t\t\t? { current: result.current }\n\t\t\t\t\t\t: { errors: result.errors }),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { ok: true, event: result.event };\n\t\t},\n\n\t\t/**\n\t\t * Handles snapshot GET requests for Elysia.\n\t\t * @returns {Object} An object with the following properties:\n\t\t * - state: The current state of the universe.\n\t\t * - version: The version of the universe.\n\t\t * - lastEventId: The ID of the last event in the universe.\n\t\t */\n\t\tsnapshot: async (request: { set: { status?: number } }) => {\n\t\t\tif (!(await authorize({ action: \"read\", request }))) {\n\t\t\t\trequest.set.status = 403;\n\t\t\t\treturn { ok: false, reason: \"FORBIDDEN\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstate: server.getUniverseState(),\n\t\t\t\tlastEventId: server.getLastEventId(),\n\t\t\t};\n\t\t},\n\t\tserver,\n\t};\n}\n"]}
File without changes