shared-reducer 5.0.3 → 6.0.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 +78 -28
- package/backend/index.d.ts +19 -15
- package/backend/index.js +1 -1
- package/backend/index.mjs +1 -1
- package/frontend/index.d.ts +2 -1
- package/frontend/index.js +1 -1
- package/frontend/index.mjs +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -15,8 +15,53 @@ npm install --save shared-reducer json-immutability-helper
|
|
|
15
15
|
|
|
16
16
|
## Usage (Backend)
|
|
17
17
|
|
|
18
|
-
This project is compatible with [
|
|
19
|
-
but can also be used in
|
|
18
|
+
This project is compatible with [web-listener](https://github.com/davidje13/web-listener) and
|
|
19
|
+
[websocket-express](https://github.com/davidje13/websocket-express), but can also be used in
|
|
20
|
+
isolation.
|
|
21
|
+
|
|
22
|
+
### With web-listener
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import {
|
|
26
|
+
Broadcaster,
|
|
27
|
+
WebsocketHandlerFactory,
|
|
28
|
+
InMemoryModel,
|
|
29
|
+
ReadWrite,
|
|
30
|
+
} from 'shared-reducer/backend';
|
|
31
|
+
import context from 'json-immutability-helper';
|
|
32
|
+
import { WebListener, Router, makeAcceptWebSocket, setSoftCloseHandler } from 'web-listener';
|
|
33
|
+
import { WebSocketServer } from 'ws';
|
|
34
|
+
|
|
35
|
+
const model = new InMemoryModel();
|
|
36
|
+
const broadcaster = new Broadcaster(model, context);
|
|
37
|
+
model.set('a', { foo: 'v1' });
|
|
38
|
+
|
|
39
|
+
const acceptWebSocket = makeAcceptWebSocket(WebSocketServer);
|
|
40
|
+
const handlerFactory = new WebsocketHandlerFactory(broadcaster);
|
|
41
|
+
|
|
42
|
+
const router = new Router();
|
|
43
|
+
router.ws(
|
|
44
|
+
'/:id',
|
|
45
|
+
handlerFactory.handler({
|
|
46
|
+
accessGetter: (req) => ({
|
|
47
|
+
id: getPathParameter(req, 'id'),
|
|
48
|
+
permission: ReadWrite,
|
|
49
|
+
}),
|
|
50
|
+
acceptWebSocket,
|
|
51
|
+
setSoftCloseHandler,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const server = new WebListener(router).listen(0, 'localhost');
|
|
56
|
+
|
|
57
|
+
// later, to shutdown gracefully:
|
|
58
|
+
// send a close signal to all clients and wait up to 1 second for acknowledgement:
|
|
59
|
+
await server.closeWithTimeout('shutdown', 1000);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For real use-cases, you will probably want to add authentication middleware to the router chain,
|
|
63
|
+
and you may want to give some users read-only and others read-write access, which can be achieved in
|
|
64
|
+
the `accessGetter` lambda.
|
|
20
65
|
|
|
21
66
|
### With websocket-express
|
|
22
67
|
|
|
@@ -34,23 +79,35 @@ const model = new InMemoryModel();
|
|
|
34
79
|
const broadcaster = new Broadcaster(model, context);
|
|
35
80
|
model.set('a', { foo: 'v1' });
|
|
36
81
|
|
|
37
|
-
const app = new WebSocketExpress();
|
|
38
|
-
const server = app.listen(0, 'localhost');
|
|
39
|
-
|
|
40
82
|
const handlerFactory = new WebsocketHandlerFactory(broadcaster);
|
|
41
|
-
|
|
83
|
+
const softClosers = new Map();
|
|
42
84
|
|
|
43
|
-
const
|
|
85
|
+
const app = new WebSocketExpress();
|
|
86
|
+
app.ws(
|
|
87
|
+
'/:id',
|
|
88
|
+
handlerFactory.handler({
|
|
89
|
+
accessGetter: (req) => ({ id: req.params.id, permission: ReadWrite }),
|
|
90
|
+
acceptWebSocket: (_, res) => res.accept(),
|
|
91
|
+
setSoftCloseHandler: (req, fn) => softClosers.set(req, fn),
|
|
92
|
+
onDisconnect: (req) => softClosers.delete(req),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const server = app.listen(0, 'localhost');
|
|
44
97
|
|
|
45
98
|
// later, to shutdown gracefully:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
99
|
+
server.close(); // stop accepting new connections
|
|
100
|
+
// send a close signal to all active clients:
|
|
101
|
+
for (const fn of softClosers.values()) {
|
|
102
|
+
fn();
|
|
103
|
+
}
|
|
104
|
+
// force-close connections after a time:
|
|
105
|
+
setTimeout(() => server.closeAllConnections(), 1000);
|
|
49
106
|
```
|
|
50
107
|
|
|
51
108
|
For real use-cases, you will probably want to add authentication middleware to the expressjs chain,
|
|
52
109
|
and you may want to give some users read-only and others read-write access, which can be achieved in
|
|
53
|
-
the
|
|
110
|
+
the `accessGetter` lambda.
|
|
54
111
|
|
|
55
112
|
### Alone
|
|
56
113
|
|
|
@@ -67,7 +124,9 @@ model.set('a', { foo: 'v1' });
|
|
|
67
124
|
const subscription = await broadcaster.subscribe('a');
|
|
68
125
|
|
|
69
126
|
const begin = subscription.getInitialData();
|
|
70
|
-
subscription.listen((change, meta) => {
|
|
127
|
+
subscription.listen((change, meta) => {
|
|
128
|
+
// ...
|
|
129
|
+
});
|
|
71
130
|
await subscription.send(['=', { foo: 'v2' }]);
|
|
72
131
|
// callback provided earlier is invoked
|
|
73
132
|
|
|
@@ -128,9 +187,7 @@ reducer.addEventListener('warning', (e) => {
|
|
|
128
187
|
|
|
129
188
|
const dispatch = reducer.dispatch;
|
|
130
189
|
|
|
131
|
-
dispatch([
|
|
132
|
-
{ a: ['=', 8] },
|
|
133
|
-
]);
|
|
190
|
+
dispatch([{ a: ['=', 8] }]);
|
|
134
191
|
|
|
135
192
|
dispatch([
|
|
136
193
|
(state) => {
|
|
@@ -151,10 +208,7 @@ dispatch(
|
|
|
151
208
|
(message) => console.warn('failed to sync', message),
|
|
152
209
|
);
|
|
153
210
|
|
|
154
|
-
dispatch([
|
|
155
|
-
{ a: ['add', 1] },
|
|
156
|
-
{ a: ['add', 1] },
|
|
157
|
-
]);
|
|
211
|
+
dispatch([{ a: ['add', 1] }, { a: ['add', 1] }]);
|
|
158
212
|
```
|
|
159
213
|
|
|
160
214
|
### Specs
|
|
@@ -226,10 +280,7 @@ import listCommands from 'json-immutability-helper/commands/list';
|
|
|
226
280
|
import mathCommands from 'json-immutability-helper/commands/math';
|
|
227
281
|
import context from 'json-immutability-helper';
|
|
228
282
|
|
|
229
|
-
const broadcaster = new Broadcaster(
|
|
230
|
-
new InMemoryModel(),
|
|
231
|
-
context.with(listCommands, mathCommands),
|
|
232
|
-
);
|
|
283
|
+
const broadcaster = new Broadcaster(new InMemoryModel(), context.with(listCommands, mathCommands));
|
|
233
284
|
```
|
|
234
285
|
|
|
235
286
|
```javascript
|
|
@@ -239,10 +290,9 @@ import listCommands from 'json-immutability-helper/commands/list';
|
|
|
239
290
|
import mathCommands from 'json-immutability-helper/commands/math';
|
|
240
291
|
import context from 'json-immutability-helper';
|
|
241
292
|
|
|
242
|
-
const reducer = new SharedReducer(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
);
|
|
293
|
+
const reducer = new SharedReducer(context.with(listCommands, mathCommands), () => ({
|
|
294
|
+
url: 'ws://destination',
|
|
295
|
+
}));
|
|
246
296
|
```
|
|
247
297
|
|
|
248
298
|
If you want to use an entirely different reducer, create a wrapper:
|
|
@@ -281,7 +331,7 @@ new Broadcaster(model, reducer[, options]);
|
|
|
281
331
|
```
|
|
282
332
|
|
|
283
333
|
- `options.subscribers`: specify a custom keyed broadcaster, used for communicating changes to all
|
|
284
|
-
consumers. Required interface:
|
|
334
|
+
consumers. Required interface:
|
|
285
335
|
|
|
286
336
|
```javascript
|
|
287
337
|
{
|
package/backend/index.d.ts
CHANGED
|
@@ -91,28 +91,31 @@ interface ServerWebSocket {
|
|
|
91
91
|
on(event: 'pong', listener: () => void): void;
|
|
92
92
|
ping(): void;
|
|
93
93
|
send(message: string): void;
|
|
94
|
+
close(): void;
|
|
94
95
|
terminate(): void;
|
|
95
96
|
}
|
|
96
|
-
interface
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
beginTransaction(): void;
|
|
100
|
-
endTransaction(): void;
|
|
97
|
+
interface Access<T, SpecT> {
|
|
98
|
+
id: string;
|
|
99
|
+
permission: Permission<T, SpecT>;
|
|
101
100
|
}
|
|
102
|
-
|
|
101
|
+
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
|
|
102
|
+
interface WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> {
|
|
103
|
+
accessGetter: AccessGetter;
|
|
104
|
+
acceptWebSocket: AcceptWebSocket;
|
|
105
|
+
}
|
|
106
|
+
interface WebsocketHandlerOptions<Arg0> {
|
|
103
107
|
pingInterval?: number;
|
|
104
108
|
pongTimeout?: number;
|
|
109
|
+
notFoundError?: Error;
|
|
110
|
+
setSoftCloseHandler?: (arg0: Arg0, handler: () => Promise<void>) => void;
|
|
111
|
+
onConnect?: (arg0: Arg0) => void;
|
|
112
|
+
onDisconnect?: (arg0: Arg0, reason: string, connectionDuration: number) => void;
|
|
113
|
+
onError?: (arg0: Arg0, error: unknown, context: string) => void;
|
|
105
114
|
}
|
|
106
115
|
declare class WebsocketHandlerFactory<T, SpecT> {
|
|
107
116
|
private readonly broadcaster;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
private readonly _pongTimeout;
|
|
111
|
-
private closing;
|
|
112
|
-
constructor(broadcaster: Broadcaster<T, SpecT>, options?: WebsocketHandlerOptions);
|
|
113
|
-
activeConnections(): number;
|
|
114
|
-
softClose(timeout: number): Promise<void>;
|
|
115
|
-
handler<Req, Res extends WSResponse>(idGetter: (req: Req, res: Res) => MaybePromise<string>, permissionGetter: (req: Req, res: Res) => MaybePromise<Permission<T, SpecT>>): (req: Req, res: Res) => Promise<void>;
|
|
117
|
+
constructor(broadcaster: Broadcaster<T, SpecT>);
|
|
118
|
+
handler<Args extends any[], AccessGetter extends (...args: Args) => MaybePromise<Access<T, SpecT>>, AcceptWebSocket extends (...args: Args) => MaybePromise<ServerWebSocket>>({ accessGetter, acceptWebSocket, pingInterval, pongTimeout, notFoundError, setSoftCloseHandler, onConnect, onDisconnect, onError, }: WebsocketHandlerCoreOptions<AccessGetter, AcceptWebSocket> & WebsocketHandlerOptions<First<Args>>): (...args: Args) => Promise<void>;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
declare const UniqueIdProvider: () => () => string;
|
|
@@ -181,4 +184,5 @@ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
|
|
|
181
184
|
broadcast(key: K, message: T): Promise<void>;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK,
|
|
187
|
+
export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, CollectionStorageModel, InMemoryModel, InMemoryTopic, PING, PONG, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, TaskQueueMap, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
|
|
188
|
+
export type { ChangeInfo, Context, Model, Permission, Subscription, Task, TaskQueue, TaskQueueFactory, Topic, TopicMap, TopicMessage };
|
package/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let
|
|
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.get(this.D,t)}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.update(this.D,t,s)}catch(t){throw this.q(t)}}},exports.InMemoryModel=class{read=this.get;validate;P=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.P.set(t,e)}get(t){return this.P.get(t)}delete(t){this.P.delete(t)}write(t,e,r){if(r!==this.P.get(t))throw new Error("Unexpected previous value");this.P.set(t,e)}},exports.InMemoryTopic=i,exports.PING="P",exports.PONG="p",exports.PermissionError=o,exports.ReadOnly=m,exports.ReadWrite=a,exports.ReadWriteStruct=class{W;constructor(t=[]){this.W=t}validateWrite(t,e){for(const r of this.W){const s=Object.prototype.hasOwnProperty.call(e,r),n=Object.prototype.hasOwnProperty.call(t,r);if(s!==n)throw new o(s?`Cannot remove field ${String(r)}`:`Cannot add field ${String(r)}`);if(n&&e[r]!==t[r])throw new o(`Cannot edit field ${String(r)}`)}}},exports.TaskQueueMap=s,exports.TrackingTopicMap=n,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;constructor(t){this.broadcaster=t}handler({accessGetter:t,acceptWebSocket:e,pingInterval:r=25e3,pongTimeout:s=3e4,notFoundError:n=w,setSoftCloseHandler:i,onConnect:a,onDisconnect:f,onError:y=p}){return async(...w)=>{const p=[];let m;try{const{id:g,permission:_}=await t(...w),x=await this.broadcaster.subscribe(g,_);if(!x)throw n;p.push(()=>x.close());let v,b=h,S="connection failed";const O=new Promise(t=>{v=e=>{v=()=>{},S=e,t()}}),E=await e(...w);w.length=1,i?.(w[0],()=>(b===h&&(b=u,E.send("X"),m||(m=setTimeout(J,s))),O));const C=Date.now();a?.(w[0]),p.push(()=>f?.(w[0],S,Date.now()-C)),E.on("close",()=>{clearTimeout(m),b=l,v("client disconnect")});const J=()=>{E.terminate(),b=d,v("connection lost")},N=()=>{E.ping(),clearTimeout(m),m=setTimeout(J,s)},T=()=>{clearTimeout(m),m=setTimeout(N,r)};E.on("pong",T),E.on("message",async(t,e)=>{if(T(),e)return E.send(JSON.stringify({error:"Binary messages are not supported"}));const r=String(t);if("P"===r)return E.send("p");if("x"===r)return b!==u&&b!==d?E.send(JSON.stringify({error:"Unexpected close ack message"})):(b=l,v("clean shutdown"),E.close());if(b===l)return E.send(JSON.stringify({error:"Unexpected message after close ack"}));try{const t=function(t){let e;try{e=JSON.parse(t)}catch{throw new c("Invalid JSON")}if("object"!=typeof e||!e||Array.isArray(e)||!("change"in e))throw new c("Must specify change and optional id");if("id"in e){if("number"!=typeof e.id)throw new c("If specified, id must be a number");return{change:e.change,id:e.id}}return{change:e.change}}(r);await x.send(t.change,t.id)}catch(t){t instanceof o||t instanceof c?E.send(JSON.stringify({error:t.message})):(y(w[0],t,"message"),E.send(JSON.stringify({error:"Internal error"})))}}),b===h&&(E.send(JSON.stringify({init:x.getInitialData()})),x.listen((t,e)=>E.send(JSON.stringify(void 0!==e?{id:e,...t}:t))),T()),await O}finally{clearTimeout(m);for(const t of p.reverse())try{await t()}catch(t){y(w[0],t,"teardown")}}}}};
|
package/backend/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let
|
|
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.get(this.D,t)}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.update(this.D,t,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
|
@@ -107,4 +107,5 @@ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEven
|
|
|
107
107
|
close(): void;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export { AT_LEAST_ONCE, AT_MOST_ONCE,
|
|
110
|
+
export { AT_LEAST_ONCE, AT_MOST_ONCE, OnlineScheduler, SharedReducer, exponentialDelay };
|
|
111
|
+
export type { Context, DeliveryStrategy, Dispatch, DispatchSpec, Scheduler, SharedReducerOptions };
|
package/frontend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s){this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((
|
|
1
|
+
"use strict";class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s){this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((e,i)=>{const n=setTimeout(()=>i(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};try{await Promise.race([this.l(t.signal),s.promise]),this.l=null,this._=0}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}const e=this.l;e&&(this.l=null,this.schedule(e,this.u))}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:e,randomness:i=0})=>n=>Math.min(Math.pow(t,n)*s,e)*(1-Math.random()*i);function e(t,s,e){const i=[],n=[];function h(){if(n.length>0){const e=t.combine(n);i.push(e),s=t.update(s,e),n.length=0}}return function(t,s){let e={v:t,T:0,C:null};for(;e;)if(e.T>=e.v.length)e=e.C;else{const t=s(e.v[e.T]);++e.T,t&&t.length&&(e={v:t,T:0,C:e})}}(e,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(i)}}class i extends EventTarget{addEventListener(t,s,e){super.addEventListener(t,s,e)}removeEventListener(t,s,e){super.removeEventListener(t,s,e)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,{detail:s});class h extends i{I;P;k=null;L=!1;constructor(t,s){super(),this.I=t,this.P=s,this.M=this.M.bind(this),this.A=this.A.bind(this),this.P.trigger(this.M,this.A)}A(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",s))}async M(t){const{url:s,token:e}=await this.I(t);if(t.aborted)throw t.reason;const i=new AbortController,h=i.signal;await new Promise((a,u)=>{const d=new WebSocket(s);let _=!0;const g=t=>{i.abort(),d.close(),_?(_=!1,u(new Error(`Connection closed ${t.code} ${t.reason}`))):(this.k=null,this.dispatchEvent(n("disconnected",t)),this.L||this.P.schedule(this.M,this.A))};e&&d.addEventListener("open",()=>d.send(e),{once:!0,signal:h}),d.addEventListener("message",t=>{t.data!==l&&(_&&(_=!1,this.k=d,this.dispatchEvent(n("connected")),a()),this.dispatchEvent(n("message",t.data)))},{signal:h}),d.addEventListener("close",g,{signal:h}),d.addEventListener("error",()=>g(o),{signal:h}),t.addEventListener("abort",()=>()=>{i.abort(),d.close(),_=!1,u(t.reason)},{signal:h}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(r)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,c)},h=()=>{null!==e&&(clearTimeout(e),e=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",i,{signal:s.signal})}(d)}).catch(t=>{throw i.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.L=!0,this.P.stop(),this.k?.close()}}const o={code:0,reason:"client side error"},r="P",l="p",c=2e4,a=()=>!0;class u{O;j;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.O=t,this.j=s}N(t){this.D.push({F:void 0,G:t,q:[],H:[]})}U(t,s,e){if(s||e)if(this.D.length){const t=this.D[this.D.length-1];t.q.push(s??d),t.H.push(e??d)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let e=0;e<this.D.length;++e){const i=this.D[e];this.j(t,i.G,void 0!==i.F)?(i.F=void 0,this.D[e-s]=i):(i.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const e=this.D[t];if(!e||void 0!==e.F||e.q.length>0||e.H.length>0){const e=t-s;if(e>1){const i=this.D[t-1],n=this.D.splice(s,e,i);i.G=this.O.combine(n.map(t=>t.G)),t-=e-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,R:!1}:{K:this.D.splice(s,1)[0],R:0===s}}V(t){if(!this.D.length)return t;const s=this.O.combine(this.D.map(({G:t})=>t));return this.O.update(t,s)}}const d=()=>null;const _={code:0,reason:"graceful shutdown"},g=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=a,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{O;k;Y=!0;S={Z:0,tt:[]};st;et=new Set;it=function(t){let s=!1;return e=>{if(s)throw new Error(t);try{return s=!0,e()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,e,{scheduler:i=new t(g,2e4),deliveryStrategy:n=a}={}){super(),this.O=s,this.st=new u(s,n),this.k=new h(e,i),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("disconnected",this.rt)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((e,i)=>t(s,e,t=>i(new Error(t))))})}((t,s,e)=>{if(!t.length&&!s&&!e)return;const i={lt:t,q:s,H:e};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(i);break;case 1:this.ct(this.ut(this.S.dt,[i])),this.gt._t()}});ut(t,s){return this.it(()=>{for(const{lt:i,q:n,H:h}of s){if(i.length){const{S:s,$:n}=e(this.O,t,i);t=s,this.st.N(n)}this.st.U(t,n,h)}return t})}gt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{ft:i,_t:()=>{null===s&&(s=setTimeout(i,0))},o:e}}(()=>{this.k.isConnected()&&!this.Y&&this.st.X(this.k.send)});wt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.ut(t.init,this.S.tt);this.S={Z:1,vt:t.init,dt:s},this.ct(s,!0)}else this.S.vt=t.init,this.st.W(t.init),this.ct(this.st.V(t.init));this.gt.ft()}else this.bt(`Ignoring init after closing: ${JSON.stringify(t)}`)}yt(t){if(1!==this.S.Z)return void this.bt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.vt=this.O.update(this.S.vt,t.change),{K:e,R:i}=this.st.B(t.id);i||this.ct(this.st.V(s)),e?.q.forEach(t=>t(s))}Tt(t){if(1!==this.S.Z)return void this.bt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.st.B(t.id);s?(this.bt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ct(this.st.V(this.S.vt))):this.bt(`API sent error: ${t.error}`)}xt(){this.k.send("x"),this.Y?this.bt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",_)))}nt=t=>{if("X"===t.detail)return void this.xt();const s=JSON.parse(t.detail);"change"in s?this.yt(s):"init"in s?this.wt(s):"error"in s?this.Tt(s):this.bt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.et.add(t),1===this.S.Z&&t(this.S.dt)}removeStateListener(t){this.et.delete(t)}ct(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S.dt!==t){this.S.dt=t;for(const s of this.et)s(t)}}getState(){return 1===this.S.Z?this.S.dt:void 0}bt(t){this.dispatchEvent(n("warning",new Error(t)))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",t.detail))};rt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.gt.o(),this.et.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("disconnected",this.rt)}},exports.exponentialDelay=s;
|
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((
|
|
1
|
+
class t{t;i;h=null;o=null;l=null;u=()=>null;_=0;constructor(t,s){this.t=t,this.i=s,this.m=this.m.bind(this)}trigger(t,s){this.stop(),this.l=t,this.u=s,this.m()}schedule(t,s){this.l!==t&&(this.o&&this.stop(),this.l=t,this.u=s,null===this.h&&("hidden"===globalThis.document?.visibilityState?globalThis.addEventListener?.("visibilitychange",this.m):(globalThis.addEventListener?.("online",this.m),this.h=setTimeout(this.m,this.t(this._))),globalThis.addEventListener?.("pageshow",this.m),globalThis.addEventListener?.("focus",this.m),++this._))}stop(){this.l=null,this.o?.(),this.o=null,this.p()}async m(){if(this.o||!this.l)return;this.p();const t=new AbortController,s=(t=>{const s={stop:()=>{}};return s.promise=new Promise((i,e)=>{const n=setTimeout(()=>e(new Error(`Timed out after ${t}ms`)),t);s.stop=()=>clearTimeout(n)}),s})(this.i);this.o=()=>{s.stop(),t.abort()};try{await Promise.race([this.l(t.signal),s.promise]),this.l=null,this._=0}catch(s){if(!t.signal.aborted){t.abort();try{this.u(s)}catch(t){console.error("Error handler failed",s,t)}const i=this.l;i&&(this.l=null,this.schedule(i,this.u))}}finally{s.stop(),this.o=null}}p(){null!==this.h&&(clearTimeout(this.h),this.h=null,globalThis.removeEventListener?.("online",this.m),globalThis.removeEventListener?.("pageshow",this.m),globalThis.removeEventListener?.("visibilitychange",this.m),globalThis.removeEventListener?.("focus",this.m))}}const s=({base:t=2,initialDelay:s,maxDelay:i,randomness:e=0})=>n=>Math.min(Math.pow(t,n)*s,i)*(1-Math.random()*e);function i(t,s,i){const e=[],n=[];function h(){if(n.length>0){const i=t.combine(n);e.push(i),s=t.update(s,i),n.length=0}}return function(t,s){let i={v:t,T:0,C:null};for(;i;)if(i.T>=i.v.length)i=i.C;else{const t=s(i.v[i.T]);++i.T,t&&t.length&&(i={v:t,T:0,C:i})}}(i,t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null}),h(),{S:s,$:t.combine(e)}}class e extends EventTarget{addEventListener(t,s,i){super.addEventListener(t,s,i)}removeEventListener(t,s,i){super.removeEventListener(t,s,i)}dispatchEvent(t){return super.dispatchEvent(t)}}const n=(t,s)=>new CustomEvent(t,{detail:s});class h extends e{I;P;k=null;L=!1;constructor(t,s){super(),this.I=t,this.P=s,this.M=this.M.bind(this),this.A=this.A.bind(this),this.P.trigger(this.M,this.A)}A(t){const s=t instanceof Error?t:new Error(`unknown connection error ${t}`);this.dispatchEvent(n("connectionfailure",s))}async M(t){const{url:s,token:i}=await this.I(t);if(t.aborted)throw t.reason;const e=new AbortController,h=e.signal;await new Promise((a,u)=>{const d=new WebSocket(s);let _=!0;const g=t=>{e.abort(),d.close(),_?(_=!1,u(new Error(`Connection closed ${t.code} ${t.reason}`))):(this.k=null,this.dispatchEvent(n("disconnected",t)),this.L||this.P.schedule(this.M,this.A))};i&&d.addEventListener("open",()=>d.send(i),{once:!0,signal:h}),d.addEventListener("message",t=>{t.data!==l&&(_&&(_=!1,this.k=d,this.dispatchEvent(n("connected")),a()),this.dispatchEvent(n("message",t.data)))},{signal:h}),d.addEventListener("close",g,{signal:h}),d.addEventListener("error",()=>g(o),{signal:h}),t.addEventListener("abort",()=>()=>{e.abort(),d.close(),_=!1,u(t.reason)},{signal:h}),function(t){const s=new AbortController;let i=null;const e=()=>{null!==i&&(clearTimeout(i),i=null),t.send(r)},n=()=>{null!==i&&clearTimeout(i),i=setTimeout(e,c)},h=()=>{null!==i&&(clearTimeout(i),i=null),s.abort()};t.addEventListener("open",n,{once:!0,signal:s.signal}),t.addEventListener("message",n,{signal:s.signal}),t.addEventListener("close",h,{signal:s.signal}),t.addEventListener("error",h,{signal:s.signal}),globalThis.addEventListener?.("offline",e,{signal:s.signal})}(d)}).catch(t=>{throw e.abort(),t})}isConnected(){return null!==this.k}send=t=>{if(!this.k)throw new Error("connection lost");this.k.send(t)};close(){this.L=!0,this.P.stop(),this.k?.close()}}const o={code:0,reason:"client side error"},r="P",l="p",c=2e4,a=()=>!0,u=(t,s,i)=>!i;class d{O;j;D=[];J=function(){let t=1;return()=>t++}();constructor(t,s){this.O=t,this.j=s}N(t){this.D.push({F:void 0,G:t,q:[],H:[]})}U(t,s,i){if(s||i)if(this.D.length){const t=this.D[this.D.length-1];t.q.push(s??_),t.H.push(i??_)}else s&&Promise.resolve(t).then(s)}W(t){let s=0;for(let i=0;i<this.D.length;++i){const e=this.D[i];this.j(t,e.G,void 0!==e.F)?(e.F=void 0,this.D[i-s]=e):(e.H.forEach(t=>t("message possibly lost")),++s)}this.D.length-=s}X(t){for(let t=0,s=0;t<=this.D.length;++t){const i=this.D[t];if(!i||void 0!==i.F||i.q.length>0||i.H.length>0){const i=t-s;if(i>1){const e=this.D[t-1],n=this.D.splice(s,i,e);e.G=this.O.combine(n.map(t=>t.G)),t-=i-1}s=t+1}}for(const s of this.D)void 0===s.F&&(s.F=this.J(),t(JSON.stringify({change:s.G,id:s.F})))}B(t){const s=void 0===t?-1:this.D.findIndex(s=>s.F===t);return-1===s?{K:null,R:!1}:{K:this.D.splice(s,1)[0],R:0===s}}V(t){if(!this.D.length)return t;const s=this.O.combine(this.D.map(({G:t})=>t));return this.O.update(t,s)}}const _=()=>null;class g extends e{O;k;Y=!0;S={Z:0,tt:[]};st;it=new Set;et=function(t){let s=!1;return i=>{if(s)throw new Error(t);try{return s=!0,i()}finally{s=!1}}}("Cannot dispatch recursively");constructor(s,i,{scheduler:e=new t(w,2e4),deliveryStrategy:n=a}={}){super(),this.O=s,this.st=new d(s,n),this.k=new h(i,e),this.k.addEventListener("message",this.nt),this.k.addEventListener("connected",this.ht),this.k.addEventListener("connectionfailure",this.ot),this.k.addEventListener("disconnected",this.rt)}dispatch=function(t){return Object.assign(t,{sync:(s=[])=>new Promise((i,e)=>t(s,i,t=>e(new Error(t))))})}((t,s,i)=>{if(!t.length&&!s&&!i)return;const e={lt:t,q:s,H:i};switch(this.S.Z){case-1:throw new Error("closed");case 0:this.S.tt.push(e);break;case 1:this.ct(this.ut(this.S.dt,[e])),this.gt._t()}});ut(t,s){return this.et(()=>{for(const{lt:e,q:n,H:h}of s){if(e.length){const{S:s,$:n}=i(this.O,t,e);t=s,this.st.N(n)}this.st.U(t,n,h)}return t})}gt=function(t){let s=null;const i=()=>{null!==s&&(clearTimeout(s),s=null)},e=()=>{i(),t()};return{ft:e,_t:()=>{null===s&&(s=setTimeout(e,0))},o:i}}(()=>{this.k.isConnected()&&!this.Y&&this.st.X(this.k.send)});wt(t){if(-1!==this.S.Z){if(this.Y=!1,0===this.S.Z){const s=this.ut(t.init,this.S.tt);this.S={Z:1,vt:t.init,dt:s},this.ct(s,!0)}else this.S.vt=t.init,this.st.W(t.init),this.ct(this.st.V(t.init));this.gt.ft()}else this.bt(`Ignoring init after closing: ${JSON.stringify(t)}`)}yt(t){if(1!==this.S.Z)return void this.bt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.vt=this.O.update(this.S.vt,t.change),{K:i,R:e}=this.st.B(t.id);e||this.ct(this.st.V(s)),i?.q.forEach(t=>t(s))}Tt(t){if(1!==this.S.Z)return void this.bt(`Ignoring error before init: ${JSON.stringify(t)}`);const{K:s}=this.st.B(t.id);s?(this.bt(`API rejected update: ${t.error}`),s?.H.forEach(s=>s(t.error)),this.ct(this.st.V(this.S.vt))):this.bt(`API sent error: ${t.error}`)}Et(){this.k.send("x"),this.Y?this.bt("Unexpected extra close message"):(this.Y=!0,this.dispatchEvent(n("disconnected",f)))}nt=t=>{if("X"===t.detail)return void this.Et();const s=JSON.parse(t.detail);"change"in s?this.yt(s):"init"in s?this.wt(s):"error"in s?this.Tt(s):this.bt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.it.add(t),1===this.S.Z&&t(this.S.dt)}removeStateListener(t){this.it.delete(t)}ct(t,s=!1){if(1!==this.S.Z)throw new Error("invalid state");if(s||this.S.dt!==t){this.S.dt=t;for(const s of this.it)s(t)}}getState(){return 1===this.S.Z?this.S.dt:void 0}bt(t){this.dispatchEvent(n("warning",new Error(t)))}ht=()=>{this.dispatchEvent(n("connected"))};ot=t=>{this.dispatchEvent(n("warning",t.detail))};rt=t=>{this.Y||(this.Y=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.Y=!0,this.S={Z:-1},this.k.close(),this.gt.o(),this.it.clear(),this.k.removeEventListener("message",this.nt),this.k.removeEventListener("connected",this.ht),this.k.removeEventListener("connectionfailure",this.ot),this.k.removeEventListener("disconnected",this.rt)}}const f={code:0,reason:"graceful shutdown"},w=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{a as AT_LEAST_ONCE,u as AT_MOST_ONCE,t as OnlineScheduler,g as SharedReducer,s as exponentialDelay};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shared-reducer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "shared state management",
|
|
5
5
|
"author": "David Evans",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,16 +45,16 @@
|
|
|
45
45
|
"homepage": "https://github.com/davidje13/shared-reducer#readme",
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rollup/plugin-terser": "0.4.x",
|
|
48
|
-
"@rollup/plugin-typescript": "
|
|
48
|
+
"@rollup/plugin-typescript": "12.x",
|
|
49
49
|
"collection-storage": "3.x",
|
|
50
50
|
"json-immutability-helper": "4.x",
|
|
51
51
|
"lean-test": "2.x",
|
|
52
|
-
"prettier": "3.
|
|
52
|
+
"prettier": "3.6.2",
|
|
53
53
|
"rollup": "4.x",
|
|
54
54
|
"rollup-plugin-dts": "6.x",
|
|
55
55
|
"superwstest": "2.x",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
56
|
+
"typescript": "5.9.x",
|
|
57
|
+
"web-listener": "0.10.0",
|
|
58
|
+
"ws": "8.x"
|
|
59
59
|
}
|
|
60
60
|
}
|