statewire 0.23.0 → 0.24.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.
Files changed (43) hide show
  1. package/dist/client/core.d.ts +4 -3
  2. package/dist/client/core.d.ts.map +1 -1
  3. package/dist/client/core.js +56 -45
  4. package/dist/client/core.js.map +1 -1
  5. package/dist/client/session.d.ts +56 -12
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +632 -260
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/sessionStore.d.ts +1 -0
  10. package/dist/client/sessionStore.d.ts.map +1 -1
  11. package/dist/client/sessionStore.js +1 -0
  12. package/dist/client/sessionStore.js.map +1 -1
  13. package/dist/host/host.d.ts +6 -5
  14. package/dist/host/host.d.ts.map +1 -1
  15. package/dist/host/host.js.map +1 -1
  16. package/dist/indexedDb.d.ts +15 -0
  17. package/dist/indexedDb.d.ts.map +1 -0
  18. package/dist/indexedDb.js +171 -0
  19. package/dist/indexedDb.js.map +1 -0
  20. package/dist/statewire.d.ts +117 -59
  21. package/dist/statewire.d.ts.map +1 -1
  22. package/dist/statewire.js +13 -25
  23. package/dist/statewire.js.map +1 -1
  24. package/dist/storage.d.ts +42 -0
  25. package/dist/storage.d.ts.map +1 -1
  26. package/dist/storage.js +387 -125
  27. package/dist/storage.js.map +1 -1
  28. package/dist/transports/remote.d.ts.map +1 -1
  29. package/dist/transports/remote.js +7 -9
  30. package/dist/transports/remote.js.map +1 -1
  31. package/dist/types.d.ts +6 -7
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/package.json +2 -1
  35. package/src/client/core.ts +81 -64
  36. package/src/client/session.ts +886 -424
  37. package/src/client/sessionStore.ts +2 -0
  38. package/src/host/host.ts +10 -3
  39. package/src/indexedDb.ts +167 -0
  40. package/src/statewire.ts +133 -86
  41. package/src/storage.ts +548 -112
  42. package/src/transports/remote.ts +13 -14
  43. package/src/types.ts +6 -7
@@ -16,7 +16,6 @@ export const CONNECTED_CONNECTION: StatewireClient.Connection = {
16
16
  reconnect: () => {},
17
17
  };
18
18
 
19
- const NO_FAILED: readonly StatewireClient.FailedCommand[] = [];
20
19
  const NO_OUTBOX: readonly StatewireClient.OutboxEntry[] = [];
21
20
 
22
21
  const failureOf = (error: unknown): StatewireClient.SendFailure =>
@@ -28,13 +27,17 @@ const failureOf = (error: unknown): StatewireClient.SendFailure =>
28
27
  message: error instanceof Error ? error.message : String(error),
29
28
  };
30
29
 
31
- type Overlay = { inv: CommandsInvocation; dropped: boolean };
30
+ type Overlay = { inv: CommandsInvocation };
32
31
 
