shared-reducer 6.0.0 → 6.2.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 +43 -19
- package/backend/index.d.ts +6 -3
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/frontend/index.d.ts +6 -4
- package/frontend/index.js +1 -1
- package/frontend/index.mjs +1 -1
- package/package.json +3 -3
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,
|
|
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(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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(
|
|
424
|
-
|
|
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:
|
package/backend/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ interface WebsocketHandlerOptions<Arg0> {
|
|
|
108
108
|
pongTimeout?: number;
|
|
109
109
|
notFoundError?: Error;
|
|
110
110
|
setSoftCloseHandler?: (arg0: Arg0, handler: () => Promise<void>) => void;
|
|
111
|
-
onConnect?: (arg0: Arg0) => void;
|
|
111
|
+
onConnect?: (arg0: Arg0, id: string, permission: Permission<unknown, unknown>) => void;
|
|
112
112
|
onDisconnect?: (arg0: Arg0, reason: string, connectionDuration: number) => void;
|
|
113
113
|
onError?: (arg0: Arg0, error: unknown, context: string) => void;
|
|
114
114
|
}
|
|
@@ -121,8 +121,11 @@ declare class WebsocketHandlerFactory<T, SpecT> {
|
|
|
121
121
|
declare const UniqueIdProvider: () => () => string;
|
|
122
122
|
|
|
123
123
|
interface Collection<T> {
|
|
124
|
-
|
|
125
|
-
|
|
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>;
|
|
126
129
|
}
|
|
127
130
|
type ErrorMapper = (e: unknown) => unknown;
|
|
128
131
|
declare class CollectionStorageModel<T extends object, K extends keyof T & string> implements Model<T[K], T> {
|
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.
|
|
1
|
+
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;u=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.u.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.u.set(t,e),r=e}return r.push(e)}}class n{l;p=new Map;constructor(t){this.l=t}async add(t,e){let r=this.p.get(t);r||(r=this.l(t),this.p.set(t,r)),await r.add(e)}async remove(t,e){const r=this.p.get(t);if(r){await r.remove(e)||this.p.delete(t)}}async broadcast(t,e){const r=this.p.get(t);r&&await r.broadcast(e)}}class i{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach(e=>e(t))}}const a={validateWrite(){}};class o extends Error{}class c extends Error{}const h=0,u=1,l=2,d=3,w=new Error("not found"),p=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),f=t=>t;const y="Cannot modify data",m={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{_;v;m;S;O;constructor(t,r,a={}){this._=t,this.v=r,this.m=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.J(t.message,t.meta):t.message.change&&r.J(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this._.read(t);null!=e&&(r={C:1,N:e,t:[]},await this.m.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.m.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.N},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,J:t},e.forEach(n)},send:(r,n)=>this.T(t,r,e,s,n),close:async()=>{await this.m.remove(t,n)}}}update(t,e,r=a){return this.T(t,e,r,null,void 0)}async I(t,e,r,s,n){try{const s=await this._.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const n=this.v.update(s,e),i=this._.validate(n);r.validateWrite(i,s),await this._.write(t,i,s)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:n})}this.m.broadcast(t,{message:{change:e},source:s,meta:n})}async T(t,e,r,s,n){return this.S.push(t,()=>this.I(t,e,r,s,n))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;D;validate;$;q;constructor(t,e,r,s={}){this.j=t,this.D=e,this.validate=r,this.$=s.readErrorIntercept??f,this.q=s.writeErrorIntercept??f}async read(t){try{return await this.j.where(this.D,t).get()}catch(t){throw this.$(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.D,t).update(s)}catch(t){throw this.q(t)}}},exports.InMemoryModel=class{read=this.get;validate;P=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.P.set(t,e)}get(t){return this.P.get(t)}delete(t){this.P.delete(t)}write(t,e,r){if(r!==this.P.get(t))throw new Error("Unexpected previous value");this.P.set(t,e)}},exports.InMemoryTopic=i,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=m,exports.ReadWrite=a,exports.ReadWriteStruct=class{W;constructor(t=[]){this.W=t}validateWrite(t,e){for(const r of this.W){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}},exports.TaskQueueMap=s,exports.TrackingTopicMap=n,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=w,setSoftCloseHandler:i,onConnect:a,onDisconnect:f,onError:y=p}){return async(...w)=>{const p=[];let m;try{const{id:g,permission:_}=await t(...w),x=await this.broadcaster.subscribe(g,_);if(!x)throw n;p.push(()=>x.close());let v,b=h,S="connection failed";const O=new Promise(t=>{v=e=>{v=()=>{},S=e,t()}}),E=await e(...w);w.length=1,i?.(w[0],()=>(b===h&&(b=u,E.send("X"),m||(m=setTimeout(J,s))),O));const C=Date.now();a?.(w[0],g,_),p.push(()=>f?.(w[0],S,Date.now()-C)),E.on("close",()=>{clearTimeout(m),b=l,v("client disconnect")});const J=()=>{E.terminate(),b=d,v("connection lost")},N=()=>{E.ping(),clearTimeout(m),m=setTimeout(J,s)},T=()=>{clearTimeout(m),m=setTimeout(N,r)};E.on("pong",T),E.on("message",async(t,e)=>{if(T(),e)return E.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return E.send("p");if("x"===r)return b!==u&&b!==d?E.send(JSON.stringify({error:"Unexpected close ack message"})):(b=l,v("clean shutdown"),E.close());if(b===l)return E.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");return{change:e.change,id:e.id}}return{change:e.change}}(r);await x.send(t.change,t.id)}catch(t){t instanceof o||t instanceof c?E.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),E.send(JSON.stringify({error:"Internal error"})))}}),b===h&&(E.send(JSON.stringify({init:x.getInitialData()})),x.listen((t,e)=>E.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await O}finally{clearTimeout(m);for(const t of p.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
|
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.
|
|
1
|
+
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;u=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.u.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.u.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.u.set(t,e),r=e}return r.push(e)}}class n{l;m=new Map;constructor(t){this.l=t}async add(t,e){let r=this.m.get(t);r||(r=this.l(t),this.m.set(t,r)),await r.add(e)}async remove(t,e){const r=this.m.get(t);if(r){await r.remove(e)||this.m.delete(t)}}async broadcast(t,e){const r=this.m.get(t);r&&await r.broadcast(e)}}class i{p=new Set;add(t){this.p.add(t)}remove(t){return this.p.delete(t),this.p.size>0}broadcast(t){this.p.forEach(e=>e(t))}}const a={validateWrite(){}};class o{_;v;p;S;O;constructor(t,r,a={}){this._=t,this.v=r,this.p=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.J(t.message,t.meta):t.message.change&&r.J(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this._.read(t);null!=e&&(r={C:1,N:e,t:[]},await this.p.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.p.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.N},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,J:t},e.forEach(n)},send:(r,n)=>this.T(t,r,e,s,n),close:async()=>{await this.p.remove(t,n)}}}update(t,e,r=a){return this.T(t,e,r,null,void 0)}async I(t,e,r,s,n){try{const s=await this._.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const n=this.v.update(s,e),i=this._.validate(n);r.validateWrite(i,s),await this._.write(t,i,s)}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:n})}this.p.broadcast(t,{message:{change:e},source:s,meta:n})}async T(t,e,r,s,n){return this.S.push(t,()=>this.I(t,e,r,s,n))}}class c extends Error{}class h extends Error{}const u="P",l="p",d="X",w="x";class f{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=_,setSoftCloseHandler:i,onConnect:a,onDisconnect:o,onError:u=v}){return async(...l)=>{const d=[];let w;try{const{id:f,permission:_}=await t(...l),v=await this.broadcaster.subscribe(f,_);if(!v)throw n;d.push(()=>v.close());let b,S=y,O="connection failed";const E=new Promise(t=>{b=e=>{b=()=>{},O=e,t()}}),C=await e(...l);l.length=1,i?.(l[0],()=>(S===y&&(S=m,C.send("X"),w||(w=setTimeout(J,s))),E));const x=Date.now();a?.(l[0],f,_),d.push(()=>o?.(l[0],O,Date.now()-x)),C.on("close",()=>{clearTimeout(w),S=p,b("client disconnect")});const J=()=>{C.terminate(),S=g,b("connection lost")},N=()=>{C.ping(),clearTimeout(w),w=setTimeout(J,s)},T=()=>{clearTimeout(w),w=setTimeout(N,r)};C.on("pong",T),C.on("message",async(t,e)=>{if(T(),e)return C.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return C.send("p");if("x"===r)return S!==m&&S!==g?C.send(JSON.stringify({error:"Unexpected close ack message"})):(S=p,b("clean shutdown"),C.close());if(S===p)return C.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new h("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new h("Must specify change and optional id");if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");return{change:e.change,id:e.id}}return{change:e.change}}(r);await v.send(t.change,t.id)}catch(t){t instanceof c||t instanceof h?C.send(JSON.stringify({error:t.message})):(u(l[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),S===y&&(C.send(JSON.stringify({init:v.getInitialData()})),v.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await E}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){u(l[0],t,"teardown")}}}}}const y=0,m=1,p=2,g=3,_=new Error("not found"),v=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class S{j;D;validate;$;P;constructor(t,e,r,s={}){this.j=t,this.D=e,this.validate=r,this.$=s.readErrorIntercept??b,this.P=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.D,t).get()}catch(t){throw this.$(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.D,t).update(s)}catch(t){throw this.P(t)}}}class O{read=this.get;validate;W=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.W.set(t,e)}get(t){return this.W.get(t)}delete(t){this.W.delete(t)}write(t,e,r){if(r!==this.W.get(t))throw new Error("Unexpected previous value");this.W.set(t,e)}}const E="Cannot modify data",C={validateWriteSpec(){throw new c(E)},validateWrite(){throw new c(E)}};class x{k;constructor(t=[]){this.k=t}validateWrite(t,e){for(const r of this.k){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new c(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new c(`Cannot edit field ${String(r)}`)}}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,S as CollectionStorageModel,O as InMemoryModel,i as InMemoryTopic,u as PING,l as PONG,c as PermissionError,C as ReadOnly,a as ReadWrite,x as ReadWriteStruct,s as TaskQueueMap,n as TrackingTopicMap,e as UniqueIdProvider,f as WebsocketHandlerFactory};
|
package/frontend/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ declare class OnlineScheduler implements Scheduler {
|
|
|
31
31
|
private _attempts;
|
|
32
32
|
constructor(_delayGetter: DelayGetter, _connectTimeLimit: number);
|
|
33
33
|
trigger(handler: Handler, errorHandler: ErrorHandler): void;
|
|
34
|
-
schedule(handler: Handler, errorHandler: ErrorHandler): void;
|
|
34
|
+
schedule(handler: Handler, errorHandler: ErrorHandler, reset?: boolean): void;
|
|
35
35
|
stop(): void;
|
|
36
36
|
private _attempt;
|
|
37
37
|
private _remove;
|
|
@@ -60,7 +60,6 @@ interface ConnectionInfo {
|
|
|
60
60
|
url: string;
|
|
61
61
|
token?: string | undefined;
|
|
62
62
|
}
|
|
63
|
-
type ConnectionGetter = (signal: AbortSignal) => MaybePromise<ConnectionInfo>;
|
|
64
63
|
interface DisconnectDetail {
|
|
65
64
|
code: number;
|
|
66
65
|
reason: string;
|
|
@@ -77,6 +76,7 @@ interface SharedReducerOptions<T, SpecT> {
|
|
|
77
76
|
type SharedReducerEvents = {
|
|
78
77
|
connected: CustomEvent<void>;
|
|
79
78
|
disconnected: CustomEvent<DisconnectDetail>;
|
|
79
|
+
rejected: CustomEvent<DisconnectDetail>;
|
|
80
80
|
warning: CustomEvent<Error>;
|
|
81
81
|
};
|
|
82
82
|
declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEvents> {
|
|
@@ -87,7 +87,8 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
87
87
|
private readonly _tracker;
|
|
88
88
|
private readonly _listeners;
|
|
89
89
|
private readonly _dispatchLock;
|
|
90
|
-
constructor(_context: Context<T, SpecT>,
|
|
90
|
+
constructor(_context: Context<T, SpecT>, connectionInfo: ConnectionInfo, { scheduler, deliveryStrategy, }?: SharedReducerOptions<T, SpecT>);
|
|
91
|
+
reconnect(connectionInfo?: ConnectionInfo): void;
|
|
91
92
|
readonly dispatch: Dispatch<T, SpecT>;
|
|
92
93
|
private _apply;
|
|
93
94
|
private readonly _share;
|
|
@@ -103,9 +104,10 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
103
104
|
private _warn;
|
|
104
105
|
private readonly _handleConnected;
|
|
105
106
|
private readonly _handleConnectionFailure;
|
|
107
|
+
private readonly _handleRejected;
|
|
106
108
|
private readonly _handleDisconnected;
|
|
107
109
|
close(): void;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
|
|
111
|
-
export type { Context, DeliveryStrategy, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
|
|
113
|
+
export type { 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([
|
|
1
|
+
"use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{j;I;k=null;P=!1;constructor(t,s){super(),this.j=t,this.I=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.I.trigger(this.L,this.M)}reconnect(t){t&&(this.j=t),this.k?this.k.close():this.P||this.I.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.j;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.k=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.I.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.k=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.P=!0,this.I.stop(),this.k?.close()}}const o="P",r="p",l=2e4,c=()=>!0;class a{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t){this.D.push({F:void 0,q:t,G:[],H:[]})}R(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.G.push(s??u),t.H.push(e??u)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}W(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.G.length>0||e.H.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,id:s.F})))}X(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{B:null,K:!1}:{B:this.D.splice(s,1)[0],K:0===s}}V(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}const u=()=>null;const d={code:0,reason:"graceful shutdown"},_=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=c,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{A;k;Y=!0;S={Z:0,tt:[]};st;et=new Set;it=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(_,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.st=new a(s,n),this.k=new h(e,i),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("rejected",this.rt),this.k.addEventListener("disconnected",this.lt)}reconnect(t){this.k.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((e,i)=>t(s,e,t=>i(new Error(t))))})}((t,s,e)=>{if(!t.length&&!s&&!e)return;const i={ct:t,G:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ut(this.dt(this.S._t,[i])),this.ft.gt()}});dt(t,s){return this.it(()=>{for(const{ct:i,G:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.A,t,i);t=s,this.st.N(n)}this.st.R(t,n,h)}return t})}ft=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{wt:i,gt:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.W(this.k.send)});vt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.dt(t.init,this.S.tt);this.S={Z:1,bt:t.init,_t:s},this.ut(s,!0)}else this.S.bt=t.init,this.st.U(t.init),this.ut(this.st.V(t.init));this.ft.wt()}else this.yt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Tt(t){if(1!==this.S.Z)return void this.yt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.bt=this.A.update(this.S.bt,t.change),{B:e,K:i}=this.st.X(t.id);i||this.ut(this.st.V(s)),e?.G.forEach(t=>t(s))}Et(t){if(1!==this.S.Z)return void this.yt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.st.X(t.id);s?(this.yt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ut(this.st.V(this.S.bt))):this.yt(`API sent error: ${t.error}`)}xt(){this.k.send("x"),this.Y?this.yt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:d})))}nt=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.Tt(s):"init"in s?this.vt(s):"error"in s?this.Et(s):this.yt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S._t)}removeStateListener(t){this.et.delete(t)}ut(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S._t!==t){this.S._t=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S._t:void 0}yt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};rt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};lt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.ft.o(),this.et.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("rejected",this.rt),this.k.removeEventListener("disconnected",this.lt)}},exports.exponentialDelay=s;
|
package/frontend/index.mjs
CHANGED
|
@@ -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
|
|
1
|
+
class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{j;I;k=null;P=!1;constructor(t,s){super(),this.j=t,this.I=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.I.trigger(this.L,this.M)}reconnect(t){t&&(this.j=t),this.k?this.k.close():this.P||this.I.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.j;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.k=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.I.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.k=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.P=!0,this.I.stop(),this.k?.close()}}const o="P",r="p",l=2e4,c=()=>!0,a=(t,s,e)=>!e;class u{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t){this.D.push({F:void 0,q:t,G:[],H:[]})}R(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.G.push(s??d),t.H.push(e??d)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}W(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.G.length>0||e.H.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,id:s.F})))}X(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{B:null,K:!1}:{B:this.D.splice(s,1)[0],K:0===s}}V(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}const d=()=>null;class _ extends i{A;k;Y=!0;S={Z:0,tt:[]};st;et=new Set;it=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(f,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.st=new u(s,n),this.k=new h(e,i),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("rejected",this.rt),this.k.addEventListener("disconnected",this.lt)}reconnect(t){this.k.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((e,i)=>t(s,e,t=>i(new Error(t))))})}((t,s,e)=>{if(!t.length&&!s&&!e)return;const i={ct:t,G:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ut(this.dt(this.S._t,[i])),this.ft.gt()}});dt(t,s){return this.it(()=>{for(const{ct:i,G:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.A,t,i);t=s,this.st.N(n)}this.st.R(t,n,h)}return t})}ft=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{wt:i,gt:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.W(this.k.send)});vt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.dt(t.init,this.S.tt);this.S={Z:1,bt:t.init,_t:s},this.ut(s,!0)}else this.S.bt=t.init,this.st.U(t.init),this.ut(this.st.V(t.init));this.ft.wt()}else this.yt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Tt(t){if(1!==this.S.Z)return void this.yt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.bt=this.A.update(this.S.bt,t.change),{B:e,K:i}=this.st.X(t.id);i||this.ut(this.st.V(s)),e?.G.forEach(t=>t(s))}Et(t){if(1!==this.S.Z)return void this.yt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.st.X(t.id);s?(this.yt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ut(this.st.V(this.S.bt))):this.yt(`API sent error: ${t.error}`)}Ct(){this.k.send("x"),this.Y?this.yt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:g})))}nt=t=>{if("X"===t.detail)return void this.Ct();const s=JSON.parse(t.detail);"change"in s?this.Tt(s):"init"in s?this.vt(s):"error"in s?this.Et(s):this.yt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S._t)}removeStateListener(t){this.et.delete(t)}ut(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S._t!==t){this.S._t=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S._t:void 0}yt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};rt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};lt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.ft.o(),this.et.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("rejected",this.rt),this.k.removeEventListener("disconnected",this.lt)}}const g={code:0,reason:"graceful shutdown"},f=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{c as AT_LEAST_ONCE,a as AT_MOST_ONCE,t as OnlineScheduler,_ as SharedReducer,s as exponentialDelay};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shared-reducer",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "shared state management",
|
|
5
5
|
"author": "David Evans",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rollup/plugin-terser": "0.4.x",
|
|
48
48
|
"@rollup/plugin-typescript": "12.x",
|
|
49
|
-
"collection-storage": "
|
|
49
|
+
"collection-storage": "4.x",
|
|
50
50
|
"json-immutability-helper": "4.x",
|
|
51
51
|
"lean-test": "2.x",
|
|
52
52
|
"prettier": "3.6.2",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"rollup-plugin-dts": "6.x",
|
|
55
55
|
"superwstest": "2.x",
|
|
56
56
|
"typescript": "5.9.x",
|
|
57
|
-
"web-listener": "0.
|
|
57
|
+
"web-listener": "0.17.1",
|
|
58
58
|
"ws": "8.x"
|
|
59
59
|
}
|
|
60
60
|
}
|