shared-reducer 6.2.0 → 6.3.1

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.
@@ -27,11 +27,14 @@ interface TopicMap<K, T> {
27
27
  broadcast(key: K, message: T): MaybePromise<void>;
28
28
  }
29
29
 
30
+ type ChangeEvent = [string, ...unknown[]];
31
+
30
32
  declare class PermissionError extends Error {
31
33
  }
32
34
  interface Permission<T, SpecT> {
33
35
  validateWriteSpec?(spec: SpecT): void;
34
36
  validateWrite(newValue: T, oldValue: T): void;
37
+ validateEvent(event: ChangeEvent): void;
35
38
  }
36
39
 
37
40
  interface Model<ID, T> {
@@ -42,17 +45,19 @@ interface Model<ID, T> {
42
45
 
43
46
  interface Context<T, SpecT> {
44
47
  update: (input: T, spec: SpecT) => T;
48
+ isNoOp?: (spec: SpecT) => boolean;
45
49
  }
46
50
  type Listener<SpecT, MetaT> = (message: ChangeInfo<SpecT>, meta: MetaT | undefined) => void;
47
51
  interface Subscription<T, SpecT, MetaT> {
48
52
  getInitialData(): Readonly<T>;
49
53
  listen(onChange: Listener<SpecT, MetaT>): void;
50
- send(change: SpecT, meta?: MetaT): Promise<void>;
54
+ send(change: SpecT, events?: ChangeEvent[] | undefined, meta?: MetaT): Promise<void>;
51
55
  close(): Promise<void>;
52
56
  }
53
57
  type Identifier = string | null;
54
58
  type ChangeInfo<SpecT> = {
55
59
  change: SpecT;
60
+ events: Readonly<Readonly<ChangeEvent>[]> | undefined;
56
61
  error?: undefined;
57
62
  } | {
58
63
  change?: undefined;
@@ -76,7 +81,10 @@ declare class Broadcaster<T, SpecT> {
76
81
  idProvider?: () => MaybePromise<string>;
77
82
  });
78
83
  subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>): Promise<Subscription<T, SpecT, MetaT> | null>;
79
- update(id: ID, change: SpecT, permission?: Permission<T, SpecT>): Promise<void>;
84
+ update(id: ID, change: SpecT, { events, permission, }?: {
85
+ events?: ChangeEvent[] | undefined;
86
+ permission?: Permission<T, SpecT>;
87
+ }): Promise<void>;
80
88
  private _internalApplyChange;
81
89
  private _internalQueueChange;
82
90
  }
@@ -161,6 +169,7 @@ declare class ReadWriteStruct<T extends object> implements Permission<T, unknown
161
169
  private readonly _readOnlyFields;
162
170
  constructor(_readOnlyFields?: (keyof T)[]);
163
171
  validateWrite(newValue: T, oldValue: T): void;
172
+ validateEvent(): void;
164
173
  }
165
174
 
166
175
  declare class AsyncTaskQueue extends EventTarget implements TaskQueue {
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;u=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.u.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.u.set(t,e),r=e}return r.push(e)}}class n{l;p=new Map;constructor(t){this.l=t}async add(t,e){let r=this.p.get(t);r||(r=this.l(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{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(){}};class o extends Error{}class c extends Error{}const h=0,u=1,l=2,d=3,w=new Error("not found"),p=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),f=t=>t;const y="Cannot modify data",m={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{_;v;m;S;O;constructor(t,r,a={}){this._=t,this.v=r,this.m=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.J(t.message,t.meta):t.message.change&&r.J(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this._.read(t);null!=e&&(r={C:1,N:e,t:[]},await this.m.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.m.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.N},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,J:t},e.forEach(n)},send:(r,n)=>this.T(t,r,e,s,n),close:async()=>{await this.m.remove(t,n)}}}update(t,e,r=a){return this.T(t,e,r,null,void 0)}async I(t,e,r,s,n){try{const s=await this._.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const n=this.v.update(s,e),i=this._.validate(n);r.validateWrite(i,s),await this._.write(t,i,s)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:n})}this.m.broadcast(t,{message:{change:e},source:s,meta:n})}async T(t,e,r,s,n){return this.S.push(t,()=>this.I(t,e,r,s,n))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;D;validate;$;q;constructor(t,e,r,s={}){this.j=t,this.D=e,this.validate=r,this.$=s.readErrorIntercept??f,this.q=s.writeErrorIntercept??f}async read(t){try{return await this.j.where(this.D,t).get()}catch(t){throw this.$(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.D,t).update(s)}catch(t){throw this.q(t)}}},exports.InMemoryModel=class{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)}},exports.InMemoryTopic=i,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=m,exports.ReadWrite=a,exports.ReadWriteStruct=class{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 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)}`)}}},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:f,onError:y=p}){return async(...w)=>{const p=[];let m;try{const{id:g,permission:_}=await t(...w),x=await this.broadcaster.subscribe(g,_);if(!x)throw n;p.push(()=>x.close());let v,b=h,S="connection failed";const O=new Promise(t=>{v=e=>{v=()=>{},S=e,t()}}),E=await e(...w);w.length=1,i?.(w[0],()=>(b===h&&(b=u,E.send("X"),m||(m=setTimeout(J,s))),O));const C=Date.now();a?.(w[0],g,_),p.push(()=>f?.(w[0],S,Date.now()-C)),E.on("close",()=>{clearTimeout(m),b=l,v("client disconnect")});const J=()=>{E.terminate(),b=d,v("connection lost")},N=()=>{E.ping(),clearTimeout(m),m=setTimeout(J,s)},T=()=>{clearTimeout(m),m=setTimeout(N,r)};E.on("pong",T),E.on("message",async(t,e)=>{if(T(),e)return E.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return E.send("p");if("x"===r)return b!==u&&b!==d?E.send(JSON.stringify({error:"Unexpected close ack message"})):(b=l,v("clean shutdown"),E.close());if(b===l)return E.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");if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");return{change:e.change,id:e.id}}return{change:e.change}}(r);await x.send(t.change,t.id)}catch(t){t instanceof o||t instanceof c?E.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),E.send(JSON.stringify({error:"Internal error"})))}}),b===h&&(E.send(JSON.stringify({init:x.getInitialData()})),x.listen((t,e)=>E.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await O}finally{clearTimeout(m);for(const t of p.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 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")}}}}};
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;u=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.u.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.u.set(t,e),r=e}return r.push(e)}}class n{l;m=new Map;constructor(t){this.l=t}async add(t,e){let r=this.m.get(t);r||(r=this.l(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(){}};class o{_;v;p;S;O;constructor(t,r,a={}){this._=t,this.v=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.J(t.message,t.meta):t.message.change&&r.J(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this._.read(t);null!=e&&(r={C:1,N: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.N},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,J:t},e.forEach(n)},send:(r,n)=>this.T(t,r,e,s,n),close:async()=>{await this.p.remove(t,n)}}}update(t,e,r=a){return this.T(t,e,r,null,void 0)}async I(t,e,r,s,n){try{const s=await this._.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const n=this.v.update(s,e),i=this._.validate(n);r.validateWrite(i,s),await this._.write(t,i,s)}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:n})}this.p.broadcast(t,{message:{change:e},source:s,meta:n})}async T(t,e,r,s,n){return this.S.push(t,()=>this.I(t,e,r,s,n))}}class c extends Error{}class h extends Error{}const u="P",l="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=_,setSoftCloseHandler:i,onConnect:a,onDisconnect:o,onError:u=v}){return async(...l)=>{const d=[];let w;try{const{id:f,permission:_}=await t(...l),v=await this.broadcaster.subscribe(f,_);if(!v)throw n;d.push(()=>v.close());let b,S=y,O="connection failed";const E=new Promise(t=>{b=e=>{b=()=>{},O=e,t()}}),C=await e(...l);l.length=1,i?.(l[0],()=>(S===y&&(S=m,C.send("X"),w||(w=setTimeout(J,s))),E));const x=Date.now();a?.(l[0],f,_),d.push(()=>o?.(l[0],O,Date.now()-x)),C.on("close",()=>{clearTimeout(w),S=p,b("client disconnect")});const J=()=>{C.terminate(),S=g,b("connection lost")},N=()=>{C.ping(),clearTimeout(w),w=setTimeout(J,s)},T=()=>{clearTimeout(w),w=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 S!==m&&S!==g?C.send(JSON.stringify({error:"Unexpected close ack message"})):(S=p,b("clean shutdown"),C.close());if(S===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");if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");return{change:e.change,id:e.id}}return{change:e.change}}(r);await v.send(t.change,t.id)}catch(t){t instanceof c||t instanceof h?C.send(JSON.stringify({error:t.message})):(u(l[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),S===y&&(C.send(JSON.stringify({init:v.getInitialData()})),v.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await E}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){u(l[0],t,"teardown")}}}}}const y=0,m=1,p=2,g=3,_=new Error("not found"),v=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class S{j;D;validate;$;P;constructor(t,e,r,s={}){this.j=t,this.D=e,this.validate=r,this.$=s.readErrorIntercept??b,this.P=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.D,t).get()}catch(t){throw this.$(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.D,t).update(s)}catch(t){throw this.P(t)}}}class O{read=this.get;validate;W=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.W.set(t,e)}get(t){return this.W.get(t)}delete(t){this.W.delete(t)}write(t,e,r){if(r!==this.W.get(t))throw new Error("Unexpected previous value");this.W.set(t,e)}}const E="Cannot modify data",C={validateWriteSpec(){throw new c(E)},validateWrite(){throw new c(E)}};class x{k;constructor(t=[]){this.k=t}validateWrite(t,e){for(const r of this.k){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)}`)}}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,S as CollectionStorageModel,O as InMemoryModel,i as InMemoryTopic,u as PING,l 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 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,3 +1,5 @@
1
+ type ChangeEvent = [string, ...unknown[]];
2
+
1
3
  interface Context<T, SpecT> {
2
4
  update: (input: T, spec: SpecT) => T;
3
5
  combine: (specs: SpecT[]) => SpecT;
@@ -5,9 +7,15 @@ interface Context<T, SpecT> {
5
7
  type SpecGenerator<T, SpecT> = (state: T) => SpecSource<T, SpecT>[];
6
8
  type SpecSource<T, SpecT> = SpecT | SpecGenerator<T, SpecT> | null;
7
9
  type DispatchSpec<T, SpecT> = SpecSource<T, SpecT>[];
8
- interface Dispatch<T, SpecT> {
9
- sync(specs?: DispatchSpec<T, SpecT>): Promise<T>;
10
- (specs: DispatchSpec<T, SpecT>, syncedCallback?: (state: T) => void, errorCallback?: (error: string) => void): void;
10
+ type DispatchFn<T, SpecT> = (specs: DispatchSpec<T, SpecT>, options?: {
11
+ events?: ChangeEvent[] | undefined;
12
+ syncedCallback?: ((state: T) => void) | undefined;
13
+ errorCallback?: ((error: string) => void) | undefined;
14
+ }) => void;
15
+ interface Dispatch<T, SpecT> extends DispatchFn<T, SpecT> {
16
+ sync(specs?: DispatchSpec<T, SpecT>, options?: {
17
+ events?: ChangeEvent[] | undefined;
18
+ }): Promise<T>;
11
19
  }
12
20
 
13
21
  type MaybePromise<T> = Promise<T> | T;
@@ -79,6 +87,7 @@ type SharedReducerEvents = {
79
87
  rejected: CustomEvent<DisconnectDetail>;
80
88
  warning: CustomEvent<Error>;
81
89
  };
90
+ type StateListener<T> = (state: Readonly<T>, events: Readonly<Readonly<ChangeEvent>[]>) => void;
82
91
  declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEvents> {
83
92
  private readonly _context;
84
93
  private readonly _ws;
@@ -97,8 +106,8 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
97
106
  private _handleErrorMessage;
98
107
  private _handleGracefulClose;
99
108
  private readonly _handleMessage;
100
- addStateListener(listener: (state: Readonly<T>) => void): void;
101
- removeStateListener(listener: (state: Readonly<T>) => void): void;
109
+ addStateListener(listener: StateListener<T>): void;
110
+ removeStateListener(listener: StateListener<T>): void;
102
111
  private _setLocalState;
103
112
  getState(): Readonly<T> | undefined;
104
113
  private _warn;
@@ -110,4 +119,4 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
110
119
  }
111
120
 
112
121
  export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
113
- export type { ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
122
+ export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions, StateListener };
package/frontend/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{j;I;k=null;P=!1;constructor(t,s){super(),this.j=t,this.I=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.I.trigger(this.L,this.M)}reconnect(t){t&&(this.j=t),this.k?this.k.close():this.P||this.I.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.j;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.k=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.I.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.k=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.P=!0,this.I.stop(),this.k?.close()}}const o="P",r="p",l=2e4,c=()=>!0;class a{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t){this.D.push({F:void 0,q:t,G:[],H:[]})}R(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.G.push(s??u),t.H.push(e??u)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}W(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.G.length>0||e.H.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,id:s.F})))}X(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{B:null,K:!1}:{B:this.D.splice(s,1)[0],K:0===s}}V(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}const u=()=>null;const d={code:0,reason:"graceful shutdown"},_=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=c,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{A;k;Y=!0;S={Z:0,tt:[]};st;et=new Set;it=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(_,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.st=new a(s,n),this.k=new h(e,i),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("rejected",this.rt),this.k.addEventListener("disconnected",this.lt)}reconnect(t){this.k.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((e,i)=>t(s,e,t=>i(new Error(t))))})}((t,s,e)=>{if(!t.length&&!s&&!e)return;const i={ct:t,G:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ut(this.dt(this.S._t,[i])),this.ft.gt()}});dt(t,s){return this.it(()=>{for(const{ct:i,G:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.A,t,i);t=s,this.st.N(n)}this.st.R(t,n,h)}return t})}ft=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{wt:i,gt:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.W(this.k.send)});vt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.dt(t.init,this.S.tt);this.S={Z:1,bt:t.init,_t:s},this.ut(s,!0)}else this.S.bt=t.init,this.st.U(t.init),this.ut(this.st.V(t.init));this.ft.wt()}else this.yt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Tt(t){if(1!==this.S.Z)return void this.yt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.bt=this.A.update(this.S.bt,t.change),{B:e,K:i}=this.st.X(t.id);i||this.ut(this.st.V(s)),e?.G.forEach(t=>t(s))}Et(t){if(1!==this.S.Z)return void this.yt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.st.X(t.id);s?(this.yt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ut(this.st.V(this.S.bt))):this.yt(`API sent error: ${t.error}`)}xt(){this.k.send("x"),this.Y?this.yt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:d})))}nt=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.Tt(s):"init"in s?this.vt(s):"error"in s?this.Et(s):this.yt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S._t)}removeStateListener(t){this.et.delete(t)}ut(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S._t!==t){this.S._t=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S._t:void 0}yt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};rt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};lt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.ft.o(),this.et.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("rejected",this.rt),this.k.removeEventListener("disconnected",this.lt)}},exports.exponentialDelay=s;
1
+ "use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,k:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{$;j;I=null;P=!1;constructor(t,s){super(),this.$=t,this.j=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.j.trigger(this.L,this.M)}reconnect(t){t&&(this.$=t),this.I?this.I.close():this.P||this.j.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.$;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.I=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.j.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.I=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.I}send=t=>{if(!this.I)throw new Error("connection lost");this.I.send(t)};close(){this.P=!0,this.j.stop(),this.I?.close()}}const o="P",r="p",l=2e4,c=()=>!0;class a{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t,s){this.D.push({F:void 0,q:t,G:s,H:[],R:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.H.push(s??d),t.R.push(e??d)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.R.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.H.length>0||e.R.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),i.G=u(n.map(t=>t.G)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,events:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,V:!1}:{K:this.D.splice(s,1)[0],V:0===s}}Y(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}function u(t){const[s,...e]=t.filter(t=>void 0!==t);if(!s)return;if(!e.length)return s;const i=[...s];for(const t of e)for(const s of t){const t=i.findIndex(t=>t[0]===s[0]);-1!==t&&i.splice(t,1),i.push(s)}return i}const d=()=>null;const _=[],g={code:0,reason:"graceful shutdown"},f=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=c,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{A;I;Z=!0;S={tt:0,st:[]};et;it=new Set;nt=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(f,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.et=new a(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.ht),this.I.addEventListener("connected",this.ot),this.I.addEventListener("connectionfailure",this.rt),this.I.addEventListener("rejected",this.lt),this.I.addEventListener("disconnected",this.ct)}reconnect(t){this.I.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[],e={})=>new Promise((i,n)=>t(s,{...e,syncedCallback:i,errorCallback:t=>n(new Error(t))}))})}((t,s={})=>{if(!(t.length||s.events?.length||s.syncedCallback||s.errorCallback))return;const e={ut:t,G:s.events,H:s.syncedCallback,R:s.errorCallback};switch(this.S.tt){case-1:throw new Error("closed");case 0:this.S.st.push(e);break;case 1:this.dt(this._t(this.S.gt,[e]),s.events),this.wt.ft()}});_t(t,s){return this.nt(()=>{for(const{ut:i,G:n,H:h,R:o}of s){if(i.length){const{S:s,k:h}=e(this.A,t,i);t=s,this.et.N(h,n)}else n?.length&&this.et.N(this.A.combine([]),n);this.et.U(t,h,o)}return t})}wt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{vt:i,ft:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.I.isConnected()&&!this.Z&&this.et.X(this.I.send)});bt(t){if(-1!==this.S.tt){if(this.Z=!1,0===this.S.tt){const s=this._t(t.init,this.S.st);this.S={tt:1,yt:t.init,gt:s},this.dt(s,_,!0)}else this.S.yt=t.init,this.et.W(t.init),this.dt(this.et.Y(t.init));this.wt.vt()}else this.Tt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Ct(t){if(1!==this.S.tt)return void this.Tt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.yt=this.A.update(this.S.yt,t.change),{K:e,V:i}=this.et.B(t.id);i||this.dt(this.et.Y(s),e?_:t.events),e?.H.forEach(t=>t(s))}Et(t){if(1!==this.S.tt)return void this.Tt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.et.B(t.id);s?(this.Tt(`API rejected update: ${t.error}`),s?.R.forEach(s=>s(t.error)),this.dt(this.et.Y(this.S.yt))):this.Tt(`API sent error: ${t.error}`)}xt(){this.I.send("x"),this.Z?this.Tt("Unexpected extra close message"):(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:g})))}ht=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.Ct(s):"init"in s?this.bt(s):"error"in s?this.Et(s):this.Tt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.tt&&t(this.S.gt,[])}removeStateListener(t){this.it.delete(t)}dt(t,s=_,e=!1){if(1!==this.S.tt)throw new Error("invalid state");if(e||this.S.gt!==t||s.length){this.S.gt=t;for(const e of this.it)e(t,s)}}getState(){return 1===this.S.tt?this.S.gt:void 0}Tt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ot=()=>{this.dispatchEvent(n("connected"))};rt=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};lt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};ct=t=>{this.Z||(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Z=!0,this.S={tt:-1},this.I.close(),this.wt.o(),this.it.clear(),this.I.removeEventListener("message",this.ht),this.I.removeEventListener("connected",this.ot),this.I.removeEventListener("connectionfailure",this.rt),this.I.removeEventListener("rejected",this.lt),this.I.removeEventListener("disconnected",this.ct)}},exports.exponentialDelay=s;
@@ -1 +1 @@
1
- class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{j;I;k=null;P=!1;constructor(t,s){super(),this.j=t,this.I=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.I.trigger(this.L,this.M)}reconnect(t){t&&(this.j=t),this.k?this.k.close():this.P||this.I.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.j;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.k=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.I.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.k=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.P=!0,this.I.stop(),this.k?.close()}}const o="P",r="p",l=2e4,c=()=>!0,a=(t,s,e)=>!e;class u{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t){this.D.push({F:void 0,q:t,G:[],H:[]})}R(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.G.push(s??d),t.H.push(e??d)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}W(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.G.length>0||e.H.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,id:s.F})))}X(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{B:null,K:!1}:{B:this.D.splice(s,1)[0],K:0===s}}V(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}const d=()=>null;class _ extends i{A;k;Y=!0;S={Z:0,tt:[]};st;et=new Set;it=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(f,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.st=new u(s,n),this.k=new h(e,i),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("rejected",this.rt),this.k.addEventListener("disconnected",this.lt)}reconnect(t){this.k.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((e,i)=>t(s,e,t=>i(new Error(t))))})}((t,s,e)=>{if(!t.length&&!s&&!e)return;const i={ct:t,G:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ut(this.dt(this.S._t,[i])),this.ft.gt()}});dt(t,s){return this.it(()=>{for(const{ct:i,G:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.A,t,i);t=s,this.st.N(n)}this.st.R(t,n,h)}return t})}ft=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{wt:i,gt:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.W(this.k.send)});vt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.dt(t.init,this.S.tt);this.S={Z:1,bt:t.init,_t:s},this.ut(s,!0)}else this.S.bt=t.init,this.st.U(t.init),this.ut(this.st.V(t.init));this.ft.wt()}else this.yt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Tt(t){if(1!==this.S.Z)return void this.yt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.bt=this.A.update(this.S.bt,t.change),{B:e,K:i}=this.st.X(t.id);i||this.ut(this.st.V(s)),e?.G.forEach(t=>t(s))}Et(t){if(1!==this.S.Z)return void this.yt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.st.X(t.id);s?(this.yt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ut(this.st.V(this.S.bt))):this.yt(`API sent error: ${t.error}`)}Ct(){this.k.send("x"),this.Y?this.yt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:g})))}nt=t=>{if("X"===t.detail)return void this.Ct();const s=JSON.parse(t.detail);"change"in s?this.Tt(s):"init"in s?this.vt(s):"error"in s?this.Et(s):this.yt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S._t)}removeStateListener(t){this.et.delete(t)}ut(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S._t!==t){this.S._t=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S._t:void 0}yt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};rt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};lt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.ft.o(),this.et.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("rejected",this.rt),this.k.removeEventListener("disconnected",this.lt)}}const g={code:0,reason:"graceful shutdown"},f=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{c as AT_LEAST_ONCE,a as AT_MOST_ONCE,t as OnlineScheduler,_ as SharedReducer,s as exponentialDelay};
1
+ class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.v()}async m(){if(this.o||!this.l)return;this.v();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}v(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={p:t,T:0,C:null};for(;e;)if(e.T>=e.p.length)e=e.C;else{const t=s(e.p[e.T]);++e.T,t&&t.length&&(e={p:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,k:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{$;j;I=null;P=!1;constructor(t,s){super(),this.$=t,this.j=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.j.trigger(this.L,this.M)}reconnect(t){t&&(this.$=t),this.I?this.I.close():this.P||this.j.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.$;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.I=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.j.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.I=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.I}send=t=>{if(!this.I)throw new Error("connection lost");this.I.send(t)};close(){this.P=!0,this.j.stop(),this.I?.close()}}const o="P",r="p",l=2e4,c=()=>!0,a=(t,s,e)=>!e;class u{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t,s){this.D.push({F:void 0,q:t,G:s,H:[],R:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.H.push(s??_),t.R.push(e??_)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.R.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.H.length>0||e.R.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),i.G=d(n.map(t=>t.G)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,events:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,V:!1}:{K:this.D.splice(s,1)[0],V:0===s}}Y(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}function d(t){const[s,...e]=t.filter(t=>void 0!==t);if(!s)return;if(!e.length)return s;const i=[...s];for(const t of e)for(const s of t){const t=i.findIndex(t=>t[0]===s[0]);-1!==t&&i.splice(t,1),i.push(s)}return i}const _=()=>null;class g extends i{A;I;Z=!0;S={tt:0,st:[]};et;it=new Set;nt=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(w,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.et=new u(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.ht),this.I.addEventListener("connected",this.ot),this.I.addEventListener("connectionfailure",this.rt),this.I.addEventListener("rejected",this.lt),this.I.addEventListener("disconnected",this.ct)}reconnect(t){this.I.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[],e={})=>new Promise((i,n)=>t(s,{...e,syncedCallback:i,errorCallback:t=>n(new Error(t))}))})}((t,s={})=>{if(!(t.length||s.events?.length||s.syncedCallback||s.errorCallback))return;const e={ut:t,G:s.events,H:s.syncedCallback,R:s.errorCallback};switch(this.S.tt){case-1:throw new Error("closed");case 0:this.S.st.push(e);break;case 1:this.dt(this._t(this.S.gt,[e]),s.events),this.wt.ft()}});_t(t,s){return this.nt(()=>{for(const{ut:i,G:n,H:h,R:o}of s){if(i.length){const{S:s,k:h}=e(this.A,t,i);t=s,this.et.N(h,n)}else n?.length&&this.et.N(this.A.combine([]),n);this.et.U(t,h,o)}return t})}wt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{vt:i,ft:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.I.isConnected()&&!this.Z&&this.et.X(this.I.send)});bt(t){if(-1!==this.S.tt){if(this.Z=!1,0===this.S.tt){const s=this._t(t.init,this.S.st);this.S={tt:1,yt:t.init,gt:s},this.dt(s,f,!0)}else this.S.yt=t.init,this.et.W(t.init),this.dt(this.et.Y(t.init));this.wt.vt()}else this.Tt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Ct(t){if(1!==this.S.tt)return void this.Tt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.yt=this.A.update(this.S.yt,t.change),{K:e,V:i}=this.et.B(t.id);i||this.dt(this.et.Y(s),e?f:t.events),e?.H.forEach(t=>t(s))}Et(t){if(1!==this.S.tt)return void this.Tt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.et.B(t.id);s?(this.Tt(`API rejected update: ${t.error}`),s?.R.forEach(s=>s(t.error)),this.dt(this.et.Y(this.S.yt))):this.Tt(`API sent error: ${t.error}`)}St(){this.I.send("x"),this.Z?this.Tt("Unexpected extra close message"):(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:m})))}ht=t=>{if("X"===t.detail)return void this.St();const s=JSON.parse(t.detail);"change"in s?this.Ct(s):"init"in s?this.bt(s):"error"in s?this.Et(s):this.Tt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.tt&&t(this.S.gt,[])}removeStateListener(t){this.it.delete(t)}dt(t,s=f,e=!1){if(1!==this.S.tt)throw new Error("invalid state");if(e||this.S.gt!==t||s.length){this.S.gt=t;for(const e of this.it)e(t,s)}}getState(){return 1===this.S.tt?this.S.gt:void 0}Tt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ot=()=>{this.dispatchEvent(n("connected"))};rt=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};lt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};ct=t=>{this.Z||(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Z=!0,this.S={tt:-1},this.I.close(),this.wt.o(),this.it.clear(),this.I.removeEventListener("message",this.ht),this.I.removeEventListener("connected",this.ot),this.I.removeEventListener("connectionfailure",this.rt),this.I.removeEventListener("rejected",this.lt),this.I.removeEventListener("disconnected",this.ct)}}const f=[],m={code:0,reason:"graceful shutdown"},w=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{c as AT_LEAST_ONCE,a as AT_MOST_ONCE,t as OnlineScheduler,g as SharedReducer,s as exponentialDelay};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-reducer",
3
- "version": "6.2.0",
3
+ "version": "6.3.1",
4
4
  "description": "shared state management",
5
5
  "author": "David Evans",
6
6
  "license": "MIT",