shared-reducer 4.0.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,16 +15,15 @@ npm install --save shared-reducer json-immutability-helper
15
15
 
16
16
  ## Usage (Backend)
17
17
 
18
- This project is compatible with
19
- [websocket-express](https://github.com/davidje13/websocket-express),
18
+ This project is compatible with [websocket-express](https://github.com/davidje13/websocket-express),
20
19
  but can also be used in isolation.
21
20
 
22
21
  ### With websocket-express
23
22
 
24
- ```js
23
+ ```javascript
25
24
  import {
26
25
  Broadcaster,
27
- websocketHandler,
26
+ WebsocketHandlerFactory,
28
27
  InMemoryModel,
29
28
  ReadWrite,
30
29
  } from 'shared-reducer/backend';
@@ -32,60 +31,57 @@ import context from 'json-immutability-helper';
32
31
  import { WebSocketExpress } from 'websocket-express';
33
32
 
34
33
  const model = new InMemoryModel();
35
- const broadcaster = Broadcaster.for(model)
36
- .withReducer(context)
37
- .build();
34
+ const broadcaster = new Broadcaster(model, context);
38
35
  model.set('a', { foo: 'v1' });
39
36
 
40
37
  const app = new WebSocketExpress();
41
38
  const server = app.listen(0, 'localhost');
42
39
 
43
- const handler = websocketHandler(broadcaster);
44
- app.ws('/:id', handler((req) => req.params.id, () => ReadWrite));
40
+ const handlerFactory = new WebsocketHandlerFactory(broadcaster);
41
+ app.ws('/:id', handlerFactory.handler((req) => req.params.id, () => ReadWrite));
42
+
43
+ const server = app.listen();
44
+
45
+ // later, to shutdown gracefully:
46
+ // send a close signal to all clients and wait up to 1 second for acknowledgement:
47
+ await handlerFactory.close(1000);
48
+ server.close();
45
49
  ```
46
50
 
47
- For real use-cases, you will probably want to add authentication middleware
48
- to the expressjs chain, and you may want to give some users read-only and
49
- others read-write access, which can be achieved in the second lambda.
51
+ For real use-cases, you will probably want to add authentication middleware to the expressjs chain,
52
+ and you may want to give some users read-only and others read-write access, which can be achieved in
53
+ the second lambda.
50
54
 
51
55
  ### Alone
52
56
 
53
- ```js
57
+ ```javascript
54
58
  import { Broadcaster, InMemoryModel } from 'shared-reducer/backend';
55
59
  import context from 'json-immutability-helper';
56
60
 
57
61
  const model = new InMemoryModel();
58
- const broadcaster = Broadcaster.for(model)
59
- .withReducer(context)
60
- .build();
62
+ const broadcaster = new Broadcaster(model, context);
61
63
  model.set('a', { foo: 'v1' });
62
64
 
63
65
  // ...
64
66
 
65
- const subscription = await broadcaster.subscribe(
66
- 'a',
67
- (change, meta) => { /*...*/ },
68
- );
67
+ const subscription = await broadcaster.subscribe('a');
69
68
 
70
69
  const begin = subscription.getInitialData();
70
+ subscription.listen((change, meta) => { /*...*/ });
71
71
  await subscription.send(['=', { foo: 'v2' }]);
72
72
  // callback provided earlier is invoked
73
73
 
74
74
  await subscription.close();
75
75
  ```
76
76
 
77
- ## Persisting data
77
+ ### Persisting data
78
78
 
79
79
  A convenience wrapper is provided for use with
80
- [collection-storage](https://github.com/davidje13/collection-storage),
81
- or you can write your own implementation of the `Model` interface to
82
- link any backend.
80
+ [collection-storage](https://github.com/davidje13/collection-storage), or you can write your own
81
+ implementation of the `Model` interface to link any backend.
83
82
 
84
- ```js
85
- import {
86
- Broadcaster,
87
- CollectionStorageModel,
88
- } from 'shared-reducer/backend';
83
+ ```javascript
84
+ import { Broadcaster, CollectionStorageModel } from 'shared-reducer/backend';
89
85
  import context from 'json-immutability-helper';
90
86
  import CollectionStorage from 'collection-storage';
91
87
 
@@ -97,29 +93,38 @@ const model = new CollectionStorageModel(
97
93
  // or throws if invalid (protects stored data from malicious changes)
98
94
  MY_VALIDATOR,
99
95
  );
100
- const broadcaster = Broadcaster.for(model)
101
- .withReducer(context)
102
- .build();
96
+ const broadcaster = new Broadcaster(model, context);
103
97
  ```
104
98
 
105
- Note that the provided validator MUST verify structural integrity (e.g.
106
- ensuring no unexpected fields are added or types are changed).
99
+ Note that the provided validator MUST verify structural integrity (e.g. ensuring no unexpected
100
+ fields are added or types are changed).
107
101
 
108
102
  ## Usage (Frontend)
109
103
 
110
104
  ```javascript
111
- import { SharedReducer, actionsHandledCallback, actionsSyncedCallback } from 'shared-reducer/frontend';
105
+ import { SharedReducer } from 'shared-reducer/frontend';
112
106
  import context from 'json-immutability-helper';
113
107
 
114
- const reducer = SharedReducer
115
- .for('ws://destination', (state) => {
116
- console.log('latest state is', state);
117
- })
118
- .withReducer(context)
119
- .withToken('my-token')
120
- .withErrorHandler((error) => { console.log('connection lost', error); })
121
- .withWarningHandler((warning) => { console.log('latest change failed', warning); })
122
- .build();
108
+ const reducer = new SharedReducer(context, () => ({
109
+ url: 'ws://destination',
110
+ token: 'my-token',
111
+ }));
112
+
113
+ reducer.addStateListener((state) => {
114
+ console.log('latest state is', state);
115
+ });
116
+
117
+ reducer.addEventListener('connected', () => {
118
+ console.log('connected / reconnected');
119
+ });
120
+
121
+ reducer.addEventListener('disconnected', (e) => {
122
+ console.log('connection lost', e.detail.code, e.detail.reason);
123
+ });
124
+
125
+ reducer.addEventListener('warning', (e) => {
126
+ console.log('latest change failed', e.detail);
127
+ });
123
128
 
124
129
  const dispatch = reducer.dispatch;
125
130
 
@@ -129,23 +134,22 @@ dispatch([
129
134
 
130
135
  dispatch([
131
136
  (state) => {
132
- return {
133
- a: ['=', Math.pow(2, state.a)],
134
- };
137
+ return [{ a: ['=', Math.pow(2, state.a)] }];
135
138
  },
136
139
  ]);
137
140
 
138
141
  dispatch([
139
- actionsHandledCallback((state) => {
142
+ (state) => {
140
143
  console.log('state after handling is', state);
141
- }),
144
+ return [];
145
+ },
142
146
  ]);
143
147
 
144
- dispatch([
145
- actionsSyncedCallback((state) => {
146
- console.log('state after syncing is', state);
147
- }),
148
- ]);
148
+ dispatch(
149
+ [{ a: ['add', 1] }],
150
+ (state) => console.log('state after syncing is', state),
151
+ (message) => console.warn('failed to sync', message),
152
+ );
149
153
 
150
154
  dispatch([
151
155
  { a: ['add', 1] },
@@ -155,8 +159,7 @@ dispatch([
155
159
 
156
160
  ### Specs
157
161
 
158
- The specs need to match whichever reducer you are using. In the examples
159
- above, that is
162
+ The specs need to match whichever reducer you are using. In the examples above, that is
160
163
  [json-immutability-helper](https://github.com/davidje13/json-immutability-helper).
161
164
 
162
165
  ## WebSocket protocol
@@ -165,101 +168,86 @@ The websocket protocol is minimal:
165
168
 
166
169
  ### Client-to-server
167
170
 
168
- `<token>`:
169
- The authentication token is sent as the first message when the connection is
170
- established. This is plaintext. The server should respond by either terminating
171
- the connection (if the token is deemed invalid), or with an `init` event which
172
- defines the latest state in its entirety. If no token is specified using
173
- `withToken`, no message will be sent (when not using authentication, it is
174
- assumed the server will send the `init` event unprompted).
171
+ - `<token>`: The authentication token is sent as the first message when the connection is
172
+ established. This is plaintext. The server should respond by either terminating the connection (if
173
+ the token is deemed invalid), or with an `init` event which defines the latest state in its
174
+ entirety. If no token is specified using `withToken`, no message will be sent (when not using
175
+ authentication, it is assumed the server will send the `init` event unprompted).
176
+
177
+ - `P` (ping): Can be sent periodically to keep the connection alive. The server sends a "Pong"
178
+ message in response immediately.
175
179
 
176
- `P` (ping):
177
- Can be sent periodically to keep the connection alive. The server sends a
178
- "Pong" message in response immediately.
180
+ - `{"change": <spec>, "id": <id>}`: Defines a delta. This may contain the aggregate result of many
181
+ operations performed on the client. The ID is an opaque identifier which is reflected back to the
182
+ same client in the confirmation message. Other clients will not receive the ID.
179
183
 
180
- `{"change": <spec>, "id": <id>}`:
181
- Defines a delta. This may contain the aggregate result of many operations
182
- performed on the client. The ID is an opaque identifier which is reflected
183
- back to the same client in the confirmation message. Other clients will not
184
- receive the ID.
184
+ - `x` (close ack): Sent by the client in response to `X` (closing). Indicates that the client will
185
+ not send any more messages on this connection (but may still be expecting some responses to
186
+ existing messages).
185
187
 
186
188
  ### Server-to-client
187
189
 
188
- `p` (pong):
189
- Reponse to a ping. May also be sent unsolicited.
190
+ - `p` (pong): Reponse to a ping. May also be sent unsolicited.
190
191
 
191
- `{"init": <state>}`:
192
- The first message sent by the server, in response to a successful
193
- connection.
192
+ - `{"init": <state>}`: The first message sent by the server, in response to a successful connection.
194
193
 
195
- `{"change": <spec>}`:
196
- Sent whenever another client has changed the server state.
194
+ - `{"change": <spec>}`: Sent whenever another client has changed the server state.
197
195
 
198
- `{"change": <spec>, "id": <id>}`:
199
- Sent whenever the current client has changed the server state. Note that
200
- the spec and ID will match the client-sent values.
196
+ - `{"change": <spec>, "id": <id>}`: Sent whenever the current client has changed the server state.
197
+ Note that the spec and ID will match the client-sent values.
201
198
 
202
- The IDs sent by different clients can coincide, so the ID is only reflected
203
- to the client which sent the spec.
199
+ The IDs sent by different clients can coincide, so the ID is only reflected to the client which
200
+ sent the spec.
204
201
 
205
- `{"error": <message>, "id": <id>}`:
206
- Sent if the server rejects a client-initiated change.
202
+ - `{"error": <message>, "id": <id>}`: Sent if the server rejects a client-initiated change.
207
203
 
208
- If this is returned, the server state will not have changed (i.e. the
209
- entire spec failed).
204
+ If this is returned, the server state will not have changed (i.e. the entire spec failed).
205
+
206
+ - `X` (closing): Sent when the server is about to shut down. The client should respond with `x` and
207
+ not send any more messages on the current connection. Any currently in-flight messages will be
208
+ acknowledged on a best-effort basis by the server. The server might not wait for the acknowledging
209
+ `x` message before closing the connection.
210
210
 
211
211
  ### Specs
212
212
 
213
- The specs need to match whichever reducer you are using. In the examples
214
- above, that is
213
+ The specs need to match whichever reducer you are using. In the examples above, that is
215
214
  [json-immutability-helper](https://github.com/davidje13/json-immutability-helper).
216
215
 
217
216
  ## Alternative reducer
218
217
 
219
- To enable different features of `json-immutability-helper`, you can
220
- customise it before passing it to `withReducer`. For example, to
221
- enable list commands such as `updateWhere` and mathematical commands
222
- such as Reverse Polish Notation (`rpn`):
223
-
224
- ### Backend
218
+ To enable different features of `json-immutability-helper`, you can customise it before passing it
219
+ to the constructor. For example, to enable list commands such as `updateWhere` and mathematical
220
+ commands such as Reverse Polish Notation (`rpn`):
225
221
 
226
- ```js
222
+ ```javascript
223
+ // Backend
227
224
  import { Broadcaster, InMemoryModel } from 'shared-reducer/backend';
228
225
  import listCommands from 'json-immutability-helper/commands/list';
229
226
  import mathCommands from 'json-immutability-helper/commands/math';
230
227
  import context from 'json-immutability-helper';
231
228
 
232
- const broadcaster = Broadcaster.for(new InMemoryModel())
233
- .withReducer(context.with(listCommands, mathCommands))
234
- .build();
235
- ```
236
-
237
- If you want to use an entirely different reducer, create a wrapper
238
- and pass it to `withReducer`:
239
-
240
- ```js
241
- import { Broadcaster, InMemoryModel } from 'shared-reducer/backend';
242
-
229
+ const broadcaster = new Broadcaster(
230
+ new InMemoryModel(),
231
+ context.with(listCommands, mathCommands),
232
+ );
243
233
  ```
244
234
 
245
- ### Frontend
246
-
247
- ```js
235
+ ```javascript
236
+ // Frontend
248
237
  import { SharedReducer } from 'shared-reducer/frontend';
249
238
  import listCommands from 'json-immutability-helper/commands/list';
250
239
  import mathCommands from 'json-immutability-helper/commands/math';
251
240
  import context from 'json-immutability-helper';
252
241
 
253
- const reducer = SharedReducer
254
- .for('ws://destination', (state) => {})
255
- .withReducer(context.with(listCommands, mathCommands))
256
- .build();
242
+ const reducer = new SharedReducer(
243
+ context.with(listCommands, mathCommands),
244
+ () => ({ url: 'ws://destination' }),
245
+ );
257
246
  ```
258
247
 
259
- If you want to use an entirely different reducer, create a wrapper
260
- and pass it to `withReducer`:
248
+ If you want to use an entirely different reducer, create a wrapper:
261
249
 
262
- ```js
250
+ ```javascript
263
251
  import context from 'json-immutability-helper';
264
252
 
265
253
  const myReducer = {
@@ -273,31 +261,29 @@ const myReducer = {
273
261
  },
274
262
  };
275
263
 
276
- // backend
277
- const broadcaster = Broadcaster.for(new InMemoryModel())
278
- .withReducer(myReducer)
279
- .build();
264
+ // Backend
265
+ const broadcaster = new Broadcaster(new InMemoryModel(), myReducer);
280
266
 
281
- // frontend
282
- const reducer = SharedReducer
283
- .for('ws://destination', (state) => {})
284
- .withReducer(myReducer)
285
- .build();
267
+ // Frontend
268
+ const reducer = new SharedReducer(myReducer, () => ({ url: 'ws://destination' }));
286
269
  ```
287
270
 
288
- Be careful when using your own reducer to avoid introducing
289
- security vulnerabilities; the functions will be called with
290
- untrusted input, so should be careful to avoid attacks such
291
- as code injection or prototype pollution.
271
+ Be careful when using your own reducer to avoid introducing security vulnerabilities; the functions
272
+ will be called with untrusted input, so should be careful to avoid attacks such as code injection or
273
+ prototype pollution.
292
274
 
293
275
  ## Other customisations (Backend)
294
276
 
295
- The `Broadcaster` builder has other settable properties:
277
+ The `Broadcaster` constructor can also take some optional arguments:
278
+
279
+ ```javascript
280
+ new Broadcaster(model, reducer[, options]);
281
+ ```
296
282
 
297
- - `withSubscribers`: specify a custom keyed broadcaster, used
298
- for communicating changes to all consumers. Required interface:
283
+ - `options.subscribers`: specify a custom keyed broadcaster, used for communicating changes to all
284
+ consumers. Required interface:
299
285
 
300
- ```js
286
+ ```javascript
301
287
  {
302
288
  add(key, listener) {
303
289
  // add the listener function to key
@@ -314,15 +300,14 @@ The `Broadcaster` builder has other settable properties:
314
300
 
315
301
  All functions can be asynchronous or synchronous.
316
302
 
317
- The main use-case for overriding this would be to share
318
- messages between multiple servers for load balancing, but
319
- note that in most cases you probably want to load balance
320
- _documents_ rather than _users_ for better scalability.
303
+ The main use-case for overriding this would be to share messages between multiple servers for load
304
+ balancing, but note that in most cases you probably want to load balance _documents_ rather than
305
+ _users_ for better scalability.
321
306
 
322
- - `withTaskQueues`: specify a custom task queue, used to ensure
323
- operations happen in the correct order. Required interface:
307
+ - `options.taskQueues`: specify a custom task queue, used to ensure operations happen in the correct
308
+ order. Required interface:
324
309
 
325
- ```js
310
+ ```javascript
326
311
  {
327
312
  push(key, task) {
328
313
  // add the (possibly asynchronous) task to the queue
@@ -331,33 +316,80 @@ The `Broadcaster` builder has other settable properties:
331
316
  }
332
317
  ```
333
318
 
334
- The default implementation will execute the task if it is
335
- the first task in a particular queue. If there is already
336
- a task in the queue, it will be stored and executed once
337
- the existing tasks have finished. Once all tasks for a
338
- particular key have finished, it will remove the queue.
319
+ The default implementation will execute the task if it is the first task in a particular queue. If
320
+ there is already a task in the queue, it will be stored and executed once the existing tasks have
321
+ finished. Once all tasks for a particular key have finished, it will remove the queue.
339
322
 
340
- As with `withSubscribers`, the main reason to override
341
- this is to provide consistency if multiple servers are
342
- able to modify the same document simultaneously.
323
+ As with `subscribers`, the main reason to override this is to provide consistency if multiple
324
+ servers are able to modify the same document simultaneously.
343
325
 
344
- - `withIdProvider`: specify a custom unique ID provider.
345
- Required interface:
326
+ - `options.idProvider`: specify a custom unique ID provider. Must be a function which returns a
327
+ unique string ID when called. Can be asynchronous.
346
328
 
347
- ```js
348
- {
349
- get() {
350
- // return a unique string (must be synchronous)
351
- },
352
- }
353
- ```
329
+ The returned ID is used internally and passed through the configured `taskQueues` to identify the
330
+ source of a change. It is not revealed to users. The default implementation uses a fixed random
331
+ prefix followed by an incrementing number, which should be sufficient for most use cases.
332
+
333
+ ## Other customisations (Frontend)
334
+
335
+ If the connection is lost, the frontend will attempt to reconnect automatically. By default this
336
+ uses an exponential backoff with a small amount of randomness, as well as attempting to connect if
337
+ the page regains focus or the computer rejoins a network. You can fully customise this behaviour:
338
+
339
+ ```javascript
340
+ import { SharedReducer, OnlineScheduler, exponentialDelay } from 'shared-reducer/frontend';
341
+
342
+ const reducer = new SharedReducer(context, () => ({ url: 'ws://destination' }), {
343
+ scheduler: new OnlineScheduler(
344
+ exponentialDelay({
345
+ base: 2,
346
+ initialDelay: 200,
347
+ maxDelay: 10 * 60 * 1000,
348
+ randomness: 0.3,
349
+ }),
350
+ 20 * 1000, // timeout for each connection attempt
351
+ ),
352
+ });
353
+ ```
354
+
355
+ The `exponentialDelay` helper returns:
356
+
357
+ ```
358
+ min(initialDelay * (base ^ attempt), maxDelay) * (1 - random(randomness))
359
+ ```
360
+
361
+ All delay values are in milliseconds.
362
+
363
+ You can also provide a custom function instead of `exponentialDelay`; it will be given the current
364
+ attempt number (0-based), and should return the number of milliseconds to wait before triggering the
365
+ attempt.
366
+
367
+ Finally, by default when reconnecting `SharedReducer` will replay all messages which have not been
368
+ confirmed (`AT_LEAST_ONCE` delivery). You can change this to `AT_MOST_ONCE` or a custom mechanism:
369
+
370
+ ```javascript
371
+ import { SharedReducer, AT_MOST_ONCE } from 'shared-reducer/frontend';
372
+
373
+ const reducer = new SharedReducer(context, () => ({ url: 'ws://destination' }), {
374
+ deliveryStrategy: AT_MOST_ONCE,
375
+ });
376
+ ```
377
+
378
+ Custom strategies can be defined as functions:
379
+
380
+ ```javascript
381
+ function myCustomDeliveryStrategy(serverState, spec, hasSent) {
382
+ return true; // re-send all (equivalent to AT_LEAST_ONCE)
383
+ }
384
+ ```
385
+
386
+ - `serverState` is the new state from the server after reconnecting.
387
+ - `spec` is a spec that has not been confirmed as delivered to the server.
388
+ - `hasSent` is `true` if the spec has already been sent to the server (but no delivery confirmation
389
+ was received). It is `false` if the message was never sent to the server.
354
390
 
355
- The returned ID is used internally and passed through
356
- the configured `taskQueues` to identify the source of
357
- a change. It is not revealed to users. The default
358
- implementation uses a fixed random prefix followed by
359
- an incrementing number, which should be sufficient for
360
- most use cases.
391
+ Note that the function will be invoked multiple times (once for each change that is pending). It
392
+ should return `true` for messages to resend, and `false` for messages to drop.
361
393
 
362
394
  ## Older versions
363
395
 
@@ -1,33 +1,30 @@
1
- declare class UniqueIdProvider {
2
- private readonly _shared;
3
- private _unique;
4
- get(): string;
5
- }
1
+ type MaybePromise<T> = Promise<T> | T;
6
2
 
7
- type Task<T> = () => Promise<T>;
8
- interface TaskQueue<T> extends EventTarget {
9
- push(task: Task<T>): Promise<T>;
3
+ type Task<T> = () => MaybePromise<T>;
4
+ interface TaskQueue extends EventTarget {
5
+ push<T>(task: Task<T>): Promise<T>;
6
+ active(): boolean;
10
7
  }
11
- type TaskQueueFactory<T> = () => TaskQueue<T>;
8
+ type TaskQueueFactory = () => TaskQueue;
12
9
 
13
- declare class TaskQueueMap<T> {
10
+ declare class TaskQueueMap<K> {
14
11
  private readonly _queueFactory;
15
12
  private readonly _queues;
16
- constructor(_queueFactory?: () => TaskQueue<T>);
17
- push(key: string, task: Task<T>): Promise<T>;
13
+ constructor(_queueFactory?: () => TaskQueue);
14
+ push<T>(key: K, task: Task<T>): Promise<T>;
18
15
  }
19
16
 
20
17
  type TopicListener<T> = (message: T) => void;
21
18
  interface Topic<T> {
22
- add(fn: TopicListener<T>): Promise<void> | void;
23
- remove(fn: TopicListener<T>): Promise<boolean> | boolean;
24
- broadcast(message: T): Promise<void> | void;
19
+ add(fn: TopicListener<T>): MaybePromise<void>;
20
+ remove(fn: TopicListener<T>): MaybePromise<boolean>;
21
+ broadcast(message: T): MaybePromise<void>;
25
22
  }
26
23
 
27
- interface TopicMap<T> {
28
- add(key: string, fn: TopicListener<T>): Promise<void> | void;
29
- remove(key: string, fn: TopicListener<T>): Promise<void> | void;
30
- broadcast(key: string, message: T): Promise<void> | void;
24
+ interface TopicMap<K, T> {
25
+ add(key: K, fn: TopicListener<T>): MaybePromise<void>;
26
+ remove(key: K, fn: TopicListener<T>): MaybePromise<void>;
27
+ broadcast(key: K, message: T): MaybePromise<void>;
31
28
  }
32
29
 
33
30
  declare class PermissionError extends Error {
@@ -37,19 +34,21 @@ interface Permission<T, SpecT> {
37
34
  validateWrite(newValue: T, oldValue: T): void;
38
35
  }
39
36
 
40
- interface Model<T> {
37
+ interface Model<ID, T> {
41
38
  validate(v: unknown): Readonly<T>;
42
- read(id: string): Promise<Readonly<T> | null | undefined> | Readonly<T> | null | undefined;
43
- write(id: string, newValue: T, oldValue: T): Promise<void> | void;
39
+ read(id: ID): MaybePromise<Readonly<T> | null | undefined>;
40
+ write(id: ID, newValue: T, oldValue: T): MaybePromise<void>;
44
41
  }
45
42
 
46
43
  interface Context<T, SpecT> {
47
44
  update: (input: T, spec: SpecT) => T;
48
45
  }
46
+ type Listener<SpecT, MetaT> = (message: ChangeInfo<SpecT>, meta: MetaT | undefined) => void;
49
47
  interface Subscription<T, SpecT, MetaT> {
50
- getInitialData: () => Readonly<T>;
51
- send: (change: SpecT, meta?: MetaT) => Promise<void>;
52
- close: () => Promise<void>;
48
+ getInitialData(): Readonly<T>;
49
+ listen(onChange: Listener<SpecT, MetaT>): void;
50
+ send(change: SpecT, meta?: MetaT): Promise<void>;
51
+ close(): Promise<void>;
53
52
  }
54
53
  type Identifier = string | null;
55
54
  type ChangeInfo<SpecT> = {
@@ -64,67 +63,88 @@ interface TopicMessage<SpecT> {
64
63
  source: Identifier;
65
64
  meta?: unknown;
66
65
  }
67
- interface BroadcasterBuilder<T, SpecT> {
68
- withReducer<SpecT2 extends SpecT>(context: Context<T, SpecT2>): BroadcasterBuilder<T, SpecT2>;
69
- withSubscribers(subscribers: TopicMap<TopicMessage<SpecT>>): this;
70
- withTaskQueues(taskQueues: TaskQueueMap<void>): this;
71
- withIdProvider(idProvider: UniqueIdProvider): this;
72
- build(): Broadcaster<T, SpecT>;
73
- }
66
+ type ID = string;
74
67
  declare class Broadcaster<T, SpecT> {
75
68
  private readonly _model;
76
69
  private readonly _context;
77
70
  private readonly _subscribers;
78
71
  private readonly _taskQueues;
79
72
  private readonly _idProvider;
80
- private constructor();
81
- static for<T2>(model: Model<T2>): BroadcasterBuilder<T2, unknown>;
82
- subscribe<MetaT>(id: string, onChange: (message: ChangeInfo<SpecT>, meta: MetaT | undefined) => void, permission?: Permission<T, SpecT>): Promise<Subscription<T, SpecT, MetaT> | null>;
83
- update(id: string, change: SpecT, permission?: Permission<T, SpecT>): Promise<void>;
73
+ constructor(_model: Model<ID, T>, _context: Context<T, SpecT>, options?: {
74
+ subscribers?: TopicMap<ID, TopicMessage<SpecT>>;
75
+ taskQueues?: TaskQueueMap<ID>;
76
+ idProvider?: () => MaybePromise<string>;
77
+ });
78
+ subscribe<MetaT = void>(id: ID, permission?: Permission<T, SpecT>): Promise<Subscription<T, SpecT, MetaT> | null>;
79
+ update(id: ID, change: SpecT, permission?: Permission<T, SpecT>): Promise<void>;
84
80
  private _internalApplyChange;
85
81
  private _internalQueueChange;
86
82
  }
87
83
 
88
84
  declare const PING = "P";
89
85
  declare const PONG = "p";
90
- type MaybePromise<T> = Promise<T> | T;
86
+ declare const CLOSE = "X";
87
+ declare const CLOSE_ACK = "x";
91
88
  interface ServerWebSocket {
92
89
  on(event: 'close', listener: () => void): void;
93
90
  on(event: 'message', listener: (data: unknown, isBinary?: boolean) => void): void;
91
+ on(event: 'pong', listener: () => void): void;
92
+ ping(): void;
94
93
  send(message: string): void;
94
+ terminate(): void;
95
95
  }
96
96
  interface WSResponse {
97
97
  accept(): Promise<ServerWebSocket>;
98
- sendError(httpStatus: number): void;
98
+ sendError(httpStatus: number, wsStatus?: number): void;
99
99
  beginTransaction(): void;
100
100
  endTransaction(): void;
101
101
  }
102
- declare const websocketHandler: <T, SpecT>(broadcaster: Broadcaster<T, SpecT>) => <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>;
102
+ interface WebsocketHandlerOptions {
103
+ pingInterval?: number;
104
+ pongTimeout?: number;
105
+ }
106
+ declare class WebsocketHandlerFactory<T, SpecT> {
107
+ private readonly broadcaster;
108
+ private readonly closers;
109
+ private readonly _pingInterval;
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>;
116
+ }
117
+
118
+ declare const UniqueIdProvider: () => () => string;
103
119
 
104
120
  interface Collection<T> {
105
121
  get<K extends keyof T & string>(searchAttribute: K, searchValue: T[K]): Promise<Readonly<T> | null>;
106
122
  update<K extends keyof T & string>(searchAttribute: K, searchValue: T[K], update: Partial<T>): Promise<void>;
107
123
  }
108
- declare class CollectionStorageModel<T extends object> implements Model<T> {
124
+ type ErrorMapper = (e: unknown) => unknown;
125
+ declare class CollectionStorageModel<T extends object, K extends keyof T & string> implements Model<T[K], T> {
109
126
  private readonly _collection;
110
127
  private readonly _idCol;
111
128
  readonly validate: (v: unknown) => T;
112
129
  private readonly _readErrorIntercept;
113
130
  private readonly _writeErrorIntercept;
114
- constructor(_collection: Collection<T>, _idCol: keyof T & string, validate: (v: unknown) => T, _readErrorIntercept?: (e: Error) => Error, _writeErrorIntercept?: (e: Error) => Error);
115
- read(id: string): Promise<Readonly<T> | null>;
116
- write(id: string, newValue: T, oldValue: T): Promise<void>;
131
+ constructor(_collection: Collection<T>, _idCol: keyof T & string, validate: (v: unknown) => T, options?: {
132
+ readErrorIntercept?: ErrorMapper;
133
+ writeErrorIntercept?: ErrorMapper;
134
+ });
135
+ read(id: T[K]): Promise<Readonly<T> | null>;
136
+ write(id: T[K], newValue: T, oldValue: T): Promise<void>;
117
137
  }
118
138
 
119
- declare class InMemoryModel<T> implements Model<T> {
120
- readonly read: (id: string) => Readonly<T> | undefined;
139
+ declare class InMemoryModel<ID, T> implements Model<ID, T> {
140
+ readonly read: (id: ID) => Readonly<T> | undefined;
121
141
  readonly validate: (v: unknown) => T;
122
142
  private readonly _memory;
123
143
  constructor(validator?: (x: unknown) => T);
124
- set(id: string, value: T): void;
125
- get(id: string): Readonly<T> | undefined;
126
- delete(id: string): void;
127
- write(id: string, newValue: T, oldValue: T): void;
144
+ set(id: ID, value: T): void;
145
+ get(id: ID): Readonly<T> | undefined;
146
+ delete(id: ID): void;
147
+ write(id: ID, newValue: T, oldValue: T): void;
128
148
  }
129
149
 
130
150
  declare const ReadOnly: Permission<unknown, unknown>;
@@ -137,11 +157,12 @@ declare class ReadWriteStruct<T extends object> implements Permission<T, unknown
137
157
  validateWrite(newValue: T, oldValue: T): void;
138
158
  }
139
159
 
140
- declare class AsyncTaskQueue<T> extends EventTarget implements TaskQueue<T> {
160
+ declare class AsyncTaskQueue extends EventTarget implements TaskQueue {
141
161
  private readonly _queue;
142
162
  private _running;
143
- push(task: Task<T>): Promise<T>;
163
+ push<T>(task: Task<T>): Promise<T>;
144
164
  private _internalConsumeQueue;
165
+ active(): boolean;
145
166
  }
146
167
 
147
168
  declare class InMemoryTopic<T> implements Topic<T> {
@@ -151,13 +172,13 @@ declare class InMemoryTopic<T> implements Topic<T> {
151
172
  broadcast(message: T): void;
152
173
  }
153
174
 
154
- declare class TrackingTopicMap<T> implements TopicMap<T> {
175
+ declare class TrackingTopicMap<K, T> implements TopicMap<K, T> {
155
176
  private readonly _topicFactory;
156
177
  private readonly _data;
157
- constructor(_topicFactory: (key: string) => Topic<T>);
158
- add(key: string, fn: TopicListener<T>): Promise<void>;
159
- remove(key: string, fn: TopicListener<T>): Promise<void>;
160
- broadcast(key: string, message: T): Promise<void>;
178
+ constructor(_topicFactory: (key: K) => Topic<T>);
179
+ add(key: K, fn: TopicListener<T>): Promise<void>;
180
+ remove(key: K, fn: TopicListener<T>): Promise<void>;
181
+ broadcast(key: K, message: T): Promise<void>;
161
182
  }
162
183
 
163
- export { AsyncTaskQueue, Broadcaster, type ChangeInfo, CollectionStorageModel, type Context, InMemoryModel, InMemoryTopic, type Model, PING, PONG, type Permission, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, type Subscription, type Task, type TaskQueue, type TaskQueueFactory, TaskQueueMap, type Topic, type TopicMap, type TopicMessage, TrackingTopicMap, UniqueIdProvider, websocketHandler };
184
+ export { AsyncTaskQueue, Broadcaster, CLOSE, CLOSE_ACK, type ChangeInfo, CollectionStorageModel, type Context, InMemoryModel, InMemoryTopic, type Model, PING, PONG, type Permission, PermissionError, ReadOnly, ReadWrite, ReadWriteStruct, type Subscription, type Task, type TaskQueue, type TaskQueueFactory, TaskQueueMap, type Topic, type TopicMap, type TopicMessage, TrackingTopicMap, UniqueIdProvider, WebsocketHandlerFactory };
package/backend/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("node:crypto");class e{t=t.randomUUID().substring(0,8);i=0;get(){const t=this.i;return this.i++,`${this.t}-${t}`}}class r extends EventTarget{o=[];h=!1;push(t){return new Promise(((e,r)=>{this.o.push({task:t,resolve:e,reject:r}),this.h||this.u()}))}async u(){for(this.h=!0;this.o.length>0;){const{task:t,resolve:e,reject:r}=this.o.shift();let s=null,i=!1;try{s=await t(),i=!0}catch(t){r(t)}i&&e(s)}this.h=!1,this.dispatchEvent(new CustomEvent("drain"))}}class s{l;p=new Map;constructor(t=()=>new r){this.l=t}push(t,e){let r=this.p.get(t);return r||(r=this.l(),r.addEventListener("drain",(()=>{this.p.delete(t)})),this.p.set(t,r)),r.push(e)}}class i{v;_=new Map;constructor(t){this.v=t}async add(t,e){let r=this._.get(t);r||(r=this.v(t),this._.set(t,r)),await r.add(e)}async remove(t,e){const r=this._.get(t);if(r){await r.remove(e)||this._.delete(t)}}async broadcast(t,e){const r=this._.get(t);r&&await r.broadcast(e)}}class n{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach((e=>e(t)))}}const a={validateWrite(){}};class o{O;j;m;C;S;constructor(t,e,r,s,i){this.O=t,this.j=e,this.m=r,this.C=s,this.S=i}static for(t){let r,a,c,h;return{withReducer(t){return r=t,this},withSubscribers(t){return a=t,this},withTaskQueues(t){return c=t,this},withIdProvider(t){return h=t,this},build(){if(!r)throw new Error("must set broadcaster context");return new o(t,r,a||new i((()=>new n)),c||new s,h||new e)}}}async subscribe(t,e,r=a){let s=await this.O.read(t);if(null==s)return null;const i=this.S.get(),n=({message:t,source:r,meta:s})=>{r===i?e(t,s):t.change&&e(t,void 0)};return this.m.add(t,n),{getInitialData:()=>{if(null===s)throw new Error("Already fetched initialData");const t=s;return s=null,t},send:(e,s)=>this.I(t,e,r,i,s),close:async()=>{await this.m.remove(t,n)}}}update(t,e,r=a){return this.I(t,e,r,null,void 0)}async q(t,e,r,s,i){try{const s=await this.O.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const i=this.j.update(s,e),n=this.O.validate(i);r.validateWrite(n,s),await this.O.write(t,n,s)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:i})}this.m.broadcast(t,{message:{change:e},source:s,meta:i})}async I(t,e,r,s,i){return this.C.push(t,(()=>this.q(t,e,r,s,i)))}}function c(t){if("object"!=typeof(e=t)||null===e||Array.isArray(e))throw new Error("Must specify change and optional id");var e;const{id:r,change:s}=t;if(void 0===r)return{change:s};if("number"!=typeof r)throw new Error("if specified, id must be a number");return{change:s,id:r}}function h(t){return{error:t instanceof Error?t.message:"Internal error"}}const u=t=>t;class l extends Error{}const d="Cannot modify data",w={validateWriteSpec(){throw new l(d)},validateWrite(){throw new l(d)}};exports.AsyncTaskQueue=r,exports.Broadcaster=o,exports.CollectionStorageModel=class{J;N;validate;P;$;constructor(t,e,r,s=u,i=u){this.J=t,this.N=e,this.validate=r,this.P=s,this.$=i}async read(t){try{return await this.J.get(this.N,t)}catch(t){throw this.P(t)}}async write(t,e,r){const s={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)}));try{await this.J.update(this.N,t,s)}catch(t){throw this.$(t)}}},exports.InMemoryModel=class{read=this.get;validate;k=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.k.set(t,e)}get(t){return this.k.get(t)}delete(t){this.k.delete(t)}write(t,e,r){if(r!==this.k.get(t))throw new Error("Unexpected previous value");this.k.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=l,exports.ReadOnly=w,exports.ReadWrite=a,exports.ReadWriteStruct=class{M;constructor(t=[]){this.M=t}validateWrite(t,e){Object.keys(e).forEach((e=>{const r=e;if(!Object.prototype.hasOwnProperty.call(t,r)&&this.M.includes(r))throw new l(`Cannot remove field ${r}`)})),Object.keys(t).forEach((r=>{const s=r;if(this.M.includes(s)){if(!Object.prototype.hasOwnProperty.call(e,s))throw new l(`Cannot add field ${s}`);if(t[s]!==e[s])throw new l(`Cannot edit field ${s}`)}}))}},exports.TaskQueueMap=s,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.websocketHandler=t=>(e,r)=>async(s,i)=>{const n=await i.accept(),a=(t,e)=>{const r=void 0!==e?{id:e,...t}:t;n.send(JSON.stringify(r))};let o;try{const n=await e(s,i),c=await r(s,i);o=await t.subscribe(n,a,c)}catch(t){return void n.send(JSON.stringify(h(t)))}o?(n.on("close",o.close),n.on("message",((t,e)=>{try{if(e)return;const r=String(t);if("P"===r)return void n.send("p");const s=function(t){return c(JSON.parse(t))}(r);i.beginTransaction(),o.send(s.change,s.id).finally((()=>i.endTransaction()))}catch(t){n.send(JSON.stringify(h(t)))}})),n.send(JSON.stringify({init:o.getInitialData()}))):i.sendError(404)};
1
+ "use strict";var t=require("node:crypto");const e=()=>{const e=t.randomUUID().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;l=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.l.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.l.set(t,e),s=e}return s.push(e)}}class i{u;p=new Map;constructor(t){this.u=t}async add(t,e){let s=this.p.get(t);s||(s=this.u(t),this.p.set(t,s)),await s.add(e)}async remove(t,e){const s=this.p.get(t);if(s){await s.remove(e)||this.p.delete(t)}}async broadcast(t,e){const s=this.p.get(t);s&&await s.broadcast(e)}}class n{m=new Set;add(t){this.m.add(t)}remove(t){return this.m.delete(t),this.m.size>0}broadcast(t){this.m.forEach((e=>e(t)))}}const o={validateWrite(){}};const a=t=>t;class c extends Error{}const h="Cannot modify data",l={validateWriteSpec(){throw new c(h)},validateWrite(){throw new c(h)}};exports.AsyncTaskQueue=s,exports.Broadcaster=class{v;_;m;S;C;constructor(t,s,o={}){this.v=t,this._=s,this.m=o.subscribers??new i((()=>new n)),this.S=o.taskQueues??new r,this.C=o.idProvider??e()}async subscribe(t,e=o){let s={T:0},r="";const i=t=>{2===s.T?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.T&&s.t.push(t)};try{if(await this.S.push(t,(async()=>{const e=await this.v.read(t);null!=e&&(s={T:1,P:e,t:[]},await this.m.add(t,i))})),0===s.T)return null;r=await this.C()}catch(e){throw await this.m.remove(t,i),e}return{getInitialData(){if(1!==s.T)throw new Error("Already started");return s.P},listen(t){if(1!==s.T)throw new Error("Already started");const e=s.t;s={T:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.m.remove(t,i)}}}update(t,e,s=o){return this.j(t,e,s,null,void 0)}async I(t,e,s,r,i){try{const r=await this.v.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this._.update(r,e),n=this.v.validate(i);s.validateWrite(n,r),await this.v.write(t,n,r)}catch(e){return void this.m.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.m.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.S.push(t,(()=>this.I(t,e,s,r,i)))}},exports.CLOSE="X",exports.CLOSE_ACK="x",exports.CollectionStorageModel=class{W;k;validate;q;$;constructor(t,e,s,r={}){this.W=t,this.k=e,this.validate=s,this.q=r.readErrorIntercept??a,this.$=r.writeErrorIntercept??a}async read(t){try{return await this.W.get(this.k,t)}catch(t){throw this.q(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.W.update(this.k,t,r)}catch(t){throw this.$(t)}}},exports.InMemoryModel=class{read=this.get;validate;A=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.A.set(t,e)}get(t){return this.A.get(t)}delete(t){this.A.delete(t)}write(t,e,s){if(s!==this.A.get(t))throw new Error("Unexpected previous value");this.A.set(t,e)}},exports.InMemoryTopic=n,exports.PING="P",exports.PONG="p",exports.PermissionError=c,exports.ReadOnly=l,exports.ReadWrite=o,exports.ReadWriteStruct=class{J;constructor(t=[]){this.J=t}validateWrite(t,e){for(const s of this.J){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new c(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new c(`Cannot edit field ${String(s)}`)}}},exports.TaskQueueMap=r,exports.TrackingTopicMap=i,exports.UniqueIdProvider=e,exports.WebsocketHandlerFactory=class{broadcaster;closers=new Set;M;N;closing=!1;constructor(t,e={}){this.broadcaster=t,this.M=e.pingInterval??25e3,this.N=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e){const s=async(s,r)=>{const i=await t(s,r),n=await e(s,r),o=await this.broadcaster.subscribe(i,n);return o||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const r=await s(t,e).catch((t=>(console.warn("WebSocket init error",t),e.sendError(500),null)));if(r)try{const t=await e.accept();let s=0,i=()=>null;const n=()=>(this.closers.delete(n),t.send("X"),s=1,new Promise((t=>{i=t})));t.on("close",(()=>{s=2,clearTimeout(c),r.close().catch((()=>null)),this.closers.delete(n),i()}));const o=()=>{r.close().catch((()=>null)),this.closers.delete(n),t.terminate(),i()},a=()=>{t.ping(),clearTimeout(c),c=setTimeout(o,this.N)};if(t.on("pong",(()=>{clearTimeout(c),c=setTimeout(a,this.M)})),t.on("message",(async(n,o)=>{clearTimeout(c),c=setTimeout(a,this.M);try{if(o)throw new Error("Binary messages are not supported");const a=String(n);if("P"===a)return void t.send("p");if("x"===a){if(1!==s)throw new Error("Unexpected close ack message");return s=2,void i()}if(2===s)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(a);e.beginTransaction();try{await r.send(c.change,c.id)}finally{e.endTransaction()}}catch(e){t.send(JSON.stringify({error:e instanceof Error?e.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void r.close().catch((()=>null));t.send(JSON.stringify({init:r.getInitialData()})),r.listen(((e,s)=>{const r=void 0!==s?{id:s,...e}:e;t.send(JSON.stringify(r))}));let c=setTimeout(a,this.M);this.closers.add(n)}catch(t){console.warn("WebSocket error",t),e.sendError(500),r.close().catch((()=>null))}}}};
package/backend/index.mjs CHANGED
@@ -1 +1 @@
1
- import{randomUUID as t}from"node:crypto";class e{t=t().substring(0,8);i=0;get(){const t=this.i;return this.i++,`${this.t}-${t}`}}class r extends EventTarget{o=[];h=!1;push(t){return new Promise(((e,r)=>{this.o.push({task:t,resolve:e,reject:r}),this.h||this.u()}))}async u(){for(this.h=!0;this.o.length>0;){const{task:t,resolve:e,reject:r}=this.o.shift();let s=null,i=!1;try{s=await t(),i=!0}catch(t){r(t)}i&&e(s)}this.h=!1,this.dispatchEvent(new CustomEvent("drain"))}}class s{l;v=new Map;constructor(t=()=>new r){this.l=t}push(t,e){let r=this.v.get(t);return r||(r=this.l(),r.addEventListener("drain",(()=>{this.v.delete(t)})),this.v.set(t,r)),r.push(e)}}class i{m;p=new Map;constructor(t){this.m=t}async add(t,e){let r=this.p.get(t);r||(r=this.m(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 n{_=new Set;add(t){this._.add(t)}remove(t){return this._.delete(t),this._.size>0}broadcast(t){this._.forEach((e=>e(t)))}}const a={validateWrite(){}};class o{O;j;_;C;S;constructor(t,e,r,s,i){this.O=t,this.j=e,this._=r,this.C=s,this.S=i}static for(t){let r,a,c,h;return{withReducer(t){return r=t,this},withSubscribers(t){return a=t,this},withTaskQueues(t){return c=t,this},withIdProvider(t){return h=t,this},build(){if(!r)throw new Error("must set broadcaster context");return new o(t,r,a||new i((()=>new n)),c||new s,h||new e)}}}async subscribe(t,e,r=a){let s=await this.O.read(t);if(null==s)return null;const i=this.S.get(),n=({message:t,source:r,meta:s})=>{r===i?e(t,s):t.change&&e(t,void 0)};return this._.add(t,n),{getInitialData:()=>{if(null===s)throw new Error("Already fetched initialData");const t=s;return s=null,t},send:(e,s)=>this.I(t,e,r,i,s),close:async()=>{await this._.remove(t,n)}}}update(t,e,r=a){return this.I(t,e,r,null,void 0)}async J(t,e,r,s,i){try{const s=await this.O.read(t);if(!s)throw new Error("Deleted");r.validateWriteSpec?.(e);const i=this.j.update(s,e),n=this.O.validate(i);r.validateWrite(n,s),await this.O.write(t,n,s)}catch(e){return void this._.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:s,meta:i})}this._.broadcast(t,{message:{change:e},source:s,meta:i})}async I(t,e,r,s,i){return this.C.push(t,(()=>this.J(t,e,r,s,i)))}}function c(t){if("object"!=typeof(e=t)||null===e||Array.isArray(e))throw new Error("Must specify change and optional id");var e;const{id:r,change:s}=t;if(void 0===r)return{change:s};if("number"!=typeof r)throw new Error("if specified, id must be a number");return{change:s,id:r}}const h="P",u="p",l=t=>(e,r)=>async(s,i)=>{const n=await i.accept(),a=(t,e)=>{const r=void 0!==e?{id:e,...t}:t;n.send(JSON.stringify(r))};let o;try{const n=await e(s,i),c=await r(s,i);o=await t.subscribe(n,a,c)}catch(t){return void n.send(JSON.stringify(d(t)))}o?(n.on("close",o.close),n.on("message",((t,e)=>{try{if(e)return;const r=String(t);if("P"===r)return void n.send("p");const s=function(t){return c(JSON.parse(t))}(r);i.beginTransaction(),o.send(s.change,s.id).finally((()=>i.endTransaction()))}catch(t){n.send(JSON.stringify(d(t)))}})),n.send(JSON.stringify({init:o.getInitialData()}))):i.sendError(404)};function d(t){return{error:t instanceof Error?t.message:"Internal error"}}const w=t=>t;class f{N;P;validate;$;k;constructor(t,e,r,s=w,i=w){this.N=t,this.P=e,this.validate=r,this.$=s,this.k=i}async read(t){try{return await this.N.get(this.P,t)}catch(t){throw this.$(t)}}async write(t,e,r){const s={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(r,i)?r[i]:void 0)&&(s[i]?Object.defineProperty(s,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):s[i]=e)}));try{await this.N.update(this.P,t,s)}catch(t){throw this.k(t)}}}class y extends Error{}class v{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)}}const b="Cannot modify data",m={validateWriteSpec(){throw new y(b)},validateWrite(){throw new y(b)}};class p{M;constructor(t=[]){this.M=t}validateWrite(t,e){Object.keys(e).forEach((e=>{const r=e;if(!Object.prototype.hasOwnProperty.call(t,r)&&this.M.includes(r))throw new y(`Cannot remove field ${r}`)})),Object.keys(t).forEach((r=>{const s=r;if(this.M.includes(s)){if(!Object.prototype.hasOwnProperty.call(e,s))throw new y(`Cannot add field ${s}`);if(t[s]!==e[s])throw new y(`Cannot edit field ${s}`)}}))}}export{r as AsyncTaskQueue,o as Broadcaster,f as CollectionStorageModel,v as InMemoryModel,n as InMemoryTopic,h as PING,u as PONG,y as PermissionError,m as ReadOnly,a as ReadWrite,p as ReadWriteStruct,s as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,l as websocketHandler};
1
+ import{randomUUID as t}from"node:crypto";const e=()=>{const e=t().substring(0,8);let s=0;return()=>{const t=s++;return`${e}-${t}`}};class s extends EventTarget{t=[];i=!1;push(t){return new Promise(((e,s)=>{this.t.push((async()=>{try{e(await t())}catch(t){s(t)}})),this.i||this.o()}))}async o(){for(this.i=!0;this.t.length>0;)await this.t.shift()();this.i=!1,this.dispatchEvent(new CustomEvent("drain"))}active(){return this.i}}class r{h;l=new Map;constructor(t=()=>new s){this.h=t}push(t,e){let s=this.l.get(t);if(!s){const e=this.h(),r=()=>{e.active()||(this.l.delete(t),e.removeEventListener("drain",r))};e.addEventListener("drain",r),this.l.set(t,e),s=e}return s.push(e)}}class i{u;m=new Map;constructor(t){this.u=t}async add(t,e){let s=this.m.get(t);s||(s=this.u(t),this.m.set(t,s)),await s.add(e)}async remove(t,e){const s=this.m.get(t);if(s){await s.remove(e)||this.m.delete(t)}}async broadcast(t,e){const s=this.m.get(t);s&&await s.broadcast(e)}}class n{p=new Set;add(t){this.p.add(t)}remove(t){return this.p.delete(t),this.p.size>0}broadcast(t){this.p.forEach((e=>e(t)))}}const a={validateWrite(){}};class o{_;v;p;S;C;constructor(t,s,a={}){this._=t,this.v=s,this.p=a.subscribers??new i((()=>new n)),this.S=a.taskQueues??new r,this.C=a.idProvider??e()}async subscribe(t,e=a){let s={T:0},r="";const i=t=>{2===s.T?t.source===r?s.O(t.message,t.meta):t.message.change&&s.O(t.message,void 0):1===s.T&&s.t.push(t)};try{if(await this.S.push(t,(async()=>{const e=await this._.read(t);null!=e&&(s={T:1,P:e,t:[]},await this.p.add(t,i))})),0===s.T)return null;r=await this.C()}catch(e){throw await this.p.remove(t,i),e}return{getInitialData(){if(1!==s.T)throw new Error("Already started");return s.P},listen(t){if(1!==s.T)throw new Error("Already started");const e=s.t;s={T:2,O:t},e.forEach(i)},send:(s,i)=>this.j(t,s,e,r,i),close:async()=>{await this.p.remove(t,i)}}}update(t,e,s=a){return this.j(t,e,s,null,void 0)}async I(t,e,s,r,i){try{const r=await this._.read(t);if(!r)throw new Error("Deleted");s.validateWriteSpec?.(e);const i=this.v.update(r,e),n=this._.validate(i);s.validateWrite(n,r),await this._.write(t,n,r)}catch(e){return void this.p.broadcast(t,{message:{error:e instanceof Error?e.message:"Internal error"},source:r,meta:i})}this.p.broadcast(t,{message:{change:e},source:r,meta:i})}async j(t,e,s,r,i){return this.S.push(t,(()=>this.I(t,e,s,r,i)))}}const c="P",h="p",l="X",u="x";class w{broadcaster;closers=new Set;W;k;closing=!1;constructor(t,e={}){this.broadcaster=t,this.W=e.pingInterval??25e3,this.k=e.pongTimeout??3e4}activeConnections(){return this.closers.size}async softClose(t){this.closing=!0;let e=null;await Promise.race([Promise.all([...this.closers].map((t=>t()))),new Promise((s=>{e=setTimeout(s,t)}))]),null!==e&&clearTimeout(e)}handler(t,e){const s=async(s,r)=>{const i=await t(s,r),n=await e(s,r),a=await this.broadcaster.subscribe(i,n);return a||(r.sendError(404),null)};return async(t,e)=>{if(this.closing)return void e.sendError(503,1012);const r=await s(t,e).catch((t=>(console.warn("WebSocket init error",t),e.sendError(500),null)));if(r)try{const t=await e.accept();let s=0,i=()=>null;const n=()=>(this.closers.delete(n),t.send("X"),s=1,new Promise((t=>{i=t})));t.on("close",(()=>{s=2,clearTimeout(c),r.close().catch((()=>null)),this.closers.delete(n),i()}));const a=()=>{r.close().catch((()=>null)),this.closers.delete(n),t.terminate(),i()},o=()=>{t.ping(),clearTimeout(c),c=setTimeout(a,this.k)};if(t.on("pong",(()=>{clearTimeout(c),c=setTimeout(o,this.W)})),t.on("message",(async(n,a)=>{clearTimeout(c),c=setTimeout(o,this.W);try{if(a)throw new Error("Binary messages are not supported");const o=String(n);if("P"===o)return void t.send("p");if("x"===o){if(1!==s)throw new Error("Unexpected close ack message");return s=2,void i()}if(2===s)throw new Error("Unexpected message after close ack");const c=function(t){const e=JSON.parse(t);if("object"!=typeof e||!e||Array.isArray(e))throw new Error("Must specify change and optional id");const{id:s,change:r}=e;if(void 0===s)return{change:r};if("number"!=typeof s)throw new Error("if specified, id must be a number");return{change:r,id:s}}(o);e.beginTransaction();try{await r.send(c.change,c.id)}finally{e.endTransaction()}}catch(e){t.send(JSON.stringify({error:e instanceof Error?e.message:"Internal error"}))}})),this.closing)return e.sendError(503,1012),void r.close().catch((()=>null));t.send(JSON.stringify({init:r.getInitialData()})),r.listen(((e,s)=>{const r=void 0!==s?{id:s,...e}:e;t.send(JSON.stringify(r))}));let c=setTimeout(o,this.W);this.closers.add(n)}catch(t){console.warn("WebSocket error",t),e.sendError(500),r.close().catch((()=>null))}}}}const d=t=>t;class f{$;q;validate;A;J;constructor(t,e,s,r={}){this.$=t,this.q=e,this.validate=s,this.A=r.readErrorIntercept??d,this.J=r.writeErrorIntercept??d}async read(t){try{return await this.$.get(this.q,t)}catch(t){throw this.A(t)}}async write(t,e,s){const r={};Object.entries(e).forEach((([t,e])=>{const i=t;e!==(Object.prototype.hasOwnProperty.call(s,i)?s[i]:void 0)&&(r[i]?Object.defineProperty(r,i,{value:e,configurable:!0,enumerable:!0,writable:!0}):r[i]=e)}));try{await this.$.update(this.q,t,r)}catch(t){throw this.J(t)}}}class m extends Error{}class y{read=this.get;validate;M=new Map;constructor(t=t=>t){this.validate=t}set(t,e){this.M.set(t,e)}get(t){return this.M.get(t)}delete(t){this.M.delete(t)}write(t,e,s){if(s!==this.M.get(t))throw new Error("Unexpected previous value");this.M.set(t,e)}}const p="Cannot modify data",g={validateWriteSpec(){throw new m(p)},validateWrite(){throw new m(p)}};class _{N;constructor(t=[]){this.N=t}validateWrite(t,e){for(const s of this.N){const r=Object.prototype.hasOwnProperty.call(e,s),i=Object.prototype.hasOwnProperty.call(t,s);if(r!==i)throw new m(r?`Cannot remove field ${String(s)}`:`Cannot add field ${String(s)}`);if(i&&e[s]!==t[s])throw new m(`Cannot edit field ${String(s)}`)}}}export{s as AsyncTaskQueue,o as Broadcaster,l as CLOSE,u as CLOSE_ACK,f as CollectionStorageModel,y as InMemoryModel,n as InMemoryTopic,c as PING,h as PONG,m as PermissionError,g as ReadOnly,a as ReadWrite,_ as ReadWriteStruct,r as TaskQueueMap,i as TrackingTopicMap,e as UniqueIdProvider,w as WebsocketHandlerFactory};
@@ -2,55 +2,107 @@ interface Context<T, SpecT> {
2
2
  update: (input: T, spec: SpecT) => T;
3
3
  combine: (specs: SpecT[]) => SpecT;
4
4
  }
5
- declare class SyncCallback<T> {
6
- readonly sync: (state: T) => void;
7
- readonly reject: (message: string) => void;
8
- constructor(sync: (state: T) => void, reject: (message: string) => void);
9
- }
10
- type SpecGenerator<T, SpecT> = (state: T) => SpecSource<T, SpecT>[] | null | undefined;
11
- type SpecSource<T, SpecT> = SpecT | SpecGenerator<T, SpecT> | SyncCallback<T> | null | undefined;
5
+ type SpecGenerator<T, SpecT> = (state: T) => SpecSource<T, SpecT>[];
6
+ type SpecSource<T, SpecT> = SpecT | SpecGenerator<T, SpecT> | null;
12
7
  type DispatchSpec<T, SpecT> = SpecSource<T, SpecT>[];
13
- type Dispatch<T, SpecT> = (specs: DispatchSpec<T, SpecT> | null | undefined) => void;
8
+ interface Dispatch<T, SpecT> {
9
+ sync(specs?: DispatchSpec<T, SpecT>): Promise<T>;
10
+ (specs: DispatchSpec<T, SpecT>, syncedCallback?: (state: T) => void, errorCallback?: (error: string) => void): void;
11
+ }
12
+
13
+ interface Scheduler {
14
+ trigger(fn: (signal: AbortSignal) => Promise<void>): void;
15
+ schedule(fn: (signal: AbortSignal) => Promise<void>): void;
16
+ stop(): void;
17
+ }
18
+
19
+ type MaybePromise<T> = Promise<T> | T;
20
+
21
+ type Handler = (signal: AbortSignal) => MaybePromise<void>;
22
+ type DelayGetter = (attempt: number) => number;
23
+ declare class OnlineScheduler implements Scheduler {
24
+ private readonly _delayGetter;
25
+ private readonly _connectTimeLimit;
26
+ private _timeout;
27
+ private _stop;
28
+ private _handler;
29
+ private _attempts;
30
+ constructor(_delayGetter: DelayGetter, _connectTimeLimit: number);
31
+ trigger(handler: Handler): void;
32
+ schedule(handler: Handler): void;
33
+ stop(): void;
34
+ private _attempt;
35
+ private _remove;
36
+ }
37
+ interface ExponentialDelayConfig {
38
+ base?: number;
39
+ initialDelay: number;
40
+ maxDelay: number;
41
+ randomness?: number;
42
+ }
43
+ declare const exponentialDelay: ({ base, initialDelay, maxDelay, randomness }: ExponentialDelayConfig) => DelayGetter;
14
44
 
15
- declare function actionsHandledCallback<T>(callback?: (state: T) => void): ((state: T) => null) | null;
45
+ type EventHandler<E extends Event> = ((e: E) => void) | {
46
+ handleEvent(e: E): void;
47
+ };
48
+ declare class TypedEventTarget<Events extends Record<string, Event>> extends EventTarget {
49
+ addEventListener<K extends keyof Events & string>(type: K, callback: EventHandler<Events[K]> | null, options?: AddEventListenerOptions | boolean): void;
50
+ removeEventListener<K extends keyof Events & string>(type: K, callback: EventHandler<Events[K]> | null, options?: EventListenerOptions | boolean): void;
51
+ dispatchEvent<K extends keyof Events & string>(event: TypedEvent<K, Events[K]>): boolean;
52
+ }
53
+ type TypedEvent<Type extends string, T extends Event> = T & {
54
+ readonly type: Type;
55
+ };
56
+
57
+ interface ConnectionInfo {
58
+ url: string;
59
+ token?: string | undefined;
60
+ }
61
+ type ConnectionGetter = (signal: AbortSignal) => MaybePromise<ConnectionInfo>;
62
+ interface DisconnectDetail {
63
+ code: number;
64
+ reason: string;
65
+ }
16
66
 
17
- declare function actionsSyncedCallback<T>(resolve?: (state: T) => void, reject?: (message: string) => void): SyncCallback<T> | null;
67
+ type DeliveryStrategy<T, SpecT> = (serverState: T, spec: SpecT, hasSent: boolean) => boolean;
68
+ declare const AT_LEAST_ONCE: DeliveryStrategy<unknown, unknown>;
69
+ declare const AT_MOST_ONCE: DeliveryStrategy<unknown, unknown>;
18
70
 
19
- interface SharedReducerBuilder<T, SpecT> {
20
- withReducer<SpecT2 extends SpecT>(context: Context<T, SpecT2>): SharedReducerBuilder<T, SpecT2>;
21
- withToken(token: string): this;
22
- withErrorHandler(handler: (error: string) => void): this;
23
- withWarningHandler(handler: (error: string) => void): this;
24
- build(): SharedReducer<T, SpecT>;
71
+ interface SharedReducerOptions<T, SpecT> {
72
+ scheduler?: Scheduler | undefined;
73
+ deliveryStrategy?: DeliveryStrategy<T, SpecT> | undefined;
25
74
  }
26
- declare class SharedReducer<T, SpecT> {
75
+ type SharedReducerEvents = {
76
+ connected: CustomEvent<void>;
77
+ disconnected: CustomEvent<DisconnectDetail>;
78
+ warning: CustomEvent<Error>;
79
+ };
80
+ declare class SharedReducer<T, SpecT> extends TypedEventTarget<SharedReducerEvents> {
27
81
  private readonly _context;
28
- private readonly _changeHandler;
29
- private readonly _warningHandler;
30
- private readonly _connection;
31
- private _latestStates;
32
- private _currentChange;
33
- private _currentSyncCallbacks;
34
- private _localChanges;
35
- private _pendingChanges;
82
+ private readonly _ws;
83
+ private _paused;
84
+ private _state;
85
+ private readonly _tracker;
86
+ private readonly _listeners;
36
87
  private readonly _dispatchLock;
37
- private readonly _nextId;
38
- private constructor();
39
- static for<T2>(wsUrl: string, changeHandler?: (state: T2) => void): SharedReducerBuilder<T2, unknown>;
40
- close(): void;
41
- dispatch: Dispatch<T, SpecT>;
42
- addSyncCallback(resolve: (state: T) => void, reject?: (message: string) => void): void;
43
- syncedState(): Promise<T>;
44
- getState(): T | undefined;
45
- private _sendCurrentChange;
46
- private _addCurrentChange;
47
- private _applySpecs;
48
- private _popLocalChange;
49
- private _handleErrorMessage;
88
+ constructor(_context: Context<T, SpecT>, connectionGetter: ConnectionGetter, { scheduler, deliveryStrategy, }?: SharedReducerOptions<T, SpecT>);
89
+ readonly dispatch: Dispatch<T, SpecT>;
90
+ private _apply;
91
+ private readonly _share;
50
92
  private _handleInitMessage;
51
93
  private _handleChangeMessage;
52
- private _handleMessage;
53
- private _computeLocal;
94
+ private _handleErrorMessage;
95
+ private _handleGracefulClose;
96
+ private readonly _handleMessage;
97
+ addStateListener(listener: (state: Readonly<T>) => void): void;
98
+ removeStateListener(listener: (state: Readonly<T>) => void): void;
99
+ private _setLocalState;
100
+ getState(): Readonly<T> | undefined;
101
+ private _warn;
102
+ private readonly _handleConnected;
103
+ private readonly _handleConnectionFailure;
104
+ private readonly _handleDisconnected;
105
+ close(): void;
54
106
  }
55
107
 
56
- export { type Context, type Dispatch, type DispatchSpec, SharedReducer, actionsHandledCallback, actionsSyncedCallback };
108
+ export { AT_LEAST_ONCE, AT_MOST_ONCE, type Context, type DeliveryStrategy, type Dispatch, type DispatchSpec, OnlineScheduler, type Scheduler, SharedReducer, type SharedReducerOptions, exponentialDelay };
package/frontend/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";class t{sync;reject;constructor(t,s){this.sync=t,this.reject=s}}const s=()=>null;function i(i,e){return i||e?new t(i||s,e||s):null}class e{t;h;l;o=null;constructor(t,s=void 0,i,e=void 0){this.t=i,this.h=e,this.l=new WebSocket(t),this.l.addEventListener("message",this.u),this.l.addEventListener("error",this._),this.l.addEventListener("close",this.p),s&&this.l.addEventListener("open",(()=>this.l.send(s)),{once:!0}),this.C()}send(t){this.l.send(JSON.stringify(t))}close(){this.l.close(),null!==this.o&&clearTimeout(this.o)}C(){null!==this.o&&clearTimeout(this.o),this.o=setTimeout(this.v,2e4)}v=()=>{this.l.send("P")};u=({data:t})=>{this.C(),"p"!==t&&this.t(JSON.parse(t))};_=()=>{this.h?.("Failed to connect")};p=()=>{null!==this.o&&clearTimeout(this.o)}}class n{m;S;k;I;O=null;P;T=[];j=[];M=[];N=function(t){let s=!1;return i=>{if(s)throw new Error(t);try{return s=!0,i()}finally{s=!1}}}("Cannot dispatch recursively");H=function(){let t=1;return()=>t++}();constructor(t,s,i,n,h,r){this.m=t,this.S=n,this.k=r,this.I=new e(s,i,this.u,h)}static for(t,s){let i,e,h,r;return{withReducer(t){return i=t,this},withToken(t){return e=t,this},withErrorHandler(t){return h=t,this},withWarningHandler(t){return r=t,this},build(){if(!i)throw new Error("must set broadcaster context");return new n(i,t,e,s,h,r)}}}close(){this.I.close(),this.O=null,this.P=void 0,this.T=[],this.j=[],this.M=[]}dispatch=t=>{if(t&&t.length)if(this.O){const s=this.J(this.O,t);s!==this.O&&(this.O=s,this.S?.(s.local))}else this.M.push(...t)};addSyncCallback(t,s){this.dispatch([i(t,s)])}syncedState(){return new Promise(((t,s)=>{this.addSyncCallback(t,s)}))}getState(){return this.O?.local}$=()=>{if(void 0===this.P)return;const t=this.H(),s=this.P,i=this.T;this.P=void 0,this.T=[],this.j.push({change:s,id:t,syncCallbacks:i}),this.I.send({change:s,id:t})};A(t){void 0===this.P?(this.P=t,setTimeout(this.$,0)):this.P=this.m.combine([this.P,t])}J(s,i){if(!i.length)return s;const{state:e,delta:n}=this.N((()=>function(s,i,e,n){let h=i;const r=[],l=[];function o(){if(l.length>0){const t=s.combine(l);r.push(t),h=s.update(h,t),l.length=0}}return function(t,s){let i={vs:t,i:0,prev:null};for(;i;)if(i.i>=i.vs.length)i=i.prev;else{const t=s(i.vs[i.i]);i.i+=1,t&&t.length&&(i={vs:t,i:0,prev:i})}}(e,(s=>s instanceof t?(o(),n(s,h),null):"function"==typeof s?(o(),s(h)):(s&&l.push(s),null))),o(),{state:h,delta:s.combine(r)}}(this.m,s.local,i,((t,i)=>{i===s.local&&void 0===this.P?t.sync(s.local):this.T.push(t)}))));return e===s.local?s:(this.A(n),{server:s.server,local:e})}L(t){const s=void 0===t?-1:this.j.findIndex((s=>s.id===t));return-1===s?{localChange:null,index:s}:{localChange:this.j.splice(s,1)[0],index:s}}W(t){const{localChange:s}=this.L(t.id);s?(this.k?.(`API rejected update: ${t.error}`),this.O&&(this.O=this.q(this.O.server),this.S?.(this.O.local)),s.syncCallbacks.forEach((s=>s.reject(t.error)))):this.k?.(`API sent error: ${t.error}`)}F(t){this.O=this.J(this.q(t.init),this.M),this.M.length=0,this.S?.(this.O.local)}R(t){if(!this.O)return void this.k?.(`Ignoring change before init: ${JSON.stringify(t)}`);const{localChange:s,index:i}=this.L(t.id),e=this.m.update(this.O.server,t.change);0===i?this.O={server:e,local:this.O.local}:(this.O=this.q(e),this.S?.(this.O.local));const n=this.O.local;s?.syncCallbacks.forEach((t=>t.sync(n)))}u=t=>{Object.prototype.hasOwnProperty.call(t,"change")?this.R(t):Object.prototype.hasOwnProperty.call(t,"init")?this.F(t):Object.prototype.hasOwnProperty.call(t,"error")?this.W(t):this.k?.(`Ignoring unknown API message: ${JSON.stringify(t)}`)};q(t){let s=t;if(this.j.length>0){const t=this.m.combine(this.j.map((({change:t})=>t)));s=this.m.update(s,t)}return void 0!==this.P&&(s=this.m.update(s,this.P)),{server:t,local:s}}}exports.SharedReducer=n,exports.actionsHandledCallback=function(t){return t?s=>(t(s),null):null},exports.actionsSyncedCallback=i;
1
+ "use strict";class t{t;i;h=null;o=null;l=null;u=0;constructor(t,s){this.t=t,this.i=s,this._=this._.bind(this)}trigger(t){this.stop(),this.l=t,this._()}schedule(t){this.l!==t&&(this.o&&this.stop(),this.l=t,null===this.h&&("hidden"===global.document?.visibilityState?global.addEventListener?.("visibilitychange",this._):(global.addEventListener?.("online",this._),this.h=setTimeout(this._,this.t(this.u))),global.addEventListener?.("pageshow",this._),global.addEventListener?.("focus",this._),++this.u))}stop(){this.l=null,this.o?.(),this.o=null,this.m()}async _(){if(this.o||!this.l)return;this.m();const t=new AbortController;let s=()=>null;this.o=()=>{s(),t.abort()};try{await Promise.race([this.l(t.signal),new Promise(((t,e)=>{const i=setTimeout(e,this.i);s=()=>clearTimeout(i)}))]),this.l=null,this.u=0}catch(s){if(!t.signal.aborted){t.abort();const s=this.l;s&&(this.l=null,this.schedule(s))}}finally{s(),this.o=null}}m(){null!==this.h&&(clearTimeout(this.h),this.h=null,global.removeEventListener?.("online",this._),global.removeEventListener?.("pageshow",this._),global.removeEventListener?.("visibilitychange",this._),global.removeEventListener?.("focus",this._))}}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,v:0,C:null};for(;e;)if(e.v>=e.p.length)e=e.C;else{const t=s(e.p[e.v]);++e.v,t&&t.length&&(e={p:t,v:0,C:e})}}(e,(t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null})),h(),{S:s,T: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{$;k;I=null;P=!1;constructor(t,s){super(),this.$=t,this.k=s,this.L=this.L.bind(this),this.k.trigger(this.L)}async L(t){const{url:s,token:e}=await this.$(t);t.throwIfAborted();const i=new AbortController,h=i.signal;await new Promise(((u,d)=>{const _=new WebSocket(s);let g=!0;const f=t=>{i.abort(),_.close(),g?(g=!1,this.dispatchEvent(n("connectionfailure",t)),d(new Error(`handshake failed: ${t.code} ${t.reason}`))):(this.I=null,this.dispatchEvent(n("disconnected",t)),this.P||this.k.schedule(this.L))};e&&_.addEventListener("open",(()=>_.send(e)),{once:!0,signal:h}),_.addEventListener("message",(t=>{t.data!==c&&(g&&(g=!1,this.I=_,this.dispatchEvent(n("connected")),u()),this.dispatchEvent(n("message",t.data)))}),{signal:h}),_.addEventListener("close",f,{signal:h}),_.addEventListener("error",(()=>f(o)),{signal:h}),t.addEventListener("abort",(()=>f(r)),{signal:h}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(l)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,a)},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}),global.addEventListener?.("offline",i,{signal:s.signal})}(_)})).catch((t=>{throw i.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.k.stop(),this.I?.close()}}const o={code:0,reason:"client side error"},r={code:0,reason:"handshake timeout"},l="P",c="p",a=2e4,u=()=>!0;class d{M;A;O=[];j=function(){let t=1;return()=>t++}();constructor(t,s){this.M=t,this.A=s}D(t){this.O.push({J:void 0,N:t,F:[],G:[]})}q(t,s,e){if(s||e)if(this.O.length){const t=this.O[this.O.length-1];t.F.push(s??_),t.G.push(e??_)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.O.length;++e){const i=this.O[e];this.A(t,i.N,void 0!==i.J)?(i.J=void 0,this.O[e-s]=i):(i.G.forEach((t=>t("message possibly lost"))),++s)}this.O.length-=s}W(t){for(let t=0,s=0;t<=this.O.length;++t){const e=this.O[t];if(!e||void 0!==e.J||e.F.length>0||e.G.length>0){const e=t-s;if(e>1){const i=this.O[t-1],n=this.O.splice(s,e,i);i.N=this.M.combine(n.map((t=>t.N))),t-=e-1}s=t+1}}for(const s of this.O)void 0===s.J&&(s.J=this.j(),t(JSON.stringify({change:s.N,id:s.J})))}X(t){const s=void 0===t?-1:this.O.findIndex((s=>s.J===t));return-1===s?{B:null,H:!1}:{B:this.O.splice(s,1)[0],H:0===s}}K(t){if(!this.O.length)return t;const s=this.M.combine(this.O.map((({N:t})=>t)));return this.M.update(t,s)}}const _=()=>null;const g={code:0,reason:"graceful shutdown"},f=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});exports.AT_LEAST_ONCE=u,exports.AT_MOST_ONCE=(t,s,e)=>!e,exports.OnlineScheduler=t,exports.SharedReducer=class extends i{M;I;R=!0;S={V:0,Y:[]};Z;tt=new Set;st=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=u}={}){super(),this.M=s,this.Z=new d(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.et),this.I.addEventListener("connected",this.it),this.I.addEventListener("connectionfailure",this.nt),this.I.addEventListener("disconnected",this.ht)}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={ot:t,F:s,G:e};switch(this.S.V){case-1:throw new Error("closed");case 0:this.S.Y.push(i);break;case 1:this.rt(this.lt(this.S.ct,[i])),this.dt.ut()}}));lt(t,s){return this.st((()=>{for(const{ot:i,F:n,G:h}of s){if(i.length){const{S:s,T:n}=e(this.M,t,i);t=s,this.Z.D(n)}this.Z.q(t,n,h)}return t}))}dt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{_t:i,ut:()=>{null===s&&(s=setTimeout(i,0))},o:e}}((()=>{this.I.isConnected()&&!this.R&&this.Z.W(this.I.send)}));gt(t){if(-1!==this.S.V){if(this.R=!1,0===this.S.V){const s=this.lt(t.init,this.S.Y);this.S={V:1,ft:t.init,ct:s},this.rt(s,!0)}else this.S.ft=t.init,this.Z.U(t.init),this.rt(this.Z.K(t.init));this.dt._t()}else this.wt(`Ignoring init after closing: ${JSON.stringify(t)}`)}vt(t){if(1!==this.S.V)return void this.wt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.ft=this.M.update(this.S.ft,t.change),{B:e,H:i}=this.Z.X(t.id);i||this.rt(this.Z.K(s)),e?.F.forEach((t=>t(s)))}bt(t){if(1!==this.S.V)return void this.wt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.Z.X(t.id);s?(this.wt(`API rejected update: ${t.error}`),s?.G.forEach((s=>s(t.error))),this.rt(this.Z.K(this.S.ft))):this.wt(`API sent error: ${t.error}`)}yt(){this.I.send("x"),this.R?this.wt("Unexpected extra close message"):(this.R=!0,this.dispatchEvent(n("disconnected",g)))}et=t=>{if("X"===t.detail)return void this.yt();const s=JSON.parse(t.detail);"change"in s?this.vt(s):"init"in s?this.gt(s):"error"in s?this.bt(s):this.wt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.tt.add(t),1===this.S.V&&t(this.S.ct)}removeStateListener(t){this.tt.delete(t)}rt(t,s=!1){if(1!==this.S.V)throw new Error("invalid state");if(s||this.S.ct!==t){this.S.ct=t;for(const s of this.tt)s(t)}}getState(){return 1===this.S.V?this.S.ct:void 0}wt(t){this.dispatchEvent(n("warning",new Error(t)))}it=()=>{this.dispatchEvent(n("connected"))};nt=t=>{this.dispatchEvent(n("warning",new Error(`connection failure: ${t.detail.code} ${t.detail.reason}`)))};ht=t=>{this.R||(this.R=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.R=!0,this.S={V:-1},this.I.close(),this.dt.o(),this.tt.clear(),this.I.removeEventListener("message",this.et),this.I.removeEventListener("connected",this.it),this.I.removeEventListener("connectionfailure",this.nt),this.I.removeEventListener("disconnected",this.ht)}},exports.exponentialDelay=s;
@@ -1 +1 @@
1
- function t(t){return t?s=>(t(s),null):null}class s{sync;reject;constructor(t,s){this.sync=t,this.reject=s}}const i=()=>null;function e(t,e){return t||e?new s(t||i,e||i):null}class n{t;h;l;o=null;constructor(t,s=void 0,i,e=void 0){this.t=i,this.h=e,this.l=new WebSocket(t),this.l.addEventListener("message",this.u),this.l.addEventListener("error",this._),this.l.addEventListener("close",this.C),s&&this.l.addEventListener("open",(()=>this.l.send(s)),{once:!0}),this.p()}send(t){this.l.send(JSON.stringify(t))}close(){this.l.close(),null!==this.o&&clearTimeout(this.o)}p(){null!==this.o&&clearTimeout(this.o),this.o=setTimeout(this.v,2e4)}v=()=>{this.l.send("P")};u=({data:t})=>{this.p(),"p"!==t&&this.t(JSON.parse(t))};_=()=>{this.h?.("Failed to connect")};C=()=>{null!==this.o&&clearTimeout(this.o)}}class h{m;S;k;I;O=null;P;T=[];j=[];M=[];N=function(t){let s=!1;return i=>{if(s)throw new Error(t);try{return s=!0,i()}finally{s=!1}}}("Cannot dispatch recursively");H=function(){let t=1;return()=>t++}();constructor(t,s,i,e,h,r){this.m=t,this.S=e,this.k=r,this.I=new n(s,i,this.u,h)}static for(t,s){let i,e,n,r;return{withReducer(t){return i=t,this},withToken(t){return e=t,this},withErrorHandler(t){return n=t,this},withWarningHandler(t){return r=t,this},build(){if(!i)throw new Error("must set broadcaster context");return new h(i,t,e,s,n,r)}}}close(){this.I.close(),this.O=null,this.P=void 0,this.T=[],this.j=[],this.M=[]}dispatch=t=>{if(t&&t.length)if(this.O){const s=this.J(this.O,t);s!==this.O&&(this.O=s,this.S?.(s.local))}else this.M.push(...t)};addSyncCallback(t,s){this.dispatch([e(t,s)])}syncedState(){return new Promise(((t,s)=>{this.addSyncCallback(t,s)}))}getState(){return this.O?.local}$=()=>{if(void 0===this.P)return;const t=this.H(),s=this.P,i=this.T;this.P=void 0,this.T=[],this.j.push({change:s,id:t,syncCallbacks:i}),this.I.send({change:s,id:t})};A(t){void 0===this.P?(this.P=t,setTimeout(this.$,0)):this.P=this.m.combine([this.P,t])}J(t,i){if(!i.length)return t;const{state:e,delta:n}=this.N((()=>function(t,i,e,n){let h=i;const r=[],l=[];function o(){if(l.length>0){const s=t.combine(l);r.push(s),h=t.update(h,s),l.length=0}}return function(t,s){let i={vs:t,i:0,prev:null};for(;i;)if(i.i>=i.vs.length)i=i.prev;else{const t=s(i.vs[i.i]);i.i+=1,t&&t.length&&(i={vs:t,i:0,prev:i})}}(e,(t=>t instanceof s?(o(),n(t,h),null):"function"==typeof t?(o(),t(h)):(t&&l.push(t),null))),o(),{state:h,delta:t.combine(r)}}(this.m,t.local,i,((s,i)=>{i===t.local&&void 0===this.P?s.sync(t.local):this.T.push(s)}))));return e===t.local?t:(this.A(n),{server:t.server,local:e})}L(t){const s=void 0===t?-1:this.j.findIndex((s=>s.id===t));return-1===s?{localChange:null,index:s}:{localChange:this.j.splice(s,1)[0],index:s}}W(t){const{localChange:s}=this.L(t.id);s?(this.k?.(`API rejected update: ${t.error}`),this.O&&(this.O=this.q(this.O.server),this.S?.(this.O.local)),s.syncCallbacks.forEach((s=>s.reject(t.error)))):this.k?.(`API sent error: ${t.error}`)}F(t){this.O=this.J(this.q(t.init),this.M),this.M.length=0,this.S?.(this.O.local)}R(t){if(!this.O)return void this.k?.(`Ignoring change before init: ${JSON.stringify(t)}`);const{localChange:s,index:i}=this.L(t.id),e=this.m.update(this.O.server,t.change);0===i?this.O={server:e,local:this.O.local}:(this.O=this.q(e),this.S?.(this.O.local));const n=this.O.local;s?.syncCallbacks.forEach((t=>t.sync(n)))}u=t=>{Object.prototype.hasOwnProperty.call(t,"change")?this.R(t):Object.prototype.hasOwnProperty.call(t,"init")?this.F(t):Object.prototype.hasOwnProperty.call(t,"error")?this.W(t):this.k?.(`Ignoring unknown API message: ${JSON.stringify(t)}`)};q(t){let s=t;if(this.j.length>0){const t=this.m.combine(this.j.map((({change:t})=>t)));s=this.m.update(s,t)}return void 0!==this.P&&(s=this.m.update(s,this.P)),{server:t,local:s}}}export{h as SharedReducer,t as actionsHandledCallback,e as actionsSyncedCallback};
1
+ class t{t;i;h=null;o=null;l=null;u=0;constructor(t,s){this.t=t,this.i=s,this._=this._.bind(this)}trigger(t){this.stop(),this.l=t,this._()}schedule(t){this.l!==t&&(this.o&&this.stop(),this.l=t,null===this.h&&("hidden"===global.document?.visibilityState?global.addEventListener?.("visibilitychange",this._):(global.addEventListener?.("online",this._),this.h=setTimeout(this._,this.t(this.u))),global.addEventListener?.("pageshow",this._),global.addEventListener?.("focus",this._),++this.u))}stop(){this.l=null,this.o?.(),this.o=null,this.m()}async _(){if(this.o||!this.l)return;this.m();const t=new AbortController;let s=()=>null;this.o=()=>{s(),t.abort()};try{await Promise.race([this.l(t.signal),new Promise(((t,e)=>{const i=setTimeout(e,this.i);s=()=>clearTimeout(i)}))]),this.l=null,this.u=0}catch(s){if(!t.signal.aborted){t.abort();const s=this.l;s&&(this.l=null,this.schedule(s))}}finally{s(),this.o=null}}m(){null!==this.h&&(clearTimeout(this.h),this.h=null,global.removeEventListener?.("online",this._),global.removeEventListener?.("pageshow",this._),global.removeEventListener?.("visibilitychange",this._),global.removeEventListener?.("focus",this._))}}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,v:0,C:null};for(;e;)if(e.v>=e.p.length)e=e.C;else{const t=s(e.p[e.v]);++e.v,t&&t.length&&(e={p:t,v:0,C:e})}}(e,(t=>{if("function"==typeof t){h();return t(s)}return t&&n.push(t),null})),h(),{S:s,T: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{$;k;I=null;P=!1;constructor(t,s){super(),this.$=t,this.k=s,this.L=this.L.bind(this),this.k.trigger(this.L)}async L(t){const{url:s,token:e}=await this.$(t);t.throwIfAborted();const i=new AbortController,h=i.signal;await new Promise(((u,d)=>{const _=new WebSocket(s);let g=!0;const f=t=>{i.abort(),_.close(),g?(g=!1,this.dispatchEvent(n("connectionfailure",t)),d(new Error(`handshake failed: ${t.code} ${t.reason}`))):(this.I=null,this.dispatchEvent(n("disconnected",t)),this.P||this.k.schedule(this.L))};e&&_.addEventListener("open",(()=>_.send(e)),{once:!0,signal:h}),_.addEventListener("message",(t=>{t.data!==c&&(g&&(g=!1,this.I=_,this.dispatchEvent(n("connected")),u()),this.dispatchEvent(n("message",t.data)))}),{signal:h}),_.addEventListener("close",f,{signal:h}),_.addEventListener("error",(()=>f(o)),{signal:h}),t.addEventListener("abort",(()=>f(r)),{signal:h}),function(t){const s=new AbortController;let e=null;const i=()=>{null!==e&&(clearTimeout(e),e=null),t.send(l)},n=()=>{null!==e&&clearTimeout(e),e=setTimeout(i,a)},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}),global.addEventListener?.("offline",i,{signal:s.signal})}(_)})).catch((t=>{throw i.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.k.stop(),this.I?.close()}}const o={code:0,reason:"client side error"},r={code:0,reason:"handshake timeout"},l="P",c="p",a=2e4,u=()=>!0,d=(t,s,e)=>!e;class _{M;A;O=[];j=function(){let t=1;return()=>t++}();constructor(t,s){this.M=t,this.A=s}D(t){this.O.push({J:void 0,N:t,F:[],G:[]})}q(t,s,e){if(s||e)if(this.O.length){const t=this.O[this.O.length-1];t.F.push(s??g),t.G.push(e??g)}else s&&Promise.resolve(t).then(s)}U(t){let s=0;for(let e=0;e<this.O.length;++e){const i=this.O[e];this.A(t,i.N,void 0!==i.J)?(i.J=void 0,this.O[e-s]=i):(i.G.forEach((t=>t("message possibly lost"))),++s)}this.O.length-=s}W(t){for(let t=0,s=0;t<=this.O.length;++t){const e=this.O[t];if(!e||void 0!==e.J||e.F.length>0||e.G.length>0){const e=t-s;if(e>1){const i=this.O[t-1],n=this.O.splice(s,e,i);i.N=this.M.combine(n.map((t=>t.N))),t-=e-1}s=t+1}}for(const s of this.O)void 0===s.J&&(s.J=this.j(),t(JSON.stringify({change:s.N,id:s.J})))}X(t){const s=void 0===t?-1:this.O.findIndex((s=>s.J===t));return-1===s?{B:null,H:!1}:{B:this.O.splice(s,1)[0],H:0===s}}K(t){if(!this.O.length)return t;const s=this.M.combine(this.O.map((({N:t})=>t)));return this.M.update(t,s)}}const g=()=>null;class f extends i{M;I;R=!0;S={V:0,Y:[]};Z;tt=new Set;st=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=u}={}){super(),this.M=s,this.Z=new _(s,n),this.I=new h(e,i),this.I.addEventListener("message",this.et),this.I.addEventListener("connected",this.it),this.I.addEventListener("connectionfailure",this.nt),this.I.addEventListener("disconnected",this.ht)}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={ot:t,F:s,G:e};switch(this.S.V){case-1:throw new Error("closed");case 0:this.S.Y.push(i);break;case 1:this.rt(this.lt(this.S.ct,[i])),this.dt.ut()}}));lt(t,s){return this.st((()=>{for(const{ot:i,F:n,G:h}of s){if(i.length){const{S:s,T:n}=e(this.M,t,i);t=s,this.Z.D(n)}this.Z.q(t,n,h)}return t}))}dt=function(t){let s=null;const e=()=>{null!==s&&(clearTimeout(s),s=null)},i=()=>{e(),t()};return{_t:i,ut:()=>{null===s&&(s=setTimeout(i,0))},o:e}}((()=>{this.I.isConnected()&&!this.R&&this.Z.W(this.I.send)}));gt(t){if(-1!==this.S.V){if(this.R=!1,0===this.S.V){const s=this.lt(t.init,this.S.Y);this.S={V:1,ft:t.init,ct:s},this.rt(s,!0)}else this.S.ft=t.init,this.Z.U(t.init),this.rt(this.Z.K(t.init));this.dt._t()}else this.wt(`Ignoring init after closing: ${JSON.stringify(t)}`)}vt(t){if(1!==this.S.V)return void this.wt(`Ignoring change before init: ${JSON.stringify(t)}`);const s=this.S.ft=this.M.update(this.S.ft,t.change),{B:e,H:i}=this.Z.X(t.id);i||this.rt(this.Z.K(s)),e?.F.forEach((t=>t(s)))}bt(t){if(1!==this.S.V)return void this.wt(`Ignoring error before init: ${JSON.stringify(t)}`);const{B:s}=this.Z.X(t.id);s?(this.wt(`API rejected update: ${t.error}`),s?.G.forEach((s=>s(t.error))),this.rt(this.Z.K(this.S.ft))):this.wt(`API sent error: ${t.error}`)}yt(){this.I.send("x"),this.R?this.wt("Unexpected extra close message"):(this.R=!0,this.dispatchEvent(n("disconnected",m)))}et=t=>{if("X"===t.detail)return void this.yt();const s=JSON.parse(t.detail);"change"in s?this.vt(s):"init"in s?this.gt(s):"error"in s?this.bt(s):this.wt(`Ignoring unknown API message: ${t.detail}`)};addStateListener(t){this.tt.add(t),1===this.S.V&&t(this.S.ct)}removeStateListener(t){this.tt.delete(t)}rt(t,s=!1){if(1!==this.S.V)throw new Error("invalid state");if(s||this.S.ct!==t){this.S.ct=t;for(const s of this.tt)s(t)}}getState(){return 1===this.S.V?this.S.ct:void 0}wt(t){this.dispatchEvent(n("warning",new Error(t)))}it=()=>{this.dispatchEvent(n("connected"))};nt=t=>{this.dispatchEvent(n("warning",new Error(`connection failure: ${t.detail.code} ${t.detail.reason}`)))};ht=t=>{this.R||(this.R=!0,this.dispatchEvent(n("disconnected",t.detail)))};close(){this.R=!0,this.S={V:-1},this.I.close(),this.dt.o(),this.tt.clear(),this.I.removeEventListener("message",this.et),this.I.removeEventListener("connected",this.it),this.I.removeEventListener("connectionfailure",this.nt),this.I.removeEventListener("disconnected",this.ht)}}const m={code:0,reason:"graceful shutdown"},w=s({base:2,initialDelay:1e3,maxDelay:6e5,randomness:.3});export{u as AT_LEAST_ONCE,d as AT_MOST_ONCE,t as OnlineScheduler,f as SharedReducer,s as exponentialDelay};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-reducer",
3
- "version": "4.0.2",
3
+ "version": "5.0.1",
4
4
  "description": "shared state management",
5
5
  "author": "David Evans",
6
6
  "license": "MIT",
@@ -47,14 +47,14 @@
47
47
  "@rollup/plugin-terser": "0.4.x",
48
48
  "@rollup/plugin-typescript": "11.x",
49
49
  "collection-storage": "3.x",
50
- "json-immutability-helper": "3.x",
50
+ "json-immutability-helper": "4.x",
51
51
  "lean-test": "2.x",
52
52
  "prettier": "3.3.3",
53
53
  "rollup": "4.x",
54
54
  "rollup-plugin-dts": "6.x",
55
55
  "superwstest": "2.x",
56
56
  "tslib": "2.7.x",
57
- "typescript": "5.5.x",
57
+ "typescript": "5.6.x",
58
58
  "websocket-express": "3.x"
59
59
  }
60
60
  }