shared-reducer 6.3.0 → 6.3.2

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.
@@ -58,9 +58,10 @@ type Identifier = string | null;
58
58
  type ChangeInfo<SpecT> = {
59
59
  change: SpecT;
60
60
  events: Readonly<Readonly<ChangeEvent>[]> | undefined;
61
- error?: undefined;
61
+ error?: never;
62
62
  } | {
63
- change?: undefined;
63
+ change?: never;
64
+ events?: never;
64
65
  error: string;
65
66
  };
66
67
  interface TopicMessage<SpecT> {
@@ -68,6 +69,7 @@ interface TopicMessage<SpecT> {
68
69
  source: Identifier;
69
70
  meta?: unknown;
70
71
  }
72
+ type EventFilter = (evt: Readonly<ChangeEvent>) => boolean;
71
73
  type ID = string;
72
74
  declare class Broadcaster<T, SpecT> {
73
75
  private readonly _model;
@@ -80,7 +82,7 @@ declare class Broadcaster<T, SpecT> {
80
82
  taskQueues?: TaskQueueMap<ID>;
81
83
  idProvider?: () => MaybePromise<string>;
82
84
  });
83
- subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>): Promise<Subscription<T, SpecT, MetaT> | null>;
85
+ subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>, eventFilter?: EventFilter): Promise<Subscription<T, SpecT, MetaT> | null>;
84
86
  update(id: ID, change: SpecT, { events, permission, }?: {
85
87
  events?: ChangeEvent[] | undefined;
86
88
  permission?: Permission<T, SpecT>;
@@ -105,6 +107,7 @@ interface ServerWebSocket {
105
107
  interface Access<T, SpecT> {
106
108
  id: string;
107
109
  permission: Permission<T, SpecT>;
110
+ eventFilter?: EventFilter;
108
111
  }
109
112
  type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
110
113
  interface WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> {
@@ -197,4 +200,4 @@ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
197
200
  }
198
201
 
199
202
  export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, CollectionStorageModel, InMemoryModel, InMemoryTopic, PING, PONG, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, TaskQueueMap, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
200
- export type { ChangeInfo, Context, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
203
+ export type { ChangeEvent, ChangeInfo, Context, EventFilter, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
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 r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(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 s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class n{u;p=new Map;constructor(t){this.u=t}async add(t,e){let r=this.p.get(t);r||(r=this.u(t),this.p.set(t,r)),await r.add(e)}async remove(t,e){const r=this.p.get(t);if(r){await r.remove(e)||this.p.delete(t)}}async broadcast(t,e){const r=this.p.get(t);r&&await r.broadcast(e)}}class i{v=new Set;add(t){this.v.add(t)}remove(t){return this.v.delete(t),this.v.size>0}broadcast(t){this.v.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o extends Error{}class c extends Error{}const h=0,l=1,u=2,d=3,w=new Error("not found"),f=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),p=t=>t;const y="Cannot modify data",v={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)},validateEvent(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{m;_;v;S;O;constructor(t,r,a={}){this.m=t,this._=r,this.v=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.I(t.message,t.meta):t.message.change&&r.I(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.m.read(t);null!=e&&(r={C:1,J:e,t:[]},await this.v.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.v.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.J},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,I:t},e.forEach(n)},send:(r,n,i)=>this.N(t,r,n,e,s,i),close:async()=>{await this.v.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,n,i){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.m.read(t);if(!r)throw new Error("Deleted");const n=this._.update(r,e),i=this.m.validate(n);s.validateWrite(i,r),await this.m.write(t,i,r)}}catch(e){return void this.v.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:n,meta:i})}0===r?.length&&(r=void 0),this.v.broadcast(t,{message:{change:e,events:r},source:n,meta:i})}async N(t,e,r,s,n,i){return this.S.push(t,()=>this.T(t,e,r,s,n,i))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??p,this.$=s.writeErrorIntercept??p}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}},exports.InMemoryModel=class{read=this.get;validate;q=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.q.set(t,e)}get(t){return this.q.get(t)}delete(t){this.q.delete(t)}write(t,e,r){if(r!==this.q.get(t))throw new Error("Unexpected previous value");this.q.set(t,e)}},exports.InMemoryTopic=i,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=v,exports.ReadWrite=a,exports.ReadWriteStruct=class{P;constructor(t=[]){this.P=t}validateWrite(t,e){for(const r of this.P){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}validateEvent(){}},exports.TaskQueueMap=s,exports.TrackingTopicMap=n,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=w,setSoftCloseHandler:i,onConnect:a,onDisconnect:p,onError:y=f}){return async(...w)=>{const f=[];let v;try{const{id:m,permission:g}=await t(...w),_=await this.broadcaster.subscribe(m,g);if(!_)throw n;f.push(()=>_.close());let x,b=h,E="connection failed";const S=new Promise(t=>{x=e=>{x=()=>{},E=e,t()}}),O=await e(...w);w.length=1,i?.(w[0],()=>(b===h&&(b=l,O.send("X"),v||(v=setTimeout(I,s))),S));const C=Date.now();a?.(w[0],m,g),f.push(()=>p?.(w[0],E,Date.now()-C)),O.on("close",()=>{clearTimeout(v),b=u,x("client disconnect")});const I=()=>{O.terminate(),b=d,x("connection lost")},J=()=>{O.ping(),clearTimeout(v),v=setTimeout(I,s)},N=()=>{clearTimeout(v),v=setTimeout(J,r)};O.on("pong",N),O.on("message",async(t,e)=>{if(N(),e)return O.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return O.send("p");if("x"===r)return b!==l&&b!==d?O.send(JSON.stringify({error:"Unexpected close ack message"})):(b=u,x("clean shutdown"),O.close());if(b===u)return O.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new c("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");r.id=e.id}return r}(r);await _.send(t.change,t.events,t.id)}catch(t){t instanceof o||t instanceof c?O.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),O.send(JSON.stringify({error:"Internal error"})))}}),b===h&&(O.send(JSON.stringify({init:_.getInitialData()})),_.listen((t,e)=>O.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),N()),await S}finally{clearTimeout(v);for(const t of f.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
1
+ "use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(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 s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class i{u;p=new Map;constructor(t){this.u=t}async add(t,e){let r=this.p.get(t);r||(r=this.u(t),this.p.set(t,r)),await r.add(e)}async remove(t,e){const r=this.p.get(t);if(r){await r.remove(e)||this.p.delete(t)}}async broadcast(t,e){const r=this.p.get(t);r&&await r.broadcast(e)}}class n{v=new Set;add(t){this.v.add(t)}remove(t){return this.v.delete(t),this.v.size>0}broadcast(t){this.v.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o extends Error{}class c extends Error{}const h=0,l=1,u=2,d=3,w=new Error("not found"),f=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),p=t=>t;const y="Cannot modify data",v={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)},validateEvent(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{m;_;v;S;O;constructor(t,r,a={}){this.m=t,this._=r,this.v=a.subscribers??new i(()=>new n),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a,r){let s={C:0},i="";const n=t=>{if(2===s.C){let e=t.message;if(r&&e.events?.length){const t=e.events.filter(r);e={...e,events:t.length?t:void 0}}if(t.source===i)s.I(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.I(e,void 0)}}else 1===s.C&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.m.read(t);null!=e&&(s={C:1,J:e,t:[]},await this.v.add(t,n))}),0===s.C)return null;i=await this.O()}catch(e){throw await this.v.remove(t,n),e}return{getInitialData(){if(1!==s.C)throw new Error("Already started");return s.J},listen(t){if(1!==s.C)throw new Error("Already started");const e=s.t;s={C:2,I:t},e.forEach(n)},send:(r,s,n)=>this.N(t,r,s,e,i,n),close:async()=>{await this.v.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,i,n){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.m.read(t);if(!r)throw new Error("Deleted");const i=this._.update(r,e),n=this.m.validate(i);s.validateWrite(n,r),await this.m.write(t,n,r)}}catch(e){return void this.v.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:i,meta:n})}0===r?.length&&(r=void 0),this.v.broadcast(t,{message:{change:e,events:r},source:i,meta:n})}async N(t,e,r,s,i,n){return this.S.push(t,()=>this.T(t,e,r,s,i,n))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??p,this.$=s.writeErrorIntercept??p}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}},exports.InMemoryModel=class{read=this.get;validate;q=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.q.set(t,e)}get(t){return this.q.get(t)}delete(t){this.q.delete(t)}write(t,e,r){if(r!==this.q.get(t))throw new Error("Unexpected previous value");this.q.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=v,exports.ReadWrite=a,exports.ReadWriteStruct=class{F;constructor(t=[]){this.F=t}validateWrite(t,e){for(const r of this.F){const s=Object.prototype.hasOwnProperty.call(e,r),i=Object.prototype.hasOwnProperty.call(t,r);if(s!==i)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(i&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}validateEvent(){}},exports.TaskQueueMap=s,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:i=w,setSoftCloseHandler:n,onConnect:a,onDisconnect:p,onError:y=f}){return async(...w)=>{const f=[];let v;try{const{id:m,permission:g,eventFilter:_}=await t(...w),x=await this.broadcaster.subscribe(m,g,_);if(!x)throw i;f.push(()=>x.close());let b,E=h,S="connection failed";const O=new Promise(t=>{b=e=>{b=()=>{},S=e,t()}}),C=await e(...w);w.length=1,n?.(w[0],()=>(E===h&&(E=l,C.send("X"),v||(v=setTimeout(J,s))),O));const I=Date.now();a?.(w[0],m,g),f.push(()=>p?.(w[0],S,Date.now()-I)),C.on("close",()=>{clearTimeout(v),E=u,b("client disconnect")});const J=()=>{C.terminate(),E=d,b("connection lost")},N=()=>{C.ping(),clearTimeout(v),v=setTimeout(J,s)},T=()=>{clearTimeout(v),v=setTimeout(N,r)};C.on("pong",T),C.on("message",async(t,e)=>{if(T(),e)return C.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return C.send("p");if("x"===r)return E!==l&&E!==d?C.send(JSON.stringify({error:"Unexpected close ack message"})):(E=u,b("clean shutdown"),C.close());if(E===u)return C.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new c("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");r.id=e.id}return r}(r);await x.send(t.change,t.events,t.id)}catch(t){t instanceof o||t instanceof c?C.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),E===h&&(C.send(JSON.stringify({init:x.getInitialData()})),x.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await O}finally{clearTimeout(v);for(const t of f.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
package/backend/index.mjs CHANGED
@@ -1 +1 @@
1
- import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(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 s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class n{u;m=new Map;constructor(t){this.u=t}async add(t,e){let r=this.m.get(t);r||(r=this.u(t),this.m.set(t,r)),await r.add(e)}async remove(t,e){const r=this.m.get(t);if(r){await r.remove(e)||this.m.delete(t)}}async broadcast(t,e){const r=this.m.get(t);r&&await r.broadcast(e)}}class i{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(){},validateEvent(){}};class o{v;_;p;S;O;constructor(t,r,a={}){this.v=t,this._=r,this.p=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.I(t.message,t.meta):t.message.change&&r.I(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.v.read(t);null!=e&&(r={C:1,J:e,t:[]},await this.p.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.p.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.J},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,I:t},e.forEach(n)},send:(r,n,i)=>this.N(t,r,n,e,s,i),close:async()=>{await this.p.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,n,i){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.v.read(t);if(!r)throw new Error("Deleted");const n=this._.update(r,e),i=this.v.validate(n);s.validateWrite(i,r),await this.v.write(t,i,r)}}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:n,meta:i})}0===r?.length&&(r=void 0),this.p.broadcast(t,{message:{change:e,events:r},source:n,meta:i})}async N(t,e,r,s,n,i){return this.S.push(t,()=>this.T(t,e,r,s,n,i))}}class c extends Error{}class h extends Error{}const l="P",u="p",d="X",w="x";class f{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=g,setSoftCloseHandler:i,onConnect:a,onDisconnect:o,onError:l=_}){return async(...u)=>{const d=[];let w;try{const{id:f,permission:g}=await t(...u),_=await this.broadcaster.subscribe(f,g);if(!_)throw n;d.push(()=>_.close());let b,E=y,S="connection failed";const O=new Promise(t=>{b=e=>{b=()=>{},S=e,t()}}),C=await e(...u);u.length=1,i?.(u[0],()=>(E===y&&(E=m,C.send("X"),w||(w=setTimeout(I,s))),O));const x=Date.now();a?.(u[0],f,g),d.push(()=>o?.(u[0],S,Date.now()-x)),C.on("close",()=>{clearTimeout(w),E=p,b("client disconnect")});const I=()=>{C.terminate(),E=v,b("connection lost")},J=()=>{C.ping(),clearTimeout(w),w=setTimeout(I,s)},N=()=>{clearTimeout(w),w=setTimeout(J,r)};C.on("pong",N),C.on("message",async(t,e)=>{if(N(),e)return C.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return C.send("p");if("x"===r)return E!==m&&E!==v?C.send(JSON.stringify({error:"Unexpected close ack message"})):(E=p,b("clean shutdown"),C.close());if(E===p)return C.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new h("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new h("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new h("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");r.id=e.id}return r}(r);await _.send(t.change,t.events,t.id)}catch(t){t instanceof c||t instanceof h?C.send(JSON.stringify({error:t.message})):(l(u[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),E===y&&(C.send(JSON.stringify({init:_.getInitialData()})),_.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),N()),await O}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){l(u[0],t,"teardown")}}}}}const y=0,m=1,p=2,v=3,g=new Error("not found"),_=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class E{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??b,this.$=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}}class S{read=this.get;validate;P=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.P.set(t,e)}get(t){return this.P.get(t)}delete(t){this.P.delete(t)}write(t,e,r){if(r!==this.P.get(t))throw new Error("Unexpected previous value");this.P.set(t,e)}}const O="Cannot modify data",C={validateWriteSpec(){throw new c(O)},validateWrite(){throw new c(O)},validateEvent(){throw new c(O)}};class x{W;constructor(t=[]){this.W=t}validateWrite(t,e){for(const r of this.W){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new c(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new c(`Cannot edit field ${String(r)}`)}}validateEvent(){}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,E as CollectionStorageModel,S as InMemoryModel,i as InMemoryTopic,l as PING,u as PONG,c as PermissionError,C as ReadOnly,a as ReadWrite,x as ReadWriteStruct,s as TaskQueueMap,n as TrackingTopicMap,e as UniqueIdProvider,f as WebsocketHandlerFactory};
1
+ import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(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 s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class i{u;v=new Map;constructor(t){this.u=t}async add(t,e){let r=this.v.get(t);r||(r=this.u(t),this.v.set(t,r)),await r.add(e)}async remove(t,e){const r=this.v.get(t);if(r){await r.remove(e)||this.v.delete(t)}}async broadcast(t,e){const r=this.v.get(t);r&&await r.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 a={validateWrite(){},validateEvent(){}};class o{p;_;m;S;O;constructor(t,r,a={}){this.p=t,this._=r,this.m=a.subscribers??new i(()=>new n),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a,r){let s={C:0},i="";const n=t=>{if(2===s.C){let e=t.message;if(r&&e.events?.length){const t=e.events.filter(r);e={...e,events:t.length?t:void 0}}if(t.source===i)s.I(e,t.meta);else if(e.change){if(!e.events?.length&&this._.isNoOp?.(e.change))return;s.I(e,void 0)}}else 1===s.C&&s.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.p.read(t);null!=e&&(s={C:1,J:e,t:[]},await this.m.add(t,n))}),0===s.C)return null;i=await this.O()}catch(e){throw await this.m.remove(t,n),e}return{getInitialData(){if(1!==s.C)throw new Error("Already started");return s.J},listen(t){if(1!==s.C)throw new Error("Already started");const e=s.t;s={C:2,I:t},e.forEach(n)},send:(r,s,n)=>this.N(t,r,s,e,i,n),close:async()=>{await this.m.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,i,n){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.p.read(t);if(!r)throw new Error("Deleted");const i=this._.update(r,e),n=this.p.validate(i);s.validateWrite(n,r),await this.p.write(t,n,r)}}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:i,meta:n})}0===r?.length&&(r=void 0),this.m.broadcast(t,{message:{change:e,events:r},source:i,meta:n})}async N(t,e,r,s,i,n){return this.S.push(t,()=>this.T(t,e,r,s,i,n))}}class c extends Error{}class h extends Error{}const l="P",u="p",d="X",w="x";class f{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:i=g,setSoftCloseHandler:n,onConnect:a,onDisconnect:o,onError:l=_}){return async(...u)=>{const d=[];let w;try{const{id:f,permission:g,eventFilter:_}=await t(...u),b=await this.broadcaster.subscribe(f,g,_);if(!b)throw i;d.push(()=>b.close());let E,S=y,O="connection failed";const C=new Promise(t=>{E=e=>{E=()=>{},O=e,t()}}),x=await e(...u);u.length=1,n?.(u[0],()=>(S===y&&(S=v,x.send("X"),w||(w=setTimeout(J,s))),C));const I=Date.now();a?.(u[0],f,g),d.push(()=>o?.(u[0],O,Date.now()-I)),x.on("close",()=>{clearTimeout(w),S=m,E("client disconnect")});const J=()=>{x.terminate(),S=p,E("connection lost")},N=()=>{x.ping(),clearTimeout(w),w=setTimeout(J,s)},T=()=>{clearTimeout(w),w=setTimeout(N,r)};x.on("pong",T),x.on("message",async(t,e)=>{if(T(),e)return x.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return x.send("p");if("x"===r)return S!==v&&S!==p?x.send(JSON.stringify({error:"Unexpected close ack message"})):(S=m,E("clean shutdown"),x.close());if(S===m)return x.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new h("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new h("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new h("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");r.id=e.id}return r}(r);await b.send(t.change,t.events,t.id)}catch(t){t instanceof c||t instanceof h?x.send(JSON.stringify({error:t.message})):(l(u[0],t,"message"),x.send(JSON.stringify({error:"Internal error"})))}}),S===y&&(x.send(JSON.stringify({init:b.getInitialData()})),b.listen((t,e)=>x.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await C}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){l(u[0],t,"teardown")}}}}}const y=0,v=1,m=2,p=3,g=new Error("not found"),_=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class E{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??b,this.$=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}}class S{read=this.get;validate;F=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.F.set(t,e)}get(t){return this.F.get(t)}delete(t){this.F.delete(t)}write(t,e,r){if(r!==this.F.get(t))throw new Error("Unexpected previous value");this.F.set(t,e)}}const O="Cannot modify data",C={validateWriteSpec(){throw new c(O)},validateWrite(){throw new c(O)},validateEvent(){throw new c(O)}};class x{P;constructor(t=[]){this.P=t}validateWrite(t,e){for(const r of this.P){const s=Object.prototype.hasOwnProperty.call(e,r),i=Object.prototype.hasOwnProperty.call(t,r);if(s!==i)throw new c(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(i&&e[r]!==t[r])throw new c(`Cannot edit field ${String(r)}`)}}validateEvent(){}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,E as CollectionStorageModel,S as InMemoryModel,n as InMemoryTopic,l as PING,u as PONG,c as PermissionError,C as ReadOnly,a as ReadWrite,x as ReadWriteStruct,s as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,f as WebsocketHandlerFactory};
@@ -119,4 +119,4 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
119
119
  }
120
120
 
121
121
  export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
122
- export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
122
+ export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions, StateListener };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-reducer",
3
- "version": "6.3.0",
3
+ "version": "6.3.2",
4
4
  "description": "shared state management",
5
5
  "author": "David Evans",
6
6
  "license": "MIT",