shared-reducer 6.1.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -19
- package/backend/index.d.ts +12 -3
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/frontend/index.d.ts +20 -9
- package/frontend/index.js +1 -1
- package/frontend/index.mjs +1 -1
- package/package.json +1 -1
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
|
@@ -27,11 +27,14 @@ interface TopicMap<K, T> {
|
|
|
27
27
|
broadcast(key: K, message: T): MaybePromise<void>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type ChangeEvent = [string, ...unknown[]];
|
|
31
|
+
|
|
30
32
|
declare class PermissionError extends Error {
|
|
31
33
|
}
|
|
32
34
|
interface Permission<T, SpecT> {
|
|
33
35
|
validateWriteSpec?(spec: SpecT): void;
|
|
34
36
|
validateWrite(newValue: T, oldValue: T): void;
|
|
37
|
+
validateEvent(event: ChangeEvent): void;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
interface Model<ID, T> {
|
|
@@ -42,17 +45,19 @@ interface Model<ID, T> {
|
|
|
42
45
|
|
|
43
46
|
interface Context<T, SpecT> {
|
|
44
47
|
update: (input: T, spec: SpecT) => T;
|
|
48
|
+
isNoOp?: (spec: SpecT) => boolean;
|
|
45
49
|
}
|
|
46
50
|
type Listener<SpecT, MetaT> = (message: ChangeInfo<SpecT>, meta: MetaT | undefined) => void;
|
|
47
51
|
interface Subscription<T, SpecT, MetaT> {
|
|
48
52
|
getInitialData(): Readonly<T>;
|
|
49
53
|
listen(onChange: Listener<SpecT, MetaT>): void;
|
|
50
|
-
send(change: SpecT, meta?: MetaT): Promise<void>;
|
|
54
|
+
send(change: SpecT, events?: ChangeEvent[] | undefined, meta?: MetaT): Promise<void>;
|
|
51
55
|
close(): Promise<void>;
|
|
52
56
|
}
|
|
53
57
|
type Identifier = string | null;
|
|
54
58
|
type ChangeInfo<SpecT> = {
|
|
55
59
|
change: SpecT;
|
|
60
|
+
events: Readonly<Readonly<ChangeEvent>[]> | undefined;
|
|
56
61
|
error?: undefined;
|
|
57
62
|
} | {
|
|
58
63
|
change?: undefined;
|
|
@@ -76,7 +81,10 @@ declare class Broadcaster<T, SpecT> {
|
|
|
76
81
|
idProvider?: () => MaybePromise<string>;
|
|
77
82
|
});
|
|
78
83
|
subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>): Promise<Subscription<T, SpecT, MetaT> | null>;
|
|
79
|
-
update(id: ID, change: SpecT,
|
|
84
|
+
update(id: ID, change: SpecT, { events, permission, }?: {
|
|
85
|
+
events?: ChangeEvent[] | undefined;
|
|
86
|
+
permission?: Permission<T, SpecT>;
|
|
87
|
+
}): Promise<void>;
|
|
80
88
|
private _internalApplyChange;
|
|
81
89
|
private _internalQueueChange;
|
|
82
90
|
}
|
|
@@ -108,7 +116,7 @@ interface WebsocketHandlerOptions<Arg0> {
|
|
|
108
116
|
pongTimeout?: number;
|
|
109
117
|
notFoundError?: Error;
|
|
110
118
|
setSoftCloseHandler?: (arg0: Arg0, handler: () => Promise<void>) => void;
|
|
111
|
-
onConnect?: (arg0: Arg0) => void;
|
|
119
|
+
onConnect?: (arg0: Arg0, id: string, permission: Permission<unknown, unknown>) => void;
|
|
112
120
|
onDisconnect?: (arg0: Arg0, reason: string, connectionDuration: number) => void;
|
|
113
121
|
onError?: (arg0: Arg0, error: unknown, context: string) => void;
|
|
114
122
|
}
|
|
@@ -161,6 +169,7 @@ declare class ReadWriteStruct<T extends object> implements Permission<T, unknown
|
|
|
161
169
|
private readonly _readOnlyFields;
|
|
162
170
|
constructor(_readOnlyFields?: (keyof T)[]);
|
|
163
171
|
validateWrite(newValue: T, oldValue: T): void;
|
|
172
|
+
validateEvent(): void;
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
declare class AsyncTaskQueue extends EventTarget implements TaskQueue {
|
package/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;
|
|
1
|
+
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class n{u;p=new Map;constructor(t){this.u=t}async add(t,e){let r=this.p.get(t);r||(r=this.u(t),this.p.set(t,r)),await r.add(e)}async remove(t,e){const r=this.p.get(t);if(r){await r.remove(e)||this.p.delete(t)}}async broadcast(t,e){const r=this.p.get(t);r&&await r.broadcast(e)}}class i{v=new Set;add(t){this.v.add(t)}remove(t){return this.v.delete(t),this.v.size>0}broadcast(t){this.v.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o extends Error{}class c extends Error{}const h=0,l=1,u=2,d=3,w=new Error("not found"),f=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),p=t=>t;const y="Cannot modify data",v={validateWriteSpec(){throw new o(y)},validateWrite(){throw new o(y)},validateEvent(){throw new o(y)}};exports.AsyncTaskQueue=r,exports.Broadcaster=class{m;_;v;S;O;constructor(t,r,a={}){this.m=t,this._=r,this.v=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.I(t.message,t.meta):t.message.change&&r.I(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.m.read(t);null!=e&&(r={C:1,J:e,t:[]},await this.v.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.v.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.J},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,I:t},e.forEach(n)},send:(r,n,i)=>this.N(t,r,n,e,s,i),close:async()=>{await this.v.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,n,i){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.m.read(t);if(!r)throw new Error("Deleted");const n=this._.update(r,e),i=this.m.validate(n);s.validateWrite(i,r),await this.m.write(t,i,r)}}catch(e){return void this.v.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:n,meta:i})}0===r?.length&&(r=void 0),this.v.broadcast(t,{message:{change:e,events:r},source:n,meta:i})}async N(t,e,r,s,n,i){return this.S.push(t,()=>this.T(t,e,r,s,n,i))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??p,this.$=s.writeErrorIntercept??p}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}},exports.InMemoryModel=class{read=this.get;validate;q=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.q.set(t,e)}get(t){return this.q.get(t)}delete(t){this.q.delete(t)}write(t,e,r){if(r!==this.q.get(t))throw new Error("Unexpected previous value");this.q.set(t,e)}},exports.InMemoryTopic=i,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=v,exports.ReadWrite=a,exports.ReadWriteStruct=class{P;constructor(t=[]){this.P=t}validateWrite(t,e){for(const r of this.P){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}validateEvent(){}},exports.TaskQueueMap=s,exports.TrackingTopicMap=n,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=w,setSoftCloseHandler:i,onConnect:a,onDisconnect:p,onError:y=f}){return async(...w)=>{const f=[];let v;try{const{id:m,permission:g}=await t(...w),_=await this.broadcaster.subscribe(m,g);if(!_)throw n;f.push(()=>_.close());let x,b=h,E="connection failed";const S=new Promise(t=>{x=e=>{x=()=>{},E=e,t()}}),O=await e(...w);w.length=1,i?.(w[0],()=>(b===h&&(b=l,O.send("X"),v||(v=setTimeout(I,s))),S));const C=Date.now();a?.(w[0],m,g),f.push(()=>p?.(w[0],E,Date.now()-C)),O.on("close",()=>{clearTimeout(v),b=u,x("client disconnect")});const I=()=>{O.terminate(),b=d,x("connection lost")},J=()=>{O.ping(),clearTimeout(v),v=setTimeout(I,s)},N=()=>{clearTimeout(v),v=setTimeout(J,r)};O.on("pong",N),O.on("message",async(t,e)=>{if(N(),e)return O.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return O.send("p");if("x"===r)return b!==l&&b!==d?O.send(JSON.stringify({error:"Unexpected close ack message"})):(b=u,x("clean shutdown"),O.close());if(b===u)return O.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new c("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");r.id=e.id}return r}(r);await _.send(t.change,t.events,t.id)}catch(t){t instanceof o||t instanceof c?O.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),O.send(JSON.stringify({error:"Internal error"})))}}),b===h&&(O.send(JSON.stringify({init:_.getInitialData()})),_.listen((t,e)=>O.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),N()),await S}finally{clearTimeout(v);for(const t of f.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
|
package/backend/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;
|
|
1
|
+
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let r=0;return()=>{const t=r++;return`${e}-${t}`}};class r extends EventTarget{t=[];i=!1;push(t){return new Promise((e,r)=>{this.t.push(async()=>{try{e(await t())}catch(t){r(t)}}),this.i||this.o()})}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class s{h;l=new Map;constructor(t=()=>new r){this.h=t}push(t,e){let r=this.l.get(t);if(!r){const e=this.h(),s=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",s))};e.addEventListener("drain",s),this.l.set(t,e),r=e}return r.push(e)}}class n{u;m=new Map;constructor(t){this.u=t}async add(t,e){let r=this.m.get(t);r||(r=this.u(t),this.m.set(t,r)),await r.add(e)}async remove(t,e){const r=this.m.get(t);if(r){await r.remove(e)||this.m.delete(t)}}async broadcast(t,e){const r=this.m.get(t);r&&await r.broadcast(e)}}class i{p=new Set;add(t){this.p.add(t)}remove(t){return this.p.delete(t),this.p.size>0}broadcast(t){this.p.forEach(e=>e(t))}}const a={validateWrite(){},validateEvent(){}};class o{v;_;p;S;O;constructor(t,r,a={}){this.v=t,this._=r,this.p=a.subscribers??new n(()=>new i),this.S=a.taskQueues??new s,this.O=a.idProvider??e()}async subscribe(t,e=a){let r={C:0},s="";const n=t=>{2===r.C?t.source===s?r.I(t.message,t.meta):t.message.change&&r.I(t.message,void 0):1===r.C&&r.t.push(t)};try{if(await this.S.push(t,async()=>{const e=await this.v.read(t);null!=e&&(r={C:1,J:e,t:[]},await this.p.add(t,n))}),0===r.C)return null;s=await this.O()}catch(e){throw await this.p.remove(t,n),e}return{getInitialData(){if(1!==r.C)throw new Error("Already started");return r.J},listen(t){if(1!==r.C)throw new Error("Already started");const e=r.t;r={C:2,I:t},e.forEach(n)},send:(r,n,i)=>this.N(t,r,n,e,s,i),close:async()=>{await this.p.remove(t,n)}}}update(t,e,{events:r,permission:s=a}={}){return this.N(t,e,r,s,null,void 0)}async T(t,e,r,s,n,i){if(r?.length)for(const t of r)s.validateEvent(t);try{if(s.validateWriteSpec?.(e),!this._.isNoOp?.(e)){const r=await this.v.read(t);if(!r)throw new Error("Deleted");const n=this._.update(r,e),i=this.v.validate(n);s.validateWrite(i,r),await this.v.write(t,i,r)}}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:n,meta:i})}0===r?.length&&(r=void 0),this.p.broadcast(t,{message:{change:e,events:r},source:n,meta:i})}async N(t,e,r,s,n,i){return this.S.push(t,()=>this.T(t,e,r,s,n,i))}}class c extends Error{}class h extends Error{}const l="P",u="p",d="X",w="x";class f{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=g,setSoftCloseHandler:i,onConnect:a,onDisconnect:o,onError:l=_}){return async(...u)=>{const d=[];let w;try{const{id:f,permission:g}=await t(...u),_=await this.broadcaster.subscribe(f,g);if(!_)throw n;d.push(()=>_.close());let b,E=y,S="connection failed";const O=new Promise(t=>{b=e=>{b=()=>{},S=e,t()}}),C=await e(...u);u.length=1,i?.(u[0],()=>(E===y&&(E=m,C.send("X"),w||(w=setTimeout(I,s))),O));const x=Date.now();a?.(u[0],f,g),d.push(()=>o?.(u[0],S,Date.now()-x)),C.on("close",()=>{clearTimeout(w),E=p,b("client disconnect")});const I=()=>{C.terminate(),E=v,b("connection lost")},J=()=>{C.ping(),clearTimeout(w),w=setTimeout(I,s)},N=()=>{clearTimeout(w),w=setTimeout(J,r)};C.on("pong",N),C.on("message",async(t,e)=>{if(N(),e)return C.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return C.send("p");if("x"===r)return E!==m&&E!==v?C.send(JSON.stringify({error:"Unexpected close ack message"})):(E=p,b("clean shutdown"),C.close());if(E===p)return C.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new h("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new h("Must specify change and optional id");const r={change:e.change};if("events"in e){if(!Array.isArray(e.events)||e.events.some(t=>!Array.isArray(t)||"string"!=typeof t[0]))throw new h("If specified, events must be an array of events");r.events=e.events}if("id"in e){if("number"!=typeof e.id)throw new h("If specified, id must be a number");r.id=e.id}return r}(r);await _.send(t.change,t.events,t.id)}catch(t){t instanceof c||t instanceof h?C.send(JSON.stringify({error:t.message})):(l(u[0],t,"message"),C.send(JSON.stringify({error:"Internal error"})))}}),E===y&&(C.send(JSON.stringify({init:_.getInitialData()})),_.listen((t,e)=>C.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),N()),await O}finally{clearTimeout(w);for(const t of d.reverse())try{await t()}catch(t){l(u[0],t,"teardown")}}}}}const y=0,m=1,p=2,v=3,g=new Error("not found"),_=(t,e,r)=>console.warn(`shared-reducer: ${r}`,e),b=t=>t;class E{j;A;validate;D;$;constructor(t,e,r,s={}){this.j=t,this.A=e,this.validate=r,this.D=s.readErrorIntercept??b,this.$=s.writeErrorIntercept??b}async read(t){try{return await this.j.where(this.A,t).get()}catch(t){throw this.D(t)}}async write(t,e,r){const s={};Object.entries(e).forEach(([t,e])=>{const n=t;e!==(Object.prototype.hasOwnProperty.call(r,n)?r[n]:void 0)&&(s[n]?Object.defineProperty(s,n,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[n]=e)});try{await this.j.where(this.A,t).update(s)}catch(t){throw this.$(t)}}}class S{read=this.get;validate;P=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.P.set(t,e)}get(t){return this.P.get(t)}delete(t){this.P.delete(t)}write(t,e,r){if(r!==this.P.get(t))throw new Error("Unexpected previous value");this.P.set(t,e)}}const O="Cannot modify data",C={validateWriteSpec(){throw new c(O)},validateWrite(){throw new c(O)},validateEvent(){throw new c(O)}};class x{W;constructor(t=[]){this.W=t}validateWrite(t,e){for(const r of this.W){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new c(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new c(`Cannot edit field ${String(r)}`)}}validateEvent(){}}export{r as AsyncTaskQueue,o as Broadcaster,d as CLOSE,w as CLOSE_ACK,E as CollectionStorageModel,S as InMemoryModel,i as InMemoryTopic,l as PING,u as PONG,c as PermissionError,C as ReadOnly,a as ReadWrite,x as ReadWriteStruct,s as TaskQueueMap,n as TrackingTopicMap,e as UniqueIdProvider,f as WebsocketHandlerFactory};
|
package/frontend/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
type ChangeEvent = [string, ...unknown[]];
|
|
2
|
+
|
|
1
3
|
interface Context<T, SpecT> {
|
|
2
4
|
update: (input: T, spec: SpecT) => T;
|
|
3
5
|
combine: (specs: SpecT[]) => SpecT;
|
|
@@ -5,9 +7,15 @@ interface Context<T, SpecT> {
|
|
|
5
7
|
type SpecGenerator<T, SpecT> = (state: T) => SpecSource<T, SpecT>[];
|
|
6
8
|
type SpecSource<T, SpecT> = SpecT | SpecGenerator<T, SpecT> | null;
|
|
7
9
|
type DispatchSpec<T, SpecT> = SpecSource<T, SpecT>[];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
type DispatchFn<T, SpecT> = (specs: DispatchSpec<T, SpecT>, options?: {
|
|
11
|
+
events?: ChangeEvent[] | undefined;
|
|
12
|
+
syncedCallback?: ((state: T) => void) | undefined;
|
|
13
|
+
errorCallback?: ((error: string) => void) | undefined;
|
|
14
|
+
}) => void;
|
|
15
|
+
interface Dispatch<T, SpecT> extends DispatchFn<T, SpecT> {
|
|
16
|
+
sync(specs?: DispatchSpec<T, SpecT>, options?: {
|
|
17
|
+
events?: ChangeEvent[] | undefined;
|
|
18
|
+
}): Promise<T>;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
type MaybePromise<T> = Promise<T> | T;
|
|
@@ -31,7 +39,7 @@ declare class OnlineScheduler implements Scheduler {
|
|
|
31
39
|
private _attempts;
|
|
32
40
|
constructor(_delayGetter: DelayGetter, _connectTimeLimit: number);
|
|
33
41
|
trigger(handler: Handler, errorHandler: ErrorHandler): void;
|
|
34
|
-
schedule(handler: Handler, errorHandler: ErrorHandler): void;
|
|
42
|
+
schedule(handler: Handler, errorHandler: ErrorHandler, reset?: boolean): void;
|
|
35
43
|
stop(): void;
|
|
36
44
|
private _attempt;
|
|
37
45
|
private _remove;
|
|
@@ -60,7 +68,6 @@ interface ConnectionInfo {
|
|
|
60
68
|
url: string;
|
|
61
69
|
token?: string | undefined;
|
|
62
70
|
}
|
|
63
|
-
type ConnectionGetter = (signal: AbortSignal) => MaybePromise<ConnectionInfo>;
|
|
64
71
|
interface DisconnectDetail {
|
|
65
72
|
code: number;
|
|
66
73
|
reason: string;
|
|
@@ -77,8 +84,10 @@ interface SharedReducerOptions<T, SpecT> {
|
|
|
77
84
|
type SharedReducerEvents = {
|
|
78
85
|
connected: CustomEvent<void>;
|
|
79
86
|
disconnected: CustomEvent<DisconnectDetail>;
|
|
87
|
+
rejected: CustomEvent<DisconnectDetail>;
|
|
80
88
|
warning: CustomEvent<Error>;
|
|
81
89
|
};
|
|
90
|
+
type StateListener<T> = (state: Readonly<T>, events: Readonly<Readonly<ChangeEvent>[]>) => void;
|
|
82
91
|
declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEvents> {
|
|
83
92
|
private readonly _context;
|
|
84
93
|
private readonly _ws;
|
|
@@ -87,7 +96,8 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
87
96
|
private readonly _tracker;
|
|
88
97
|
private readonly _listeners;
|
|
89
98
|
private readonly _dispatchLock;
|
|
90
|
-
constructor(_context: Context<T, SpecT>,
|
|
99
|
+
constructor(_context: Context<T, SpecT>, connectionInfo: ConnectionInfo, { scheduler, deliveryStrategy, }?: SharedReducerOptions<T, SpecT>);
|
|
100
|
+
reconnect(connectionInfo?: ConnectionInfo): void;
|
|
91
101
|
readonly dispatch: Dispatch<T, SpecT>;
|
|
92
102
|
private _apply;
|
|
93
103
|
private readonly _share;
|
|
@@ -96,16 +106,17 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
96
106
|
private _handleErrorMessage;
|
|
97
107
|
private _handleGracefulClose;
|
|
98
108
|
private readonly _handleMessage;
|
|
99
|
-
addStateListener(listener:
|
|
100
|
-
removeStateListener(listener:
|
|
109
|
+
addStateListener(listener: StateListener<T>): void;
|
|
110
|
+
removeStateListener(listener: StateListener<T>): void;
|
|
101
111
|
private _setLocalState;
|
|
102
112
|
getState(): Readonly<T> | undefined;
|
|
103
113
|
private _warn;
|
|
104
114
|
private readonly _handleConnected;
|
|
105
115
|
private readonly _handleConnectionFailure;
|
|
116
|
+
private readonly _handleRejected;
|
|
106
117
|
private readonly _handleDisconnected;
|
|
107
118
|
close(): void;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
|
|
111
|
-
export type { Context, DeliveryStrategy, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
|
|
122
|
+
export type { ChangeEvent, ConnectionInfo, Context, DeliveryStrategy, DisconnectDetail, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
|
package/frontend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s){this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};try{await Promise.race([
|
|
1
|
+
"use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,k:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{$;j;I=null;P=!1;constructor(t,s){super(),this.$=t,this.j=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.j.trigger(this.L,this.M)}reconnect(t){t&&(this.$=t),this.I?this.I.close():this.P||this.j.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.$;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.I=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.j.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.I=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.I}send=t=>{if(!this.I)throw new Error("connection lost");this.I.send(t)};close(){this.P=!0,this.j.stop(),this.I?.close()}}const o="P",r="p",l=2e4,c=()=>!0;class a{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t,s){this.D.push({F:void 0,q:t,G:s,H:[],R:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.H.push(s??d),t.R.push(e??d)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.R.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.H.length>0||e.R.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),i.G=u(n.map(t=>t.G)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,events:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,V:!1}:{K:this.D.splice(s,1)[0],V:0===s}}Y(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}function u(t){const[s,...e]=t.filter(t=>void 0!==t);if(!s)return;if(!e.length)return s;const i=[...s];for(const t of e)for(const s of t){const t=i.findIndex(t=>t[0]===s[0]);-1!==t&&i.splice(t,1),i.push(s)}return i}const d=()=>null;const _=[],g={code:0,reason:"graceful shutdown"},f=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=c,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{A;I;Z=!0;S={tt:0,st:[]};et;it=new Set;nt=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(f,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.et=new a(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.ht),this.I.addEventListener("connected",this.ot),this.I.addEventListener("connectionfailure",this.rt),this.I.addEventListener("rejected",this.lt),this.I.addEventListener("disconnected",this.ct)}reconnect(t){this.I.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[],e={})=>new Promise((i,n)=>t(s,{...e,syncedCallback:i,errorCallback:t=>n(new Error(t))}))})}((t,s={})=>{if(!(t.length||s.events?.length||s.syncedCallback||s.errorCallback))return;const e={ut:t,G:s.events,H:s.syncedCallback,R:s.errorCallback};switch(this.S.tt){case-1:throw new Error("closed");case 0:this.S.st.push(e);break;case 1:this.dt(this._t(this.S.gt,[e]),s.events),this.wt.ft()}});_t(t,s){return this.nt(()=>{for(const{ut:i,G:n,H:h,R:o}of s){if(i.length){const{S:s,k:h}=e(this.A,t,i);t=s,this.et.N(h,n)}else n?.length&&this.et.N(this.A.combine([]),n);this.et.U(t,h,o)}return t})}wt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{vt:i,ft:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.I.isConnected()&&!this.Z&&this.et.X(this.I.send)});bt(t){if(-1!==this.S.tt){if(this.Z=!1,0===this.S.tt){const s=this._t(t.init,this.S.st);this.S={tt:1,yt:t.init,gt:s},this.dt(s,_,!0)}else this.S.yt=t.init,this.et.W(t.init),this.dt(this.et.Y(t.init));this.wt.vt()}else this.Tt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Ct(t){if(1!==this.S.tt)return void this.Tt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.yt=this.A.update(this.S.yt,t.change),{K:e,V:i}=this.et.B(t.id);i||this.dt(this.et.Y(s),e?_:t.events),e?.H.forEach(t=>t(s))}Et(t){if(1!==this.S.tt)return void this.Tt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.et.B(t.id);s?(this.Tt(`API rejected update: ${t.error}`),s?.R.forEach(s=>s(t.error)),this.dt(this.et.Y(this.S.yt))):this.Tt(`API sent error: ${t.error}`)}xt(){this.I.send("x"),this.Z?this.Tt("Unexpected extra close message"):(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:g})))}ht=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.Ct(s):"init"in s?this.bt(s):"error"in s?this.Et(s):this.Tt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.tt&&t(this.S.gt,[])}removeStateListener(t){this.it.delete(t)}dt(t,s=_,e=!1){if(1!==this.S.tt)throw new Error("invalid state");if(e||this.S.gt!==t||s.length){this.S.gt=t;for(const e of this.it)e(t,s)}}getState(){return 1===this.S.tt?this.S.gt:void 0}Tt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ot=()=>{this.dispatchEvent(n("connected"))};rt=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};lt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};ct=t=>{this.Z||(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Z=!0,this.S={tt:-1},this.I.close(),this.wt.o(),this.it.clear(),this.I.removeEventListener("message",this.ht),this.I.removeEventListener("connected",this.ot),this.I.removeEventListener("connectionfailure",this.rt),this.I.removeEventListener("rejected",this.lt),this.I.removeEventListener("disconnected",this.ct)}},exports.exponentialDelay=s;
|
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.
|
|
1
|
+
class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s,e=!0){e&&(this._=0),this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.v()}async m(){if(this.o||!this.l)return;this.v();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};const e=this.l;this.l=null;try{await Promise.race([e(t.signal),s.promise])}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}e&&this.schedule(e,this.u,!1)}}finally{s.stop(),this.o=null}}v(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={p:t,T:0,C:null};for(;e;)if(e.T>=e.p.length)e=e.C;else{const t=s(e.p[e.T]);++e.T,t&&t.length&&(e={p:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,k:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,s);class h extends i{$;j;I=null;P=!1;constructor(t,s){super(),this.$=t,this.j=s,this.L=this.L.bind(this),this.M=this.M.bind(this),this.j.trigger(this.L,this.M)}reconnect(t){t&&(this.$=t),this.I?this.I.close():this.P||this.j.schedule(this.L,this.M)}M(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",{detail:s}))}async L(t){t.throwIfAborted();const s=new AbortController,e=s.signal,{url:i,token:h}=this.$;await new Promise((c,a)=>{const u=new WebSocket(i);let d=!0;const _=t=>{s.abort(),u.close();const e=d;d=!1,e||(this.I=null,this.dispatchEvent(n("disconnected",{detail:t}))),this.P||this.dispatchEvent(n("rejected",{detail:t,cancelable:!0}))?e?a(new Error(`Connection closed ${t.code} ${t.reason}`)):this.P||this.j.schedule(this.L,this.M):e&&c()};h&&u.addEventListener("open",()=>u.send(h),{once:!0,signal:e}),u.addEventListener("message",t=>{t.data!==r&&(d&&(d=!1,this.I=u,this.dispatchEvent(n("connected")),c()),this.dispatchEvent(n("message",{detail:t.data})))},{signal:e}),u.addEventListener("close",_,{signal:e}),u.addEventListener("error",t=>{let s="unknown";if("error"in t){const e=t.error;s=e instanceof Error?e.stack??String(e):String(e)}_({code:0,reason:`client side error: ${s}`})},{signal:e}),t.addEventListener("abort",()=>{s.abort(),u.close(),d=!1,a(t.reason)},{signal:e}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(o)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,l)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(u)}).catch(t=>{throw s.abort(),t})}isConnected(){return null!==this.I}send=t=>{if(!this.I)throw new Error("connection lost");this.I.send(t)};close(){this.P=!0,this.j.stop(),this.I?.close()}}const o="P",r="p",l=2e4,c=()=>!0,a=(t,s,e)=>!e;class u{A;O;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.A=t,this.O=s}N(t,s){this.D.push({F:void 0,q:t,G:s,H:[],R:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.H.push(s??_),t.R.push(e??_)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.O(t,i.q,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.R.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.H.length>0||e.R.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.q=this.A.combine(n.map(t=>t.q)),i.G=d(n.map(t=>t.G)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.q,events:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,V:!1}:{K:this.D.splice(s,1)[0],V:0===s}}Y(t){if(!this.D.length)return t;const s=this.A.combine(this.D.map(({q:t})=>t));return this.A.update(t,s)}}function d(t){const[s,...e]=t.filter(t=>void 0!==t);if(!s)return;if(!e.length)return s;const i=[...s];for(const t of e)for(const s of t){const t=i.findIndex(t=>t[0]===s[0]);-1!==t&&i.splice(t,1),i.push(s)}return i}const _=()=>null;class g extends i{A;I;Z=!0;S={tt:0,st:[]};et;it=new Set;nt=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(w,2e4),deliveryStrategy:n=c}={}){super(),this.A=s,this.et=new u(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.ht),this.I.addEventListener("connected",this.ot),this.I.addEventListener("connectionfailure",this.rt),this.I.addEventListener("rejected",this.lt),this.I.addEventListener("disconnected",this.ct)}reconnect(t){this.I.reconnect(t)}dispatch=function(t){return Object.assign(t,{sync:(s=[],e={})=>new Promise((i,n)=>t(s,{...e,syncedCallback:i,errorCallback:t=>n(new Error(t))}))})}((t,s={})=>{if(!(t.length||s.events?.length||s.syncedCallback||s.errorCallback))return;const e={ut:t,G:s.events,H:s.syncedCallback,R:s.errorCallback};switch(this.S.tt){case-1:throw new Error("closed");case 0:this.S.st.push(e);break;case 1:this.dt(this._t(this.S.gt,[e]),s.events),this.wt.ft()}});_t(t,s){return this.nt(()=>{for(const{ut:i,G:n,H:h,R:o}of s){if(i.length){const{S:s,k:h}=e(this.A,t,i);t=s,this.et.N(h,n)}else n?.length&&this.et.N(this.A.combine([]),n);this.et.U(t,h,o)}return t})}wt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{vt:i,ft:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.I.isConnected()&&!this.Z&&this.et.X(this.I.send)});bt(t){if(-1!==this.S.tt){if(this.Z=!1,0===this.S.tt){const s=this._t(t.init,this.S.st);this.S={tt:1,yt:t.init,gt:s},this.dt(s,f,!0)}else this.S.yt=t.init,this.et.W(t.init),this.dt(this.et.Y(t.init));this.wt.vt()}else this.Tt(`Ignoring init after closing: ${JSON.stringify(t)}`)}Ct(t){if(1!==this.S.tt)return void this.Tt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.yt=this.A.update(this.S.yt,t.change),{K:e,V:i}=this.et.B(t.id);i||this.dt(this.et.Y(s),e?f:t.events),e?.H.forEach(t=>t(s))}Et(t){if(1!==this.S.tt)return void this.Tt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.et.B(t.id);s?(this.Tt(`API rejected update: ${t.error}`),s?.R.forEach(s=>s(t.error)),this.dt(this.et.Y(this.S.yt))):this.Tt(`API sent error: ${t.error}`)}St(){this.I.send("x"),this.Z?this.Tt("Unexpected extra close message"):(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:m})))}ht=t=>{if("X"===t.detail)return void this.St();const s=JSON.parse(t.detail);"change"in s?this.Ct(s):"init"in s?this.bt(s):"error"in s?this.Et(s):this.Tt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.tt&&t(this.S.gt,[])}removeStateListener(t){this.it.delete(t)}dt(t,s=f,e=!1){if(1!==this.S.tt)throw new Error("invalid state");if(e||this.S.gt!==t||s.length){this.S.gt=t;for(const e of this.it)e(t,s)}}getState(){return 1===this.S.tt?this.S.gt:void 0}Tt(t){this.dispatchEvent(n("warning",{detail:new Error(t)}))}ot=()=>{this.dispatchEvent(n("connected"))};rt=t=>{this.dispatchEvent(n("warning",{detail:t.detail}))};lt=t=>{this.dispatchEvent(n("rejected",{detail:t.detail,cancelable:!0}))||t.preventDefault()};ct=t=>{this.Z||(this.Z=!0,this.dispatchEvent(n("disconnected",{detail:t.detail})))};close(){this.Z=!0,this.S={tt:-1},this.I.close(),this.wt.o(),this.it.clear(),this.I.removeEventListener("message",this.ht),this.I.removeEventListener("connected",this.ot),this.I.removeEventListener("connectionfailure",this.rt),this.I.removeEventListener("rejected",this.lt),this.I.removeEventListener("disconnected",this.ct)}}const f=[],m={code:0,reason:"graceful shutdown"},w=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{c as AT_LEAST_ONCE,a as AT_MOST_ONCE,t as OnlineScheduler,g as SharedReducer,s as exponentialDelay};
|