statewire 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -78
- package/dist/client/connection.d.ts +20 -6
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +120 -52
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +3 -2
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +10 -4
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +2 -0
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +97 -17
- package/dist/client/session.js.map +1 -1
- package/dist/host/adapter.d.ts +1 -1
- package/dist/host/adapter.d.ts.map +1 -1
- package/dist/host/adapter.js +7 -1
- package/dist/host/adapter.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +45 -5
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts +2 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +108 -47
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +33 -8
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +388 -58
- package/dist/host/socket.js.map +1 -1
- package/dist/host/work.d.ts +7 -0
- package/dist/host/work.d.ts.map +1 -0
- package/dist/host/work.js +53 -0
- package/dist/host/work.js.map +1 -0
- package/dist/host-internal.d.ts +42 -4
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +2 -1
- package/dist/host.d.ts +16 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/statewire.d.ts +21 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +2 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/sse.d.ts +1 -4
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +47 -56
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.js +2 -2
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +1 -1
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +54 -41
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/documents.d.ts +20 -0
- package/dist/wire/documents.d.ts.map +1 -0
- package/dist/wire/documents.js +74 -0
- package/dist/wire/documents.js.map +1 -0
- package/dist/wire/limits.d.ts +10 -0
- package/dist/wire/limits.d.ts.map +1 -0
- package/dist/wire/limits.js +11 -0
- package/dist/wire/limits.js.map +1 -0
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +1 -1
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +1 -2
- package/dist/wire/ops.js.map +1 -1
- package/dist/wire/version.d.ts +7 -0
- package/dist/wire/version.d.ts.map +1 -0
- package/dist/wire/version.js +8 -0
- package/dist/wire/version.js.map +1 -0
- package/package.json +1 -1
- package/src/client/connection.ts +213 -67
- package/src/client/core.ts +10 -3
- package/src/client/session.ts +106 -30
- package/src/host/adapter.ts +10 -2
- package/src/host/host.ts +54 -4
- package/src/host/http.ts +168 -64
- package/src/host/socket.ts +603 -60
- package/src/host/work.ts +50 -0
- package/src/host-internal.ts +44 -3
- package/src/host.ts +18 -2
- package/src/statewire.ts +33 -10
- package/src/transports/sse.ts +60 -68
- package/src/transports/testing.ts +2 -2
- package/src/transports/ws.ts +29 -48
- package/src/types.ts +1 -0
- package/src/wire/documents.ts +109 -0
- package/src/wire/limits.ts +6 -0
- package/src/wire/meta.ts +6 -1
- package/src/wire/ops.ts +1 -2
- package/src/wire/version.ts +3 -0
package/src/client/session.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { applyDocumentOps } from "../wire/documents";
|
|
2
|
+
import type { StatewireDocuments } from "../wire/documents";
|
|
1
3
|
import { useContext, useEffect, useState } from "react";
|
|
2
|
-
|
|
4
|
+
|
|
3
5
|
import type { StatewirePath } from "../wire/ops";
|
|
6
|
+
import { MAX_COMMANDS_PER_FRAME } from "../wire/limits";
|
|
4
7
|
import { TERMINAL } from "./connection";
|
|
5
8
|
import type { StatewireCmd, StatewireSyn } from "./connection";
|
|
6
9
|
import {
|
|
@@ -103,6 +106,7 @@ type Session = {
|
|
|
103
106
|
frameFailures: number;
|
|
104
107
|
pending: Pending[];
|
|
105
108
|
canonical: unknown;
|
|
109
|
+
documents: StatewireDocuments.State;
|
|
106
110
|
lastPublished: unknown;
|
|
107
111
|
changed: StatewirePath[];
|
|
108
112
|
source: Statewire.StateSource;
|
|
@@ -238,7 +242,7 @@ export const failedCommands = (
|
|
|
238
242
|
s: Session,
|
|
239
243
|
): readonly StatewireClient.FailedCommand[] => s.record.failed ?? NO_FAILED;
|
|
240
244
|
|
|
241
|
-
const
|
|
245
|
+
const retainFailedInner = (
|
|
242
246
|
s: Session,
|
|
243
247
|
entry: Pending,
|
|
244
248
|
failure: StatewireClient.SendFailure,
|
|
@@ -260,6 +264,18 @@ const retainFailed = (
|
|
|
260
264
|
s.notify();
|
|
261
265
|
};
|
|
262
266
|
|
|
267
|
+
const retainFailed = (
|
|
268
|
+
s: Session,
|
|
269
|
+
entry: Pending,
|
|
270
|
+
failure: StatewireClient.SendFailure,
|
|
271
|
+
) => {
|
|
272
|
+
if (!s.shared) {
|
|
273
|
+
retainFailedInner(s, entry, failure);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
void transact(s, () => retainFailedInner(s, entry, failure));
|
|
277
|
+
};
|
|
278
|
+
|
|
263
279
|
const settleEntry = (
|
|
264
280
|
s: Session,
|
|
265
281
|
entry: Pending,
|
|
@@ -481,8 +497,9 @@ const handleCmds = (s: Session, cmds: StatewireCmd[]) => {
|
|
|
481
497
|
};
|
|
482
498
|
|
|
483
499
|
const ingestOps = (s: Session, ops: unknown[]) => {
|
|
484
|
-
const applied =
|
|
485
|
-
s.
|
|
500
|
+
const applied = applyDocumentOps(s.documents, ops);
|
|
501
|
+
s.documents = applied;
|
|
502
|
+
s.canonical = applied.documents.get(0)?.value;
|
|
486
503
|
s.changed.push(...applied.changedPaths);
|
|
487
504
|
};
|
|
488
505
|
|
|
@@ -546,6 +563,7 @@ const handleHello = (
|
|
|
546
563
|
if (syn.record !== undefined && syn.record !== "ephemeral") {
|
|
547
564
|
protocolError(`unknown record: ${String(syn.record)}`);
|
|
548
565
|
}
|
|
566
|
+
s.documents = { mode: null, documents: new Map() };
|
|
549
567
|
ingestOps(s, ops);
|
|
550
568
|
s.attach = "attached";
|
|
551
569
|
s.attachEpoch += 1;
|
|
@@ -642,7 +660,7 @@ const handleDetach = (s: Session) => {
|
|
|
642
660
|
};
|
|
643
661
|
|
|
644
662
|
const handleFinish = (s: Session, { reason }: { reason: string }) => {
|
|
645
|
-
if (reason === "
|
|
663
|
+
if (reason === "reconnect" || reason === "error") return;
|
|
646
664
|
s.attach = "finished";
|
|
647
665
|
settleAll(s, "gone");
|
|
648
666
|
publish(s);
|
|
@@ -663,6 +681,7 @@ const buildLanes = (s: Session): Lanes | null => {
|
|
|
663
681
|
const ctxEntries: CtxPending[] = [];
|
|
664
682
|
for (const entry of s.ctxPending) {
|
|
665
683
|
if (entry.posted) continue;
|
|
684
|
+
if (ctxEntries.length >= MAX_COMMANDS_PER_FRAME) break;
|
|
666
685
|
if (ctxEntries.length > 0 && entry.seq !== ctxEntries.at(-1)!.seq + 1)
|
|
667
686
|
break;
|
|
668
687
|
ctxEntries.push(entry);
|
|
@@ -670,6 +689,7 @@ const buildLanes = (s: Session): Lanes | null => {
|
|
|
670
689
|
const cmdEntries: Pending[] = [];
|
|
671
690
|
for (const entry of s.pending) {
|
|
672
691
|
if (entry.posted) continue;
|
|
692
|
+
if (cmdEntries.length >= MAX_COMMANDS_PER_FRAME) break;
|
|
673
693
|
if (cmdEntries.length > 0 && entry.seq !== cmdEntries.at(-1)!.seq + 1)
|
|
674
694
|
break;
|
|
675
695
|
cmdEntries.push(entry);
|
|
@@ -713,7 +733,8 @@ const pump = async (s: Session) => {
|
|
|
713
733
|
if (buildLanes(s) === null) return;
|
|
714
734
|
if (s.attach !== "attached" || !transport) {
|
|
715
735
|
if (
|
|
716
|
-
transport?.connection.status === "
|
|
736
|
+
(transport?.connection.status === "disconnected" &&
|
|
737
|
+
transport.connection.reason === "idle") ||
|
|
717
738
|
(transport?.parked && s.pending.length > 0)
|
|
718
739
|
) {
|
|
719
740
|
transport.connection.reconnect();
|
|
@@ -861,9 +882,17 @@ const enqueue = (
|
|
|
861
882
|
void pump(s);
|
|
862
883
|
return;
|
|
863
884
|
}
|
|
864
|
-
void transact(s, () =>
|
|
865
|
-
|
|
866
|
-
|
|
885
|
+
void transact(s, () => {
|
|
886
|
+
if (!s.running) {
|
|
887
|
+
reject(new StatewireSendError({ fate: "aborted", cause: "unmount" }));
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (isFinished(s)) {
|
|
891
|
+
reject(new StatewireSendError({ fate: "aborted", cause: "stream-gone" }));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
enqueueInner(s, command, params, resolve, reject);
|
|
895
|
+
}).then(() => {
|
|
867
896
|
wake(s);
|
|
868
897
|
void pump(s);
|
|
869
898
|
});
|
|
@@ -927,18 +956,25 @@ const sendCommand = (
|
|
|
927
956
|
return result;
|
|
928
957
|
};
|
|
929
958
|
|
|
930
|
-
const
|
|
959
|
+
const findFailed = (
|
|
931
960
|
s: Session,
|
|
932
961
|
entry: StatewireClient.FailedCommand,
|
|
933
|
-
): StatewireClient.FailedCommand =>
|
|
934
|
-
|
|
935
|
-
const found = failed.find(
|
|
962
|
+
): StatewireClient.FailedCommand | undefined =>
|
|
963
|
+
(s.record.failed ?? []).find(
|
|
936
964
|
(f) =>
|
|
937
965
|
f.seq === entry.seq && f.ts === entry.ts && f.command === entry.command,
|
|
938
966
|
);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
967
|
+
|
|
968
|
+
const unknownFailed = (entry: StatewireClient.FailedCommand) =>
|
|
969
|
+
new Error(`statewire: unknown failed command "${entry.command}"`);
|
|
970
|
+
|
|
971
|
+
const takeFailed = (
|
|
972
|
+
s: Session,
|
|
973
|
+
entry: StatewireClient.FailedCommand,
|
|
974
|
+
): StatewireClient.FailedCommand => {
|
|
975
|
+
const failed = s.record.failed ?? [];
|
|
976
|
+
const found = findFailed(s, entry);
|
|
977
|
+
if (found === undefined) throw unknownFailed(entry);
|
|
942
978
|
setRecord(s, { ...s.record, failed: failed.filter((f) => f !== found) });
|
|
943
979
|
s.notify();
|
|
944
980
|
return found;
|
|
@@ -948,7 +984,14 @@ export const discardFailed = (
|
|
|
948
984
|
s: Session,
|
|
949
985
|
entry: StatewireClient.FailedCommand,
|
|
950
986
|
): void => {
|
|
951
|
-
|
|
987
|
+
if (!s.shared) {
|
|
988
|
+
takeFailed(s, entry);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (findFailed(s, entry) === undefined) throw unknownFailed(entry);
|
|
992
|
+
void transact(s, () => {
|
|
993
|
+
if (findFailed(s, entry) !== undefined) takeFailed(s, entry);
|
|
994
|
+
});
|
|
952
995
|
};
|
|
953
996
|
|
|
954
997
|
export const retryFailed = (
|
|
@@ -961,23 +1004,52 @@ export const retryFailed = (
|
|
|
961
1004
|
if (isFinished(s)) {
|
|
962
1005
|
return rejectedSend({ fate: "aborted", cause: "stream-gone" });
|
|
963
1006
|
}
|
|
964
|
-
const found =
|
|
1007
|
+
const found = findFailed(s, entry);
|
|
1008
|
+
if (found === undefined) throw unknownFailed(entry);
|
|
965
1009
|
const params =
|
|
966
1010
|
found.meta === undefined
|
|
967
1011
|
? found.params
|
|
968
1012
|
: (reviveValue([...found.params], found.meta) as unknown[]);
|
|
969
1013
|
const result = new Promise<unknown>((resolve, reject) => {
|
|
970
|
-
|
|
971
|
-
s
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1014
|
+
const retry = () => {
|
|
1015
|
+
if (!s.running) {
|
|
1016
|
+
reject(new StatewireSendError({ fate: "aborted", cause: "unmount" }));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (isFinished(s)) {
|
|
1020
|
+
reject(
|
|
1021
|
+
new StatewireSendError({ fate: "aborted", cause: "stream-gone" }),
|
|
1022
|
+
);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const current = findFailed(s, entry);
|
|
1026
|
+
if (current === undefined) {
|
|
1027
|
+
reject(unknownFailed(entry));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
takeFailed(s, current);
|
|
1031
|
+
enqueueInner(
|
|
1032
|
+
s,
|
|
1033
|
+
{
|
|
1034
|
+
method: current.command,
|
|
1035
|
+
params: [...current.params],
|
|
1036
|
+
...(current.meta !== undefined && { meta: current.meta }),
|
|
1037
|
+
},
|
|
1038
|
+
params,
|
|
1039
|
+
resolve,
|
|
1040
|
+
reject,
|
|
1041
|
+
);
|
|
1042
|
+
};
|
|
1043
|
+
if (!s.shared) {
|
|
1044
|
+
retry();
|
|
1045
|
+
wake(s);
|
|
1046
|
+
void pump(s);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
void transact(s, retry).then(() => {
|
|
1050
|
+
wake(s);
|
|
1051
|
+
void pump(s);
|
|
1052
|
+
}, reject);
|
|
981
1053
|
});
|
|
982
1054
|
result.catch(() => {});
|
|
983
1055
|
return result;
|
|
@@ -1009,7 +1081,10 @@ const setContext = (s: Session, value: Record<string, unknown>): void => {
|
|
|
1009
1081
|
void pump(s);
|
|
1010
1082
|
return;
|
|
1011
1083
|
}
|
|
1012
|
-
void transact(s,
|
|
1084
|
+
void transact(s, () => {
|
|
1085
|
+
if (!s.running || isFinished(s)) return;
|
|
1086
|
+
mint();
|
|
1087
|
+
}).then(() => {
|
|
1013
1088
|
wake(s);
|
|
1014
1089
|
void pump(s);
|
|
1015
1090
|
});
|
|
@@ -1080,6 +1155,7 @@ export const useStatewireSession = (
|
|
|
1080
1155
|
frameFailures: 0,
|
|
1081
1156
|
pending: [],
|
|
1082
1157
|
canonical: undefined,
|
|
1158
|
+
documents: { mode: null, documents: new Map() },
|
|
1083
1159
|
lastPublished: undefined,
|
|
1084
1160
|
changed: [],
|
|
1085
1161
|
source: {
|
package/src/host/adapter.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLIENT_ID_RE } from "./socket";
|
|
1
2
|
import type { StatewireHostInternal } from "../host-internal";
|
|
2
3
|
|
|
3
4
|
export const CLIENT_QUERY_PARAM = "client";
|
|
@@ -20,14 +21,21 @@ export const clientIdFromUrl = (url: string | null | undefined): string => {
|
|
|
20
21
|
`statewire: url is missing the "${CLIENT_QUERY_PARAM}" query parameter`,
|
|
21
22
|
);
|
|
22
23
|
}
|
|
24
|
+
if (!CLIENT_ID_RE.test(clientId))
|
|
25
|
+
throw new Error("statewire: invalid client id");
|
|
23
26
|
return clientId;
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export const textFrameOrClose = (
|
|
27
|
-
socket: Pick<StatewireHostInternal.Socket, "close">,
|
|
30
|
+
socket: Pick<StatewireHostInternal.Socket, "send" | "close">,
|
|
28
31
|
data: unknown,
|
|
29
32
|
): string | null => {
|
|
30
33
|
if (typeof data === "string") return data;
|
|
31
|
-
socket.
|
|
34
|
+
socket.send(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
fin: { reason: "protocol-error", message: "malformed frame" },
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
socket.close(1008, "malformed frame");
|
|
32
40
|
return null;
|
|
33
41
|
};
|
package/src/host/host.ts
CHANGED
|
@@ -136,6 +136,7 @@ export type CommandsHost = {
|
|
|
136
136
|
submit?: (call: CommandsSubmission) => Promise<unknown>;
|
|
137
137
|
/** Registered command names, read live. */
|
|
138
138
|
methods?: () => string[];
|
|
139
|
+
abort?: () => void;
|
|
139
140
|
};
|
|
140
141
|
|
|
141
142
|
const commandsHosts = new WeakMap<object, CommandsHost>();
|
|
@@ -158,12 +159,15 @@ type QueuedCall = {
|
|
|
158
159
|
|
|
159
160
|
type ActiveInvocation = CommandsInvocation & {
|
|
160
161
|
applied: boolean;
|
|
162
|
+
controller: AbortController;
|
|
163
|
+
settled: boolean;
|
|
161
164
|
origin?: CommandsSubmission;
|
|
162
165
|
};
|
|
163
166
|
|
|
164
167
|
type CommandsCell = {
|
|
165
168
|
handlers: Record<string, (...args: never[]) => unknown> | null;
|
|
166
169
|
queue: QueuedCall[];
|
|
170
|
+
active: Map<ActiveInvocation, QueuedCall>;
|
|
167
171
|
running: boolean;
|
|
168
172
|
listener: CommandsListener | null;
|
|
169
173
|
seq: number;
|
|
@@ -178,6 +182,7 @@ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
|
|
|
178
182
|
get clientId(): string {
|
|
179
183
|
throw new Error("statewire: in-process dispatch has no client id");
|
|
180
184
|
},
|
|
185
|
+
auth: undefined,
|
|
181
186
|
context: null,
|
|
182
187
|
isConnected: true,
|
|
183
188
|
});
|
|
@@ -202,6 +207,7 @@ const applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
|
|
|
202
207
|
if (inv.applied) return;
|
|
203
208
|
inv.applied = true;
|
|
204
209
|
cell.listener?.onApplied(inv);
|
|
210
|
+
cell.listener?.onCovered?.(inv);
|
|
205
211
|
inv.origin?.onApplied?.();
|
|
206
212
|
};
|
|
207
213
|
|
|
@@ -211,6 +217,9 @@ const settleInvocation = (
|
|
|
211
217
|
call: QueuedCall,
|
|
212
218
|
settled: Settled,
|
|
213
219
|
) => {
|
|
220
|
+
if (inv.settled) return;
|
|
221
|
+
inv.settled = true;
|
|
222
|
+
cell.active.delete(inv);
|
|
214
223
|
applyInvocation(cell, inv);
|
|
215
224
|
if (settled.type === "resolved") {
|
|
216
225
|
const outcome: CommandsOutcome =
|
|
@@ -229,13 +238,40 @@ const settleInvocation = (
|
|
|
229
238
|
}
|
|
230
239
|
};
|
|
231
240
|
|
|
241
|
+
const abortInvocations = (cell: CommandsCell) => {
|
|
242
|
+
for (const [inv, call] of cell.active) {
|
|
243
|
+
inv.settled = true;
|
|
244
|
+
inv.controller.abort();
|
|
245
|
+
const error = new StatewireSendError({ fate: "crashed", cause: "server" });
|
|
246
|
+
const outcome: CommandsOutcome = { type: "rejected", error };
|
|
247
|
+
cell.listener?.onSettle(inv, outcome);
|
|
248
|
+
inv.origin?.onSettle?.(outcome);
|
|
249
|
+
call.reject(error);
|
|
250
|
+
}
|
|
251
|
+
cell.active.clear();
|
|
252
|
+
};
|
|
253
|
+
|
|
232
254
|
const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
233
255
|
typeof value === "object" &&
|
|
234
256
|
value !== null &&
|
|
235
257
|
typeof (value as { then?: unknown }).then === "function";
|
|
236
258
|
|
|
259
|
+
// A command is an own, enumerable, function-valued property — exactly what
|
|
260
|
+
// `Object.keys` shows, so dispatch, `statewire/commands`, and the reserved-
|
|
261
|
+
// namespace guard share one view; prototype-chain names are not commands.
|
|
262
|
+
const handlerOf = (
|
|
263
|
+
handlers: Record<string, (...args: never[]) => unknown>,
|
|
264
|
+
method: string,
|
|
265
|
+
): ((...args: never[]) => unknown) | undefined => {
|
|
266
|
+
if (!Object.prototype.propertyIsEnumerable.call(handlers, method)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
const handler = handlers[method];
|
|
270
|
+
return typeof handler === "function" ? handler : undefined;
|
|
271
|
+
};
|
|
272
|
+
|
|
237
273
|
const dispatch = (cell: CommandsCell, call: QueuedCall) => {
|
|
238
|
-
const handler = cell.handlers
|
|
274
|
+
const handler = handlerOf(cell.handlers!, call.method);
|
|
239
275
|
if (handler === undefined) {
|
|
240
276
|
const error = new StatewireSendError({
|
|
241
277
|
fate: "rejected",
|
|
@@ -252,17 +288,22 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
|
|
|
252
288
|
ts: call.ts,
|
|
253
289
|
seq: ++cell.seq,
|
|
254
290
|
applied: false,
|
|
291
|
+
controller: new AbortController(),
|
|
292
|
+
settled: false,
|
|
255
293
|
...(call.origin !== undefined && { origin: call.origin }),
|
|
256
294
|
...(call.origin?.clientId !== undefined && {
|
|
257
295
|
clientId: call.origin.clientId,
|
|
258
296
|
}),
|
|
259
297
|
};
|
|
298
|
+
cell.active.set(inv, call);
|
|
260
299
|
cell.listener?.onStart(inv);
|
|
261
300
|
cell.listener?.onPosted?.(inv);
|
|
262
|
-
cell.listener?.onCovered?.(inv);
|
|
263
301
|
const ctx: StatewireHost.Ctx = {
|
|
264
302
|
caller: call.origin?.caller ?? LOCAL_CALLER,
|
|
265
|
-
|
|
303
|
+
signal: inv.controller.signal,
|
|
304
|
+
applied: () => {
|
|
305
|
+
if (!inv.settled) applyInvocation(cell, inv);
|
|
306
|
+
},
|
|
266
307
|
};
|
|
267
308
|
try {
|
|
268
309
|
const value = (
|
|
@@ -332,6 +373,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
|
|
|
332
373
|
const cell: CommandsCell = {
|
|
333
374
|
handlers: null,
|
|
334
375
|
queue: [],
|
|
376
|
+
active: new Map(),
|
|
335
377
|
running: false,
|
|
336
378
|
listener: null,
|
|
337
379
|
seq: 0,
|
|
@@ -358,10 +400,17 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
|
|
|
358
400
|
}),
|
|
359
401
|
);
|
|
360
402
|
commandsHosts.set(proxy, {
|
|
403
|
+
abort: () => abortInvocations(cell),
|
|
361
404
|
setListener: (listener) => {
|
|
362
405
|
cell.listener = listener;
|
|
363
406
|
},
|
|
364
|
-
methods: () =>
|
|
407
|
+
methods: () => {
|
|
408
|
+
const handlers = cell.handlers;
|
|
409
|
+
if (!handlers) return [];
|
|
410
|
+
return Object.keys(handlers).filter(
|
|
411
|
+
(method) => handlerOf(handlers, method) !== undefined,
|
|
412
|
+
);
|
|
413
|
+
},
|
|
365
414
|
submit: (origin) =>
|
|
366
415
|
new Promise((resolve, reject) => {
|
|
367
416
|
if (cell.unmounted) {
|
|
@@ -394,6 +443,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
|
|
|
394
443
|
}
|
|
395
444
|
queueMicrotask(() => {
|
|
396
445
|
if (!cell.unmounted) return;
|
|
446
|
+
abortInvocations(cell);
|
|
397
447
|
const queued = cell.queue;
|
|
398
448
|
cell.queue = [];
|
|
399
449
|
for (const call of queued) {
|