33
32
  type Core = {
34
33
  getOptions: () => StatewireClient.Options<any, any>;
35
34
  value: Statewire<unknown> | null;
36
35
  source: Statewire.StateSource | null;
37
36
  overlays: Overlay[];
37
+ overlayOutbox: readonly StatewireClient.OutboxEntry[];
38
+ /** Folds whose reducer threw. */
39
+ dropped: WeakSet<object>;
40
+ droppedIds: Set<string>;
38
41
  updates: Statewire.CommandUpdate[];
39
42
  pendingChanged: StatewirePath[];
40
43
  canonical: unknown;
@@ -45,7 +48,6 @@ type Core = {
45
48
  notifiedConnection: StatewireClient.Connection | null;
46
49
  notifiedError: StatewireClient.ConnectionError | undefined;
47
50
  notifiedBusy: boolean;
48
- notifiedFailed: readonly StatewireClient.FailedCommand[];
49
51
  notifiedOutbox: readonly StatewireClient.OutboxEntry[];
50
52
  stale: boolean;
51
53
  everAttached: boolean;
@@ -58,7 +60,6 @@ type Core = {
58
60
  commandsListener: CommandsListener;
59
61
  getState: () => unknown;
60
62
  getBusy: () => boolean;
61
- getFailed: () => readonly StatewireClient.FailedCommand[];
62
63
  getOutbox: () => readonly StatewireClient.OutboxEntry[];
63
64
  rematerialize: () => void;
64
65
  commands: Statewire.CommandsProxy<any>;
@@ -100,15 +101,6 @@ const drainCanonical = (c: Core) => {
100
101
  }
101
102
  };
102
103
 
103
- const dropCovered = (c: Core) => {
104
- const { failureCovered } = c.getOptions();
105
- const failures = c.value?.failures;
106
- if (!failureCovered || !failures || c.canonical === undefined) return;
107
- for (const entry of failures.list()) {
108
- if (failureCovered(c.canonical as never, entry)) failures.discard(entry);
109
- }
110
- };
111
-
112
104
  const recompute = (c: Core) => {
113
105
  if (!c.value) return;
114
106
  const { optimistic, materialize, materializeKey } = c.getOptions();
@@ -117,44 +109,82 @@ const recompute = (c: Core) => {
117
109
  c.stale = false;
118
110
  const prevCanonical = c.canonical;
119
111
  drainCanonical(c);
120
- if (c.canonical !== prevCanonical) dropCovered(c);
112
+ const outbox = c.value.outbox?.list() ?? NO_OUTBOX;
113
+ if (outbox !== c.overlayOutbox) {
114
+ c.overlayOutbox = outbox;
115
+ c.overlaysDirty = true;
116
+ }
121
117
  if (c.canonical === prevCanonical && !c.overlaysDirty && !rekeyed) return;
122
118
  c.overlaysDirty = false;
123
119
  c.materializeKey = materializeKey;
124
120
  let next = c.canonical;
125
121
  let paths: readonly StatewirePath[] = [];
126
122
  if (optimistic && c.canonical !== undefined) {
127
- for (const overlay of c.overlays) {
128
- if (overlay.dropped) continue;
129
- const { inv } = overlay;
130
- if (
131
- inv.protocol !== undefined &&
132
- inv.protocol !== (c.getOptions().protocol ?? "default")
133
- )
123
+ const protocol = c.getOptions().protocol ?? "default";
124
+ const unsent = outbox.filter((e) => e.state !== "sending");
125
+ const unsentIds = new Set(unsent.map((e) => e.id));
126
+ const clientId = c.value.outbox?.clientId();
127
+ // In queue order: the commands on their way, then the outbox's queued
128
+ // and held entries across sharers.
129
+ const folds: {
130
+ key: object;
131
+ id: string | undefined;
132
+ method: string;
133
+ params: readonly unknown[];
134
+ ctx: StatewireClient.OptimisticContext;
135
+ }[] = [
136
+ ...c.overlays
137
+ .filter(
138
+ ({ inv }) => inv.protocol === undefined || inv.protocol === protocol,
139
+ )
140
+ .filter(({ inv }) => inv.id === undefined || !unsentIds.has(inv.id))
141
+ .map(({ inv }) => ({
142
+ key: inv,
143
+ id: inv.id,
144
+ method: inv.method,
145
+ params: inv.params,
146
+ ctx: {
147
+ ts: inv.ts,
148
+ ...(inv.seq !== undefined && { seq: inv.seq }),
149
+ ...(inv.clientId !== undefined && { clientId: inv.clientId }),
150
+ state:
151
+ inv.seq !== undefined ? "sending" : inv.held ? "held" : "queued",
152
+ localMeta: inv.localMeta,
153
+ } satisfies StatewireClient.OptimisticContext,
154
+ })),
155
+ ...unsent
156
+ .filter((e) => e.protocol === protocol)
157
+ .map((e) => ({
158
+ key: e,
159
+ id: e.id,
160
+ method: e.command.method,
161
+ params: e.command.params,
162
+ ctx: {
163
+ ts: e.ts,
164
+ ...(clientId !== undefined && { clientId }),
165
+ state: e.state,
166
+ localMeta: e.localMeta,
167
+ } satisfies StatewireClient.OptimisticContext,
168
+ })),
169
+ ];
170
+ for (const fold of folds) {
171
+ if (c.dropped.has(fold.key) || (fold.id && c.droppedIds.has(fold.id)))
134
172
  continue;
135
- const ctx: StatewireClient.OptimisticContext = {
136
- ts: inv.ts,
137
- ...(inv.seq !== undefined && { seq: inv.seq }),
138
- ...(inv.clientId !== undefined && { clientId: inv.clientId }),
139
- held:
140
- inv.seq === undefined && inv.hold !== undefined
141
- ? { meta: inv.hold.meta }
142
- : null,
143
- };
144
173
  try {
145
174
  const recorded = recordMutations(next, (draft) =>
146
175
  optimistic(
147
176
  draft as never,
148
- { method: inv.method, params: inv.params } as Statewire.Command,
149
- ctx,
177
+ { method: fold.method, params: fold.params } as Statewire.Command,
178
+ fold.ctx,
150
179
  ),
151
180
  );
152
181
  next = recorded.state;
153
182
  paths = [...paths, ...recorded.changedPaths];
154
183
  } catch (error) {
155
- overlay.dropped = true;
184
+ c.dropped.add(fold.key);
185
+ if (fold.id) c.droppedIds.add(fold.id);
156
186
  console.error(
157
- `statewire: optimistic reducer threw for "${inv.method}"; dropping its optimistic update`,
187
+ `statewire: optimistic reducer threw for "${fold.method}"; dropping its optimistic update`,
158
188
  error,
159
189
  );
160
190
  }
@@ -224,18 +254,8 @@ const flush = (c: Core, scheduled = false) => {
224
254
  const busy = c.getBusy();
225
255
  const busyChanged = busy !== c.notifiedBusy;
226
256
  c.notifiedBusy = busy;
227
- // The failed snapshot advances only on the scheduled pass: settles land
257
+ // The outbox snapshot advances only on the scheduled pass: settles land
228
258
  // mid-unmount, where a changed snapshot re-renders the unmounting tree.
229
- const failed = c.value?.failures?.list() ?? NO_FAILED;
230
- let failedChanged = false;
231
- if (failed !== c.notifiedFailed) {
232
- if (scheduled) {
233
- c.notifiedFailed = failed;
234
- failedChanged = true;
235
- } else {
236
- schedule(c);
237
- }
238
- }
239
259
  const outbox = c.value?.outbox?.list() ?? NO_OUTBOX;
240
260
  let outboxChanged = false;
241
261
  if (outbox !== c.notifiedOutbox) {
@@ -246,13 +266,7 @@ const flush = (c: Core, scheduled = false) => {
246
266
  schedule(c);
247
267
  }
248
268
  }
249
- if (
250
- stateChanged ||
251
- connectionChanged ||
252
- busyChanged ||
253
- failedChanged ||
254
- outboxChanged
255
- ) {
269
+ if (stateChanged || connectionChanged || busyChanged || outboxChanged) {
256
270
  for (const listener of [...c.listeners]) listener();
257
271
  }
258
272
  } finally {
@@ -274,6 +288,7 @@ const note = (
274
288
  params: inv.params as never[],
275
289
  ...(inv.seq !== undefined && { seq: inv.seq }),
276
290
  status,
291
+ ...(inv.localMeta !== undefined && { localMeta: inv.localMeta }),
277
292
  ...extra,
278
293
  });
279
294
  schedule(c);
@@ -329,6 +344,9 @@ export const useStatewireCore = (
329
344
  value: null,
330
345
  source: null,
331
346
  overlays: [],
347
+ overlayOutbox: NO_OUTBOX,
348
+ dropped: new WeakSet(),
349
+ droppedIds: new Set(),
332
350
  updates: [],
333
351
  pendingChanged: [],
334
352
  canonical: undefined,
@@ -339,7 +357,6 @@ export const useStatewireCore = (
339
357
  notifiedConnection: null,
340
358
  notifiedError: undefined,
341
359
  notifiedBusy: true,
342
- notifiedFailed: NO_FAILED,
343
360
  notifiedOutbox: NO_OUTBOX,
344
361
  stale: false,
345
362
  everAttached: false,
@@ -354,7 +371,7 @@ export const useStatewireCore = (
354
371
  }),
355
372
  commandsListener: {
356
373
  onStart: (inv) => {
357
- c.overlays.push({ inv, dropped: false });
374
+ c.overlays.push({ inv });
358
375
  c.overlaysDirty = true;
359
376
  markStale(c);
360
377
  note(c, inv, "queued");
@@ -373,15 +390,16 @@ export const useStatewireCore = (
373
390
  c.overlaysDirty = true;
374
391
  markStale(c);
375
392
  },
376
- onSettle: (inv, outcome) => {
377
- note(
378
- c,
379
- inv,
380
- "settled",
381
- outcome.type === "accepted"
382
- ? { result: outcome.payload }
383
- : { failure: failureOf(outcome.error) },
384
- );
393
+ onSettle: (inv, outcome, durable) => {
394
+ if (!durable)
395
+ note(
396
+ c,
397
+ inv,
398
+ "settled",
399
+ outcome.type === "accepted"
400
+ ? { result: outcome.payload }
401
+ : { failure: failureOf(outcome.error) },
402
+ );
385
403
  flush(c);
386
404
  },
387
405
  },
@@ -390,7 +408,6 @@ export const useStatewireCore = (
390
408
  return c.published;
391
409
  },
392
410
  getBusy: () => c.value?.isBusy ?? true,
393
- getFailed: () => c.notifiedFailed,
394
411
  getOutbox: () => c.notifiedOutbox,
395
412
  rematerialize: () => flush(c),
396
413
  commands: createMethodProxy<Statewire.CommandsProxy<any>>(