shared-reducer 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,10 +164,10 @@ fields are added or types are changed).
164
164
  import { SharedReducer } from 'shared-reducer/frontend';
165
165
  import context from 'json-immutability-helper';
166
166
 
167
- const reducer = new SharedReducer(context, () => ({
167
+ const reducer = new SharedReducer(context, {
168
168
  url: 'ws://destination',
169
169
  token: 'my-token',
170
- }));
170
+ });
171
171
 
172
172
  reducer.addStateListener((state) => {
173
173
  console.log('latest state is', state);
@@ -290,9 +290,9 @@ import listCommands from 'json-immutability-helper/commands/list';
290
290
  import mathCommands from 'json-immutability-helper/commands/math';
291
291
  import context from 'json-immutability-helper';
292
292
 
293
- const reducer = new SharedReducer(context.with(listCommands, mathCommands), () => ({
293
+ const reducer = new SharedReducer(context.with(listCommands, mathCommands), {
294
294
  url: 'ws://destination',
295
- }));
295
+ });
296
296
  ```
297
297
 
298
298
  If you want to use an entirely different reducer, create a wrapper:
@@ -315,7 +315,7 @@ const myReducer = {
315
315
  const broadcaster = new Broadcaster(new InMemoryModel(), myReducer);
316
316
 
317
317
  // Frontend
318
- const reducer = new SharedReducer(myReducer, () => ({ url: 'ws://destination' }));
318
+ const reducer = new SharedReducer(myReducer, { url: 'ws://destination' });
319
319
  ```
320
320
 
321
321
  Be careful when using your own reducer to avoid introducing security vulnerabilities; the functions
@@ -389,17 +389,21 @@ the page regains focus or the computer rejoins a network. You can fully customis
389
389
  ```javascript
390
390
  import { SharedReducer, OnlineScheduler, exponentialDelay } from 'shared-reducer/frontend';
391
391
 
392
- const reducer = new SharedReducer(context, () => ({ url: 'ws://destination' }), {
393
- scheduler: new OnlineScheduler(
394
- exponentialDelay({
395
- base: 2,
396
- initialDelay: 200,
397
- maxDelay: 10 * 60 * 1000,
398
- randomness: 0.3,
399
- }),
400
- 20 * 1000, // timeout for each connection attempt
401
- ),
402
- });
392
+ const reducer = new SharedReducer(
393
+ context,
394
+ { url: 'ws://destination' },
395
+ {
396
+ scheduler: new OnlineScheduler(
397
+ exponentialDelay({
398
+ base: 2,
399
+ initialDelay: 200,
400
+ maxDelay: 10 * 60 * 1000,
401
+ randomness: 0.3,
402
+ }),
403
+ 20 * 1000, // timeout for each connection attempt
404
+ ),
405
+ },
406
+ );
403
407
  ```
404
408
 
405
409
  The `exponentialDelay` helper returns:
@@ -414,15 +418,35 @@ You can also provide a custom function instead of `exponentialDelay`; it will be
414
418
  attempt number (0-based), and should return the number of milliseconds to wait before triggering the
415
419
  attempt.
416
420
 
421
+ If you need to reauthenticate (e.g. due to an expired token), you can listen for the `'rejected'`
422
+ event and call `reconnect` with a new token (or a new URL):
423
+
424
+ ```javascript
425
+ const reducer = new SharedReducer(context, { url: 'ws://destination', token: 'my-initial-token' });
426
+ reducer.addEventListener('rejected', (e) => {
427
+ if (e.detail.code === 4401) {
428
+ // example websocket code sent by server when rejecting the auth
429
+ e.preventDefault(); // do not automatically retry
430
+
431
+ // these steps do not need to be performed synchronously;
432
+ // just call .reconnect once you have a new token to use
433
+ const password = prompt('Enter the new password');
434
+ reducer.reconnect({ url: 'ws://destination', token: tokenFromPassword(password) });
435
+ }
436
+ });
437
+ ```
438
+
417
439
  Finally, by default when reconnecting `SharedReducer` will replay all messages which have not been
418
440
  confirmed (`AT_LEAST_ONCE` delivery). You can change this to `AT_MOST_ONCE` or a custom mechanism:
419
441
 
420
442
  ```javascript
421
443
  import { SharedReducer, AT_MOST_ONCE } from 'shared-reducer/frontend';
422
444
 
423
- const reducer = new SharedReducer(context, () => ({ url: 'ws://destination' }), {
424
- deliveryStrategy: AT_MOST_ONCE,
425
- });
445
+ const reducer = new SharedReducer(
446
+ context,
447
+ { url: 'ws://destination' },
448
+ { deliveryStrategy: AT_MOST_ONCE },
449
+ );
426
450
  ```
427
451
 
428
452
  Custom strategies can be defined as functions:
@@ -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
  }
@@ -108,7 +116,7 @@ interface WebsocketHandlerOptions<Arg0> {
108
116
  pongTimeout?: number;
109
117
  notFoundError?: Error;
110
118
  setSoftCloseHandler?: (arg0: Arg0, handler: () => Promise<void>) => void;
111
- onConnect?: (arg0: Arg0) => void;
119
+ onConnect?: (arg0: Arg0, id: string, permission: Permission<unknown, unknown>) => void;
112
120
  onDisconnect?: (arg0: Arg0, reason: string, connectionDuration: number) => void;
113
121
  onError?: (arg0: Arg0, error: unknown, context: string) => void;
114
122
  }
@@ -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]),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]),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;
@@ -31,7 +39,7 @@ declare class OnlineScheduler implements Scheduler {
31
39
  private _attempts;
32
40
  constructor(_delayGetter: DelayGetter, _connectTimeLimit: number);
33
41
  trigger(handler: Handler, errorHandler: ErrorHandler): void;
34
- schedule(handler: Handler, errorHandler: ErrorHandler): void;
42
+ schedule(handler: Handler, errorHandler: ErrorHandler, reset?: boolean): void;
35
43
  stop(): void;
36
44
  private _attempt;
37
45
  private _remove;
@@ -60,7 +68,6 @@ interface ConnectionInfo {
60
68
  url: string;
61
69
  token?: string | undefined;
62
70
  }
63
- type ConnectionGetter = (signal: AbortSignal) => MaybePromise<ConnectionInfo>;
64
71
  interface DisconnectDetail {
65
72
  code: number;
66
73
  reason: string;
@@ -77,8 +84,10 @@ interface SharedReducerOptions<T, SpecT> {
77
84
  type SharedReducerEvents = {
78
85
  connected: CustomEvent<void>;
79
86
  disconnected: CustomEvent<DisconnectDetail>;
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;
@@ -87,7 +96,8 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
87
96
  private readonly _tracker;
88
97
  private readonly _listeners;
89
98
  private readonly _dispatchLock;
90
- constructor(_context: Context<T, SpecT>, connectionGetter: ConnectionGetter, { scheduler, deliveryStrategy, }?: SharedReducerOptions<T, SpecT>);
99
+ constructor(_context: Context<T, SpecT>, connectionInfo: ConnectionInfo, { scheduler, deliveryStrategy, }?: SharedReducerOptions<T, SpecT>);
100
+ reconnect(connectionInfo?: ConnectionInfo): void;
91
101
  readonly dispatch: Dispatch<T, SpecT>;
92
102
  private _apply;
93
103
  private readonly _share;
@@ -96,16 +106,17 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
96
106
  private _handleErrorMessage;
97
107
  private _handleGracefulClose;
98
108
  private readonly _handleMessage;
99
- addStateListener(listener: (state: Readonly<T>) => void): void;
100
- removeStateListener(listener: (state: Readonly<T>) => void): void;
109
+ addStateListener(listener: StateListener<T>): void;
110
+ removeStateListener(listener: StateListener<T>): void;
101
111
  private _setLocalState;
102
112
  getState(): Readonly<T> | undefined;
103
113
  private _warn;
104
114
  private readonly _handleConnected;
105
115
  private readonly _handleConnectionFailure;
116
+ private readonly _handleRejected;
106
117
  private readonly _handleDisconnected;
107
118
  close(): void;
108
119
  }
109
120
 
110
121
  export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
111
- export type { Context, DeliveryStrategy, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
122
+ export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
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){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()};try{await Promise.race([this.l(t.signal),s.promise]),this.l=null,this._=0}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}const e=this.l;e&&(this.l=null,this.schedule(e,this.u))}}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,{detail:s});class h extends i{I;P;k=null;L=!1;constructor(t,s){super(),this.I=t,this.P=s,this.M=this.M.bind(this),this.A=this.A.bind(this),this.P.trigger(this.M,this.A)}A(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",s))}async M(t){const{url:s,token:e}=await this.I(t);if(t.aborted)throw t.reason;const i=new AbortController,h=i.signal;await new Promise((a,u)=>{const d=new WebSocket(s);let _=!0;const g=t=>{i.abort(),d.close(),_?(_=!1,u(new Error(`Connection closed ${t.code} ${t.reason}`))):(this.k=null,this.dispatchEvent(n("disconnected",t)),this.L||this.P.schedule(this.M,this.A))};e&&d.addEventListener("open",()=>d.send(e),{once:!0,signal:h}),d.addEventListener("message",t=>{t.data!==l&&(_&&(_=!1,this.k=d,this.dispatchEvent(n("connected")),a()),this.dispatchEvent(n("message",t.data)))},{signal:h}),d.addEventListener("close",g,{signal:h}),d.addEventListener("error",()=>g(o),{signal:h}),t.addEventListener("abort",()=>()=>{i.abort(),d.close(),_=!1,u(t.reason)},{signal:h}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(r)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,c)},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})}(d)}).catch(t=>{throw i.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.L=!0,this.P.stop(),this.k?.close()}}const o={code:0,reason:"client side error"},r="P",l="p",c=2e4,a=()=>!0;class u{O;j;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.O=t,this.j=s}N(t){this.D.push({F:void 0,G:t,q:[],H:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.q.push(s??d),t.H.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.j(t,i.G,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}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.q.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.G=this.O.combine(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.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,R:!1}:{K:this.D.splice(s,1)[0],R:0===s}}V(t){if(!this.D.length)return t;const s=this.O.combine(this.D.map(({G:t})=>t));return this.O.update(t,s)}}const d=()=>null;const _={code:0,reason:"graceful shutdown"},g=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=a,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{O;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(g,2e4),deliveryStrategy:n=a}={}){super(),this.O=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("disconnected",this.rt)}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={lt:t,q:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ct(this.ut(this.S.dt,[i])),this.gt._t()}});ut(t,s){return this.it(()=>{for(const{lt:i,q:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.O,t,i);t=s,this.st.N(n)}this.st.U(t,n,h)}return t})}gt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{ft:i,_t:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.X(this.k.send)});wt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.ut(t.init,this.S.tt);this.S={Z:1,vt:t.init,dt:s},this.ct(s,!0)}else this.S.vt=t.init,this.st.W(t.init),this.ct(this.st.V(t.init));this.gt.ft()}else this.bt(`Ignoring init after closing: ${JSON.stringify(t)}`)}yt(t){if(1!==this.S.Z)return void this.bt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.vt=this.O.update(this.S.vt,t.change),{K:e,R:i}=this.st.B(t.id);i||this.ct(this.st.V(s)),e?.q.forEach(t=>t(s))}Tt(t){if(1!==this.S.Z)return void this.bt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.st.B(t.id);s?(this.bt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ct(this.st.V(this.S.vt))):this.bt(`API sent error: ${t.error}`)}xt(){this.k.send("x"),this.Y?this.bt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",_)))}nt=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.yt(s):"init"in s?this.wt(s):"error"in s?this.Tt(s):this.bt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S.dt)}removeStateListener(t){this.et.delete(t)}ct(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S.dt!==t){this.S.dt=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S.dt:void 0}bt(t){this.dispatchEvent(n("warning",new Error(t)))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",t.detail))};rt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.gt.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("disconnected",this.rt)}},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){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((i,e)=>{const n=setTimeout(()=>e(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};try{await Promise.race([this.l(t.signal),s.promise]),this.l=null,this._=0}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}const i=this.l;i&&(this.l=null,this.schedule(i,this.u))}}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:i,randomness:e=0})=>n=>Math.min(Math.pow(t,n)*s,i)*(1-Math.random()*e);function i(t,s,i){const e=[],n=[];function h(){if(n.length>0){const i=t.combine(n);e.push(i),s=t.update(s,i),n.length=0}}return function(t,s){let i={v:t,T:0,C:null};for(;i;)if(i.T>=i.v.length)i=i.C;else{const t=s(i.v[i.T]);++i.T,t&&t.length&&(i={v:t,T:0,C:i})}}(i,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(e)}}class e extends EventTarget{addEventListener(t,s,i){super.addEventListener(t,s,i)}removeEventListener(t,s,i){super.removeEventListener(t,s,i)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,{detail:s});class h extends e{I;P;k=null;L=!1;constructor(t,s){super(),this.I=t,this.P=s,this.M=this.M.bind(this),this.A=this.A.bind(this),this.P.trigger(this.M,this.A)}A(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",s))}async M(t){const{url:s,token:i}=await this.I(t);if(t.aborted)throw t.reason;const e=new AbortController,h=e.signal;await new Promise((a,u)=>{const d=new WebSocket(s);let _=!0;const g=t=>{e.abort(),d.close(),_?(_=!1,u(new Error(`Connection closed ${t.code} ${t.reason}`))):(this.k=null,this.dispatchEvent(n("disconnected",t)),this.L||this.P.schedule(this.M,this.A))};i&&d.addEventListener("open",()=>d.send(i),{once:!0,signal:h}),d.addEventListener("message",t=>{t.data!==l&&(_&&(_=!1,this.k=d,this.dispatchEvent(n("connected")),a()),this.dispatchEvent(n("message",t.data)))},{signal:h}),d.addEventListener("close",g,{signal:h}),d.addEventListener("error",()=>g(o),{signal:h}),t.addEventListener("abort",()=>()=>{e.abort(),d.close(),_=!1,u(t.reason)},{signal:h}),function(t){const s=new AbortController;let i=null;const e=()=>{null!==i&&(clearTimeout(i),i=null),t.send(r)},n=()=>{null!==i&&clearTimeout(i),i=setTimeout(e,c)},h=()=>{null!==i&&(clearTimeout(i),i=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",e,{signal:s.signal})}(d)}).catch(t=>{throw e.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.L=!0,this.P.stop(),this.k?.close()}}const o={code:0,reason:"client side error"},r="P",l="p",c=2e4,a=()=>!0,u=(t,s,i)=>!i;class d{O;j;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.O=t,this.j=s}N(t){this.D.push({F:void 0,G:t,q:[],H:[]})}U(t,s,i){if(s||i)if(this.D.length){const t=this.D[this.D.length-1];t.q.push(s??_),t.H.push(i??_)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let i=0;i<this.D.length;++i){const e=this.D[i];this.j(t,e.G,void 0!==e.F)?(e.F=void 0,this.D[i-s]=e):(e.H.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 i=this.D[t];if(!i||void 0!==i.F||i.q.length>0||i.H.length>0){const i=t-s;if(i>1){const e=this.D[t-1],n=this.D.splice(s,i,e);e.G=this.O.combine(n.map(t=>t.G)),t-=i-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change: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,R:!1}:{K:this.D.splice(s,1)[0],R:0===s}}V(t){if(!this.D.length)return t;const s=this.O.combine(this.D.map(({G:t})=>t));return this.O.update(t,s)}}const _=()=>null;class g extends e{O;k;Y=!0;S={Z:0,tt:[]};st;it=new Set;et=function(t){let s=!1;return i=>{if(s)throw new Error(t);try{return s=!0,i()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,i,{scheduler:e=new t(w,2e4),deliveryStrategy:n=a}={}){super(),this.O=s,this.st=new d(s,n),this.k=new h(i,e),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("disconnected",this.rt)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((i,e)=>t(s,i,t=>e(new Error(t))))})}((t,s,i)=>{if(!t.length&&!s&&!i)return;const e={lt:t,q:s,H:i};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(e);break;case 1:this.ct(this.ut(this.S.dt,[e])),this.gt._t()}});ut(t,s){return this.et(()=>{for(const{lt:e,q:n,H:h}of s){if(e.length){const{S:s,$:n}=i(this.O,t,e);t=s,this.st.N(n)}this.st.U(t,n,h)}return t})}gt=function(t){let s=null;const i=()=>{null!==s&&(clearTimeout(s),s=null)},e=()=>{i(),t()};return{ft:e,_t:()=>{null===s&&(s=setTimeout(e,0))},o:i}}(()=>{this.k.isConnected()&&!this.Y&&this.st.X(this.k.send)});wt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.ut(t.init,this.S.tt);this.S={Z:1,vt:t.init,dt:s},this.ct(s,!0)}else this.S.vt=t.init,this.st.W(t.init),this.ct(this.st.V(t.init));this.gt.ft()}else this.bt(`Ignoring init after closing: ${JSON.stringify(t)}`)}yt(t){if(1!==this.S.Z)return void this.bt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.vt=this.O.update(this.S.vt,t.change),{K:i,R:e}=this.st.B(t.id);e||this.ct(this.st.V(s)),i?.q.forEach(t=>t(s))}Tt(t){if(1!==this.S.Z)return void this.bt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.st.B(t.id);s?(this.bt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ct(this.st.V(this.S.vt))):this.bt(`API sent error: ${t.error}`)}Et(){this.k.send("x"),this.Y?this.bt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",f)))}nt=t=>{if("X"===t.detail)return void this.Et();const s=JSON.parse(t.detail);"change"in s?this.yt(s):"init"in s?this.wt(s):"error"in s?this.Tt(s):this.bt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.Z&&t(this.S.dt)}removeStateListener(t){this.it.delete(t)}ct(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S.dt!==t){this.S.dt=t;for(const s of this.it)s(t)}}getState(){return 1===this.S.Z?this.S.dt:void 0}bt(t){this.dispatchEvent(n("warning",new Error(t)))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",t.detail))};rt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.gt.o(),this.it.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("disconnected",this.rt)}}const f={code:0,reason:"graceful shutdown"},w=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{a as AT_LEAST_ONCE,u as AT_MOST_ONCE,t as OnlineScheduler,g 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.1.0",
3
+ "version": "6.3.0",
4
4
  "description": "shared state management",
5
5
  "author": "David Evans",
6
6
  "license": "MIT",