shared-reducer 5.1.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,8 +15,53 @@ npm install --save shared-reducer json-immutability-helper
15
15
 
16
16
  ## Usage (Backend)
17
17
 
18
- This project is compatible with [websocket-express](https://github.com/davidje13/websocket-express),
19
- but can also be used in isolation.
18
+ This project is compatible with [web-listener](https://github.com/davidje13/web-listener) and
19
+ [websocket-express](https://github.com/davidje13/websocket-express), but can also be used in
20
+ isolation.
21
+
22
+ ### With web-listener
23
+
24
+ ```javascript
25
+ import {
26
+ Broadcaster,
27
+ WebsocketHandlerFactory,
28
+ InMemoryModel,
29
+ ReadWrite,
30
+ } from 'shared-reducer/backend';
31
+ import context from 'json-immutability-helper';
32
+ import { WebListener, Router, makeAcceptWebSocket, setSoftCloseHandler } from 'web-listener';
33
+ import { WebSocketServer } from 'ws';
34
+
35
+ const model = new InMemoryModel();
36
+ const broadcaster = new Broadcaster(model, context);
37
+ model.set('a', { foo: 'v1' });
38
+
39
+ const acceptWebSocket = makeAcceptWebSocket(WebSocketServer);
40
+ const handlerFactory = new WebsocketHandlerFactory(broadcaster);
41
+
42
+ const router = new Router();
43
+ router.ws(
44
+ '/:id',
45
+ handlerFactory.handler({
46
+ accessGetter: (req) => ({
47
+ id: getPathParameter(req, 'id'),
48
+ permission: ReadWrite,
49
+ }),
50
+ acceptWebSocket,
51
+ setSoftCloseHandler,
52
+ }),
53
+ );
54
+
55
+ const server = new WebListener(router).listen(0, 'localhost');
56
+
57
+ // later, to shutdown gracefully:
58
+ // send a close signal to all clients and wait up to 1 second for acknowledgement:
59
+ await server.closeWithTimeout('shutdown', 1000);
60
+ ```
61
+
62
+ For real use-cases, you will probably want to add authentication middleware to the router chain,
63
+ and you may want to give some users read-only and others read-write access, which can be achieved in
64
+ the `accessGetter` lambda.
20
65
 
21
66
  ### With websocket-express
22
67
 
@@ -34,23 +79,35 @@ const model = new InMemoryModel();
34
79
  const broadcaster = new Broadcaster(model, context);
35
80
  model.set('a', { foo: 'v1' });
36
81
 
37
- const app = new WebSocketExpress();
38
- const server = app.listen(0, 'localhost');
39
-
40
82
  const handlerFactory = new WebsocketHandlerFactory(broadcaster);
41
- app.ws('/:id', handlerFactory.handler((req) => req.params.id, () => ReadWrite));
83
+ const softClosers = new Map();
42
84
 
43
- const server = app.listen();
85
+ const app = new WebSocketExpress();
86
+ app.ws(
87
+ '/:id',
88
+ handlerFactory.handler({
89
+ accessGetter: (req) => ({ id: req.params.id, permission: ReadWrite }),
90
+ acceptWebSocket: (_, res) => res.accept(),
91
+ setSoftCloseHandler: (req, fn) => softClosers.set(req, fn),
92
+ onDisconnect: (req) => softClosers.delete(req),
93
+ }),
94
+ );
95
+
96
+ const server = app.listen(0, 'localhost');
44
97
 
45
98
  // later, to shutdown gracefully:
46
- // send a close signal to all clients and wait up to 1 second for acknowledgement:
47
- await handlerFactory.close(1000);
48
- server.close();
99
+ server.close(); // stop accepting new connections
100
+ // send a close signal to all active clients:
101
+ for (const fn of softClosers.values()) {
102
+ fn();
103
+ }
104
+ // force-close connections after a time:
105
+ setTimeout(() => server.closeAllConnections(), 1000);
49
106
  ```
50
107
 
51
108
  For real use-cases, you will probably want to add authentication middleware to the expressjs chain,
52
109
  and you may want to give some users read-only and others read-write access, which can be achieved in
53
- the second lambda.
110
+ the `accessGetter` lambda.
54
111
 
55
112
  ### Alone
56
113
 
@@ -67,7 +124,9 @@ model.set('a', { foo: 'v1' });
67
124
  const subscription = await broadcaster.subscribe('a');
68
125
 
69
126
  const begin = subscription.getInitialData();
70
- subscription.listen((change, meta) => { /*...*/ });
127
+ subscription.listen((change, meta) => {
128
+ // ...
129
+ });
71
130
  await subscription.send(['=', { foo: 'v2' }]);
72
131
  // callback provided earlier is invoked
73
132
 
@@ -128,9 +187,7 @@ reducer.addEventListener('warning', (e) => {
128
187
 
129
188
  const dispatch = reducer.dispatch;
130
189
 
131
- dispatch([
132
- { a: ['=', 8] },
133
- ]);
190
+ dispatch([{ a: ['=', 8] }]);
134
191
 
135
192
  dispatch([
136
193
  (state) => {
@@ -151,10 +208,7 @@ dispatch(
151
208
  (message) => console.warn('failed to sync', message),
152
209
  );
153
210
 
154
- dispatch([
155
- { a: ['add', 1] },
156
- { a: ['add', 1] },
157
- ]);
211
+ dispatch([{ a: ['add', 1] }, { a: ['add', 1] }]);
158
212
  ```
159
213
 
160
214
  ### Specs
@@ -226,10 +280,7 @@ import listCommands from 'json-immutability-helper/commands/list';
226
280
  import mathCommands from 'json-immutability-helper/commands/math';
227
281
  import context from 'json-immutability-helper';
228
282
 
229
- const broadcaster = new Broadcaster(
230
- new InMemoryModel(),
231
- context.with(listCommands, mathCommands),
232
- );
283
+ const broadcaster = new Broadcaster(new InMemoryModel(), context.with(listCommands, mathCommands));
233
284
  ```
234
285
 
235
286
  ```javascript
@@ -239,10 +290,9 @@ import listCommands from 'json-immutability-helper/commands/list';
239
290
  import mathCommands from 'json-immutability-helper/commands/math';
240
291
  import context from 'json-immutability-helper';
241
292
 
242
- const reducer = new SharedReducer(
243
- context.with(listCommands, mathCommands),
244
- () => ({ url: 'ws://destination' }),
245
- );
293
+ const reducer = new SharedReducer(context.with(listCommands, mathCommands), () => ({
294
+ url: 'ws://destination',
295
+ }));
246
296
  ```
247
297
 
248
298
  If you want to use an entirely different reducer, create a wrapper:
@@ -281,7 +331,7 @@ new Broadcaster(model, reducer[, options]);
281
331
  ```
282
332
 
283
333
  - `options.subscribers`: specify a custom keyed broadcaster, used for communicating changes to all
284
- consumers. Required interface:
334
+ consumers. Required interface:
285
335
 
286
336
  ```javascript
287
337
  {
@@ -91,40 +91,41 @@ interface ServerWebSocket {
91
91
  on(event: 'pong', listener: () => void): void;
92
92
  ping(): void;
93
93
  send(message: string): void;
94
+ close(): void;
94
95
  terminate(): void;
95
96
  }
96
- interface WSResponse {
97
- accept(): Promise<ServerWebSocket>;
98
- sendError(httpStatus: number, wsStatus?: number): void;
99
- beginTransaction(): void;
100
- endTransaction(): void;
97
+ interface Access<T, SpecT> {
98
+ id: string;
99
+ permission: Permission<T, SpecT>;
101
100
  }
102
- interface WebsocketHandlerOptions {
101
+ type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
102
+ interface WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> {
103
+ accessGetter: AccessGetter;
104
+ acceptWebSocket: AcceptWebSocket;
105
+ }
106
+ interface WebsocketHandlerOptions<Arg0> {
103
107
  pingInterval?: number;
104
108
  pongTimeout?: number;
105
- }
106
- interface HandlerCallbacks<Req> {
107
- onConnect?: (req: Req) => void;
108
- onDisconnect?: (req: Req, reason: string, connectionDuration: number) => void;
109
- onError?: (req: Req, context: string, error: unknown) => void;
109
+ notFoundError?: Error;
110
+ setSoftCloseHandler?: (arg0: Arg0, handler: () => Promise<void>) => void;
111
+ onConnect?: (arg0: Arg0) => void;
112
+ onDisconnect?: (arg0: Arg0, reason: string, connectionDuration: number) => void;
113
+ onError?: (arg0: Arg0, error: unknown, context: string) => void;
110
114
  }
111
115
  declare class WebsocketHandlerFactory<T, SpecT> {
112
116
  private readonly broadcaster;
113
- private readonly closers;
114
- private readonly _pingInterval;
115
- private readonly _pongTimeout;
116
- private closing;
117
- constructor(broadcaster: Broadcaster<T, SpecT>, options?: WebsocketHandlerOptions);
118
- activeConnections(): number;
119
- softClose(timeout: number): Promise<void>;
120
- handler<Req, Res extends WSResponse>(idGetter: (req: Req, res: Res) => MaybePromise<string>, permissionGetter: (req: Req, res: Res) => MaybePromise<Permission<T, SpecT>>, { onConnect, onDisconnect, onError }?: HandlerCallbacks<Req>): (req: Req, res: Res) => Promise<void>;
117
+ constructor(broadcaster: Broadcaster<T, SpecT>);
118
+ handler<Args extends any[], AccessGetter extends (...args: Args) => MaybePromise<Access<T, SpecT>>, AcceptWebSocket extends (...args: Args) => MaybePromise<ServerWebSocket>>({ accessGetter, acceptWebSocket, pingInterval, pongTimeout, notFoundError, setSoftCloseHandler, onConnect, onDisconnect, onError, }: WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> & WebsocketHandlerOptions<First<Args>>): (...args: Args) => Promise<void>;
121
119
  }
122
120
 
123
121
  declare const UniqueIdProvider: () => () => string;
124
122
 
125
123
  interface Collection<T> {
126
- get<K extends keyof T & string>(searchAttribute: K, searchValue: T[K]): Promise<Readonly<T> | null>;
127
- update<K extends keyof T & string>(searchAttribute: K, searchValue: T[K], update: Partial<T>): Promise<void>;
124
+ where<K extends string & keyof T>(attribute: K, value: T[K]): Filtered<T>;
125
+ }
126
+ interface Filtered<T> {
127
+ get(): Promise<Readonly<T> | null>;
128
+ update(delta: Partial<T>): Promise<void>;
128
129
  }
129
130
  type ErrorMapper = (e: unknown) => unknown;
130
131
  declare class CollectionStorageModel<T extends object, K extends keyof T & string> implements Model<T[K], T> {
@@ -186,4 +187,5 @@ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
186
187
  broadcast(key: K, message: T): Promise<void>;
187
188
  }
188
189
 
189
- export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, type ChangeInfo, CollectionStorageModel, type Context, InMemoryModel, InMemoryTopic, type Model, PING, PONG, type Permission, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, type Subscription, type Task, type TaskQueue, type TaskQueueFactory, TaskQueueMap, type Topic, type TopicMap, type TopicMessage, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
190
+ export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, CollectionStorageModel, InMemoryModel, InMemoryTopic, PING, PONG, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, TaskQueueMap, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
191
+ export type { ChangeInfo, Context, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
package/backend/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;u=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.u.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.u.set(t,e),s=e}return s.push(e)}}class i{l;p=new Map;constructor(t){this.l=t}async add(t,e){let s=this.p.get(t);s||(s=this.l(t),this.p.set(t,s)),await s.add(e)}async remove(t,e){const s=this.p.get(t);if(s){await s.remove(e)||this.p.delete(t)}}async broadcast(t,e){const s=this.p.get(t);s&&await s.broadcast(e)}}class n{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach((e=>e(t)))}}const o={validateWrite(){}};const a=(t,e,s)=>console.warn(`shared-reducer: ${e}`,s),c=t=>t;class h extends Error{}const u="Cannot modify data",l={validateWriteSpec(){throw new h(u)},validateWrite(){throw new h(u)}};exports.AsyncTaskQueue=s,exports.Broadcaster=class{v;_;m;C;T;constructor(t,s,o={}){this.v=t,this._=s,this.m=o.subscribers??new i((()=>new n)),this.C=o.taskQueues??new r,this.T=o.idProvider??e()}async subscribe(t,e=o){let s={S:0},r="";const i=t=>{2===s.S?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.S&&s.t.push(t)};try{if(await this.C.push(t,(async()=>{const e=await this.v.read(t);null!=e&&(s={S:1,P:e,t:[]},await this.m.add(t,i))})),0===s.S)return null;r=await this.T()}catch(e){throw await this.m.remove(t,i),e}return{getInitialData(){if(1!==s.S)throw new Error("Already started");return s.P},listen(t){if(1!==s.S)throw new Error("Already started");const e=s.t;s={S:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.m.remove(t,i)}}}update(t,e,s=o){return this.j(t,e,s,null,void 0)}async D(t,e,s,r,i){try{const r=await this.v.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this._.update(r,e),n=this.v.validate(i);s.validateWrite(n,r),await this.v.write(t,n,r)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.m.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.C.push(t,(()=>this.D(t,e,s,r,i)))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{I;$;validate;k;q;constructor(t,e,s,r={}){this.I=t,this.$=e,this.validate=s,this.k=r.readErrorIntercept??c,this.q=r.writeErrorIntercept??c}async read(t){try{return await this.I.get(this.$,t)}catch(t){throw this.k(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.I.update(this.$,t,r)}catch(t){throw this.q(t)}}},exports.InMemoryModel=class{read=this.get;validate;A=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.A.set(t,e)}get(t){return this.A.get(t)}delete(t){this.A.delete(t)}write(t,e,s){if(s!==this.A.get(t))throw new Error("Unexpected previous value");this.A.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=h,exports.ReadOnly=l,exports.ReadWrite=o,exports.ReadWriteStruct=class{J;constructor(t=[]){this.J=t}validateWrite(t,e){for(const s of this.J){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new h(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new h(`Cannot edit field ${String(s)}`)}}},exports.TaskQueueMap=r,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;closers=new Set;M;N;closing=!1;constructor(t,e={}){this.broadcaster=t,this.M=e.pingInterval??25e3,this.N=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e,{onConnect:s,onDisconnect:r,onError:i=a}={}){const n=async(s,r)=>{const i=await t(s,r),n=await e(s,r),o=await this.broadcaster.subscribe(i,n);return o||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const o=(e,s)=>{try{i(t,e,s)}catch{}},a=await n(t,e).catch((t=>(o("handshake",t),e.sendError(500),null)));if(!a)return;const c=Date.now(),h=e=>{const s=Date.now()-c;try{r?.(t,e,s)}catch(t){o("disconnect hook",t)}a.close().catch((()=>null))};try{const r=await e.accept();s?.(t);let i=0,n=()=>null;const o=()=>(this.closers.delete(o),r.send("X"),i=1,new Promise((t=>{n=t})));r.on("close",(()=>{i=2,clearTimeout(l),h("disconnect"),this.closers.delete(o),n()}));const c=()=>{h("lost"),this.closers.delete(o),r.terminate(),n()},u=()=>{r.ping(),clearTimeout(l),l=setTimeout(c,this.N)};if(r.on("pong",(()=>{clearTimeout(l),l=setTimeout(u,this.M)})),r.on("message",(async(t,s)=>{clearTimeout(l),l=setTimeout(u,this.M);try{if(s)throw new Error("Binary messages are not supported");const o=String(t);if("P"===o)return void r.send("p");if("x"===o){if(1!==i)throw new Error("Unexpected close ack message");return i=2,void n()}if(2===i)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(o);e.beginTransaction();try{await a.send(c.change,c.id)}finally{e.endTransaction()}}catch(t){r.send(JSON.stringify({error:t instanceof Error?t.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void h("server shutdown");r.send(JSON.stringify({init:a.getInitialData()})),a.listen(((t,e)=>{const s=void 0!==e?{id:e,...t}:t;r.send(JSON.stringify(s))}));let l=setTimeout(u,this.M);this.closers.add(o)}catch(t){o("communication",t),e.sendError(500),h("error")}}}};
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")}}}}};
package/backend/index.mjs CHANGED
@@ -1 +1 @@
1
- import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;u=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.u.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.u.set(t,e),s=e}return s.push(e)}}class i{l;m=new Map;constructor(t){this.l=t}async add(t,e){let s=this.m.get(t);s||(s=this.l(t),this.m.set(t,s)),await s.add(e)}async remove(t,e){const s=this.m.get(t);if(s){await s.remove(e)||this.m.delete(t)}}async broadcast(t,e){const s=this.m.get(t);s&&await s.broadcast(e)}}class n{p=new Set;add(t){this.p.add(t)}remove(t){return this.p.delete(t),this.p.size>0}broadcast(t){this.p.forEach((e=>e(t)))}}const a={validateWrite(){}};class o{v;_;p;C;T;constructor(t,s,a={}){this.v=t,this._=s,this.p=a.subscribers??new i((()=>new n)),this.C=a.taskQueues??new r,this.T=a.idProvider??e()}async subscribe(t,e=a){let s={S:0},r="";const i=t=>{2===s.S?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.S&&s.t.push(t)};try{if(await this.C.push(t,(async()=>{const e=await this.v.read(t);null!=e&&(s={S:1,P:e,t:[]},await this.p.add(t,i))})),0===s.S)return null;r=await this.T()}catch(e){throw await this.p.remove(t,i),e}return{getInitialData(){if(1!==s.S)throw new Error("Already started");return s.P},listen(t){if(1!==s.S)throw new Error("Already started");const e=s.t;s={S:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.p.remove(t,i)}}}update(t,e,s=a){return this.j(t,e,s,null,void 0)}async D(t,e,s,r,i){try{const r=await this.v.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this._.update(r,e),n=this.v.validate(i);s.validateWrite(n,r),await this.v.write(t,n,r)}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.p.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.C.push(t,(()=>this.D(t,e,s,r,i)))}}const c="P",h="p",u="X",l="x";class d{broadcaster;closers=new Set;I;$;closing=!1;constructor(t,e={}){this.broadcaster=t,this.I=e.pingInterval??25e3,this.$=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e,{onConnect:s,onDisconnect:r,onError:i=w}={}){const n=async(s,r)=>{const i=await t(s,r),n=await e(s,r),a=await this.broadcaster.subscribe(i,n);return a||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const a=(e,s)=>{try{i(t,e,s)}catch{}},o=await n(t,e).catch((t=>(a("handshake",t),e.sendError(500),null)));if(!o)return;const c=Date.now(),h=e=>{const s=Date.now()-c;try{r?.(t,e,s)}catch(t){a("disconnect hook",t)}o.close().catch((()=>null))};try{const r=await e.accept();s?.(t);let i=0,n=()=>null;const a=()=>(this.closers.delete(a),r.send("X"),i=1,new Promise((t=>{n=t})));r.on("close",(()=>{i=2,clearTimeout(l),h("disconnect"),this.closers.delete(a),n()}));const c=()=>{h("lost"),this.closers.delete(a),r.terminate(),n()},u=()=>{r.ping(),clearTimeout(l),l=setTimeout(c,this.$)};if(r.on("pong",(()=>{clearTimeout(l),l=setTimeout(u,this.I)})),r.on("message",(async(t,s)=>{clearTimeout(l),l=setTimeout(u,this.I);try{if(s)throw new Error("Binary messages are not supported");const a=String(t);if("P"===a)return void r.send("p");if("x"===a){if(1!==i)throw new Error("Unexpected close ack message");return i=2,void n()}if(2===i)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(a);e.beginTransaction();try{await o.send(c.change,c.id)}finally{e.endTransaction()}}catch(t){r.send(JSON.stringify({error:t instanceof Error?t.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void h("server shutdown");r.send(JSON.stringify({init:o.getInitialData()})),o.listen(((t,e)=>{const s=void 0!==e?{id:e,...t}:t;r.send(JSON.stringify(s))}));let l=setTimeout(u,this.I);this.closers.add(a)}catch(t){a("communication",t),e.sendError(500),h("error")}}}}const w=(t,e,s)=>console.warn(`shared-reducer: ${e}`,s),m=t=>t;class y{k;q;validate;A;J;constructor(t,e,s,r={}){this.k=t,this.q=e,this.validate=s,this.A=r.readErrorIntercept??m,this.J=r.writeErrorIntercept??m}async read(t){try{return await this.k.get(this.q,t)}catch(t){throw this.A(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.k.update(this.q,t,r)}catch(t){throw this.J(t)}}}class f extends Error{}class p{read=this.get;validate;M=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.M.set(t,e)}get(t){return this.M.get(t)}delete(t){this.M.delete(t)}write(t,e,s){if(s!==this.M.get(t))throw new Error("Unexpected previous value");this.M.set(t,e)}}const g="Cannot modify data",v={validateWriteSpec(){throw new f(g)},validateWrite(){throw new f(g)}};class _{N;constructor(t=[]){this.N=t}validateWrite(t,e){for(const s of this.N){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new f(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new f(`Cannot edit field ${String(s)}`)}}}export{s as AsyncTaskQueue,o as Broadcaster,u as CLOSE,l as CLOSE_ACK,y as CollectionStorageModel,p as InMemoryModel,n as InMemoryTopic,c as PING,h as PONG,f as PermissionError,v as ReadOnly,a as ReadWrite,_ as ReadWriteStruct,r as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,d as WebsocketHandlerFactory};
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};
@@ -107,4 +107,5 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
107
107
  close(): void;
108
108
  }
109
109
 
110
- export { AT_LEAST_ONCE, AT_MOST_ONCE, type Context, type DeliveryStrategy, type Dispatch, type DispatchSpec, OnlineScheduler, type Scheduler, SharedReducer, type SharedReducerOptions, exponentialDelay };
110
+ export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
111
+ export type { Context, DeliveryStrategy, 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){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 +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){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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-reducer",
3
- "version": "5.1.0",
3
+ "version": "6.1.0",
4
4
  "description": "shared state management",
5
5
  "author": "David Evans",
6
6
  "license": "MIT",
@@ -46,15 +46,15 @@
46
46
  "devDependencies": {
47
47
  "@rollup/plugin-terser": "0.4.x",
48
48
  "@rollup/plugin-typescript": "12.x",
49
- "collection-storage": "3.x",
49
+ "collection-storage": "4.x",
50
50
  "json-immutability-helper": "4.x",
51
51
  "lean-test": "2.x",
52
- "prettier": "3.4.2",
52
+ "prettier": "3.6.2",
53
53
  "rollup": "4.x",
54
54
  "rollup-plugin-dts": "6.x",
55
55
  "superwstest": "2.x",
56
- "tslib": "2.8.x",
57
- "typescript": "5.7.x",
58
- "websocket-express": "3.x"
56
+ "typescript": "5.9.x",
57
+ "web-listener": "0.17.1",
58
+ "ws": "8.x"
59
59
  }
60
60
  }