shared-reducer 5.0.3 → 5.1.0
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/backend/index.d.ts +6 -1
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/package.json +5 -5
package/backend/index.d.ts
CHANGED
|
@@ -103,6 +103,11 @@ interface WebsocketHandlerOptions {
|
|
|
103
103
|
pingInterval?: number;
|
|
104
104
|
pongTimeout?: number;
|
|
105
105
|
}
|
|
106
|
+
interface HandlerCallbacks<Req> {
|
|
107
|
+
onConnect?: (req: Req) => void;
|
|
108
|
+
onDisconnect?: (req: Req, reason: string, connectionDuration: number) => void;
|
|
109
|
+
onError?: (req: Req, context: string, error: unknown) => void;
|
|
110
|
+
}
|
|
106
111
|
declare class WebsocketHandlerFactory<T, SpecT> {
|
|
107
112
|
private readonly broadcaster;
|
|
108
113
|
private readonly closers;
|
|
@@ -112,7 +117,7 @@ declare class WebsocketHandlerFactory<T, SpecT> {
|
|
|
112
117
|
constructor(broadcaster: Broadcaster<T, SpecT>, options?: WebsocketHandlerOptions);
|
|
113
118
|
activeConnections(): number;
|
|
114
119
|
softClose(timeout: number): Promise<void>;
|
|
115
|
-
handler<Req, Res extends WSResponse>(idGetter: (req: Req, res: Res) => MaybePromise<string>, permissionGetter: (req: Req, res: Res) => MaybePromise<Permission<T, SpecT
|
|
120
|
+
handler<Req, Res extends WSResponse>(idGetter: (req: Req, res: Res) => MaybePromise<string>, permissionGetter: (req: Req, res: Res) => MaybePromise<Permission<T, SpecT>>, { onConnect, onDisconnect, onError }?: HandlerCallbacks<Req>): (req: Req, res: Res) => Promise<void>;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
declare const UniqueIdProvider: () => () => string;
|
package/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;
|
|
1
|
+
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;u=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.u.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.u.set(t,e),s=e}return s.push(e)}}class i{l;p=new Map;constructor(t){this.l=t}async add(t,e){let s=this.p.get(t);s||(s=this.l(t),this.p.set(t,s)),await s.add(e)}async remove(t,e){const s=this.p.get(t);if(s){await s.remove(e)||this.p.delete(t)}}async broadcast(t,e){const s=this.p.get(t);s&&await s.broadcast(e)}}class n{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach((e=>e(t)))}}const o={validateWrite(){}};const a=(t,e,s)=>console.warn(`shared-reducer: ${e}`,s),c=t=>t;class h extends Error{}const u="Cannot modify data",l={validateWriteSpec(){throw new h(u)},validateWrite(){throw new h(u)}};exports.AsyncTaskQueue=s,exports.Broadcaster=class{v;_;m;C;T;constructor(t,s,o={}){this.v=t,this._=s,this.m=o.subscribers??new i((()=>new n)),this.C=o.taskQueues??new r,this.T=o.idProvider??e()}async subscribe(t,e=o){let s={S:0},r="";const i=t=>{2===s.S?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.S&&s.t.push(t)};try{if(await this.C.push(t,(async()=>{const e=await this.v.read(t);null!=e&&(s={S:1,P:e,t:[]},await this.m.add(t,i))})),0===s.S)return null;r=await this.T()}catch(e){throw await this.m.remove(t,i),e}return{getInitialData(){if(1!==s.S)throw new Error("Already started");return s.P},listen(t){if(1!==s.S)throw new Error("Already started");const e=s.t;s={S:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.m.remove(t,i)}}}update(t,e,s=o){return this.j(t,e,s,null,void 0)}async D(t,e,s,r,i){try{const r=await this.v.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this._.update(r,e),n=this.v.validate(i);s.validateWrite(n,r),await this.v.write(t,n,r)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.m.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.C.push(t,(()=>this.D(t,e,s,r,i)))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{I;$;validate;k;q;constructor(t,e,s,r={}){this.I=t,this.$=e,this.validate=s,this.k=r.readErrorIntercept??c,this.q=r.writeErrorIntercept??c}async read(t){try{return await this.I.get(this.$,t)}catch(t){throw this.k(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.I.update(this.$,t,r)}catch(t){throw this.q(t)}}},exports.InMemoryModel=class{read=this.get;validate;A=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.A.set(t,e)}get(t){return this.A.get(t)}delete(t){this.A.delete(t)}write(t,e,s){if(s!==this.A.get(t))throw new Error("Unexpected previous value");this.A.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=h,exports.ReadOnly=l,exports.ReadWrite=o,exports.ReadWriteStruct=class{J;constructor(t=[]){this.J=t}validateWrite(t,e){for(const s of this.J){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new h(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new h(`Cannot edit field ${String(s)}`)}}},exports.TaskQueueMap=r,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;closers=new Set;M;N;closing=!1;constructor(t,e={}){this.broadcaster=t,this.M=e.pingInterval??25e3,this.N=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e,{onConnect:s,onDisconnect:r,onError:i=a}={}){const n=async(s,r)=>{const i=await t(s,r),n=await e(s,r),o=await this.broadcaster.subscribe(i,n);return o||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const o=(e,s)=>{try{i(t,e,s)}catch{}},a=await n(t,e).catch((t=>(o("handshake",t),e.sendError(500),null)));if(!a)return;const c=Date.now(),h=e=>{const s=Date.now()-c;try{r?.(t,e,s)}catch(t){o("disconnect hook",t)}a.close().catch((()=>null))};try{const r=await e.accept();s?.(t);let i=0,n=()=>null;const o=()=>(this.closers.delete(o),r.send("X"),i=1,new Promise((t=>{n=t})));r.on("close",(()=>{i=2,clearTimeout(l),h("disconnect"),this.closers.delete(o),n()}));const c=()=>{h("lost"),this.closers.delete(o),r.terminate(),n()},u=()=>{r.ping(),clearTimeout(l),l=setTimeout(c,this.N)};if(r.on("pong",(()=>{clearTimeout(l),l=setTimeout(u,this.M)})),r.on("message",(async(t,s)=>{clearTimeout(l),l=setTimeout(u,this.M);try{if(s)throw new Error("Binary messages are not supported");const o=String(t);if("P"===o)return void r.send("p");if("x"===o){if(1!==i)throw new Error("Unexpected close ack message");return i=2,void n()}if(2===i)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(o);e.beginTransaction();try{await a.send(c.change,c.id)}finally{e.endTransaction()}}catch(t){r.send(JSON.stringify({error:t instanceof Error?t.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void h("server shutdown");r.send(JSON.stringify({init:a.getInitialData()})),a.listen(((t,e)=>{const s=void 0!==e?{id:e,...t}:t;r.send(JSON.stringify(s))}));let l=setTimeout(u,this.M);this.closers.add(o)}catch(t){o("communication",t),e.sendError(500),h("error")}}}};
|
package/backend/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;
|
|
1
|
+
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;u=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.u.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.u.set(t,e),s=e}return s.push(e)}}class i{l;m=new Map;constructor(t){this.l=t}async add(t,e){let s=this.m.get(t);s||(s=this.l(t),this.m.set(t,s)),await s.add(e)}async remove(t,e){const s=this.m.get(t);if(s){await s.remove(e)||this.m.delete(t)}}async broadcast(t,e){const s=this.m.get(t);s&&await s.broadcast(e)}}class n{p=new Set;add(t){this.p.add(t)}remove(t){return this.p.delete(t),this.p.size>0}broadcast(t){this.p.forEach((e=>e(t)))}}const a={validateWrite(){}};class o{v;_;p;C;T;constructor(t,s,a={}){this.v=t,this._=s,this.p=a.subscribers??new i((()=>new n)),this.C=a.taskQueues??new r,this.T=a.idProvider??e()}async subscribe(t,e=a){let s={S:0},r="";const i=t=>{2===s.S?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.S&&s.t.push(t)};try{if(await this.C.push(t,(async()=>{const e=await this.v.read(t);null!=e&&(s={S:1,P:e,t:[]},await this.p.add(t,i))})),0===s.S)return null;r=await this.T()}catch(e){throw await this.p.remove(t,i),e}return{getInitialData(){if(1!==s.S)throw new Error("Already started");return s.P},listen(t){if(1!==s.S)throw new Error("Already started");const e=s.t;s={S:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.p.remove(t,i)}}}update(t,e,s=a){return this.j(t,e,s,null,void 0)}async D(t,e,s,r,i){try{const r=await this.v.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this._.update(r,e),n=this.v.validate(i);s.validateWrite(n,r),await this.v.write(t,n,r)}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.p.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.C.push(t,(()=>this.D(t,e,s,r,i)))}}const c="P",h="p",u="X",l="x";class d{broadcaster;closers=new Set;I;$;closing=!1;constructor(t,e={}){this.broadcaster=t,this.I=e.pingInterval??25e3,this.$=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e,{onConnect:s,onDisconnect:r,onError:i=w}={}){const n=async(s,r)=>{const i=await t(s,r),n=await e(s,r),a=await this.broadcaster.subscribe(i,n);return a||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const a=(e,s)=>{try{i(t,e,s)}catch{}},o=await n(t,e).catch((t=>(a("handshake",t),e.sendError(500),null)));if(!o)return;const c=Date.now(),h=e=>{const s=Date.now()-c;try{r?.(t,e,s)}catch(t){a("disconnect hook",t)}o.close().catch((()=>null))};try{const r=await e.accept();s?.(t);let i=0,n=()=>null;const a=()=>(this.closers.delete(a),r.send("X"),i=1,new Promise((t=>{n=t})));r.on("close",(()=>{i=2,clearTimeout(l),h("disconnect"),this.closers.delete(a),n()}));const c=()=>{h("lost"),this.closers.delete(a),r.terminate(),n()},u=()=>{r.ping(),clearTimeout(l),l=setTimeout(c,this.$)};if(r.on("pong",(()=>{clearTimeout(l),l=setTimeout(u,this.I)})),r.on("message",(async(t,s)=>{clearTimeout(l),l=setTimeout(u,this.I);try{if(s)throw new Error("Binary messages are not supported");const a=String(t);if("P"===a)return void r.send("p");if("x"===a){if(1!==i)throw new Error("Unexpected close ack message");return i=2,void n()}if(2===i)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(a);e.beginTransaction();try{await o.send(c.change,c.id)}finally{e.endTransaction()}}catch(t){r.send(JSON.stringify({error:t instanceof Error?t.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void h("server shutdown");r.send(JSON.stringify({init:o.getInitialData()})),o.listen(((t,e)=>{const s=void 0!==e?{id:e,...t}:t;r.send(JSON.stringify(s))}));let l=setTimeout(u,this.I);this.closers.add(a)}catch(t){a("communication",t),e.sendError(500),h("error")}}}}const w=(t,e,s)=>console.warn(`shared-reducer: ${e}`,s),m=t=>t;class y{k;q;validate;A;J;constructor(t,e,s,r={}){this.k=t,this.q=e,this.validate=s,this.A=r.readErrorIntercept??m,this.J=r.writeErrorIntercept??m}async read(t){try{return await this.k.get(this.q,t)}catch(t){throw this.A(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.k.update(this.q,t,r)}catch(t){throw this.J(t)}}}class f extends Error{}class p{read=this.get;validate;M=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.M.set(t,e)}get(t){return this.M.get(t)}delete(t){this.M.delete(t)}write(t,e,s){if(s!==this.M.get(t))throw new Error("Unexpected previous value");this.M.set(t,e)}}const g="Cannot modify data",v={validateWriteSpec(){throw new f(g)},validateWrite(){throw new f(g)}};class _{N;constructor(t=[]){this.N=t}validateWrite(t,e){for(const s of this.N){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new f(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new f(`Cannot edit field ${String(s)}`)}}}export{s as AsyncTaskQueue,o as Broadcaster,u as CLOSE,l as CLOSE_ACK,y as CollectionStorageModel,p as InMemoryModel,n as InMemoryTopic,c as PING,h as PONG,f as PermissionError,v as ReadOnly,a as ReadWrite,_ as ReadWriteStruct,r as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,d as WebsocketHandlerFactory};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shared-reducer",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "shared state management",
|
|
5
5
|
"author": "David Evans",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,16 +45,16 @@
|
|
|
45
45
|
"homepage": "https://github.com/davidje13/shared-reducer#readme",
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rollup/plugin-terser": "0.4.x",
|
|
48
|
-
"@rollup/plugin-typescript": "
|
|
48
|
+
"@rollup/plugin-typescript": "12.x",
|
|
49
49
|
"collection-storage": "3.x",
|
|
50
50
|
"json-immutability-helper": "4.x",
|
|
51
51
|
"lean-test": "2.x",
|
|
52
|
-
"prettier": "3.
|
|
52
|
+
"prettier": "3.4.2",
|
|
53
53
|
"rollup": "4.x",
|
|
54
54
|
"rollup-plugin-dts": "6.x",
|
|
55
55
|
"superwstest": "2.x",
|
|
56
|
-
"tslib": "2.
|
|
57
|
-
"typescript": "5.
|
|
56
|
+
"tslib": "2.8.x",
|
|
57
|
+
"typescript": "5.7.x",
|
|
58
58
|
"websocket-express": "3.x"
|
|
59
59
|
}
|
|
60
60
|
}
|