statewire 0.0.4 → 0.4.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 +3 -1
- package/dist/client/session.d.ts +7 -2
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +65 -33
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +2 -14
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +44 -92
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +1 -0
- package/dist/host/socket.js.map +1 -1
- package/dist/host.d.ts +16 -20
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/statewire.d.ts +14 -1
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/remote.d.ts +2 -2
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +4 -1
- package/dist/transports/remote.js.map +1 -1
- package/dist/util/statePath.js +8 -6
- package/dist/util/statePath.js.map +1 -1
- package/package.json +1 -1
- package/src/client/session.ts +63 -19
- package/src/host/host.ts +58 -113
- package/src/host/socket.ts +1 -0
- package/src/host.ts +14 -22
- package/src/statewire.ts +15 -6
- package/src/transports/remote.ts +9 -3
package/src/client/session.ts
CHANGED
|
@@ -147,6 +147,12 @@ const rejectedSend = (
|
|
|
147
147
|
|
|
148
148
|
type AttachState = "detached" | "attached" | "finished";
|
|
149
149
|
|
|
150
|
+
export type StatewireSessionOptions = StatewireSessionStoreOptions & {
|
|
151
|
+
onCommandSessionLost?:
|
|
152
|
+
| ((command: StatewireClient.SessionLostCommand) => "replay" | void)
|
|
153
|
+
| undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
150
156
|
type Session = {
|
|
151
157
|
record: StatewireSessionRecord;
|
|
152
158
|
ctx: StatewireRemoteProps;
|
|
@@ -166,13 +172,14 @@ type Session = {
|
|
|
166
172
|
transport: RemoteLink | null;
|
|
167
173
|
waiters: (() => void)[];
|
|
168
174
|
lifecycle: CommandsListener | null;
|
|
175
|
+
onCommandSessionLost: StatewireSessionOptions["onCommandSessionLost"];
|
|
169
176
|
notify: () => void;
|
|
170
177
|
emitEvent: (event: unknown) => void;
|
|
171
178
|
send: (command: StatewireWireCommand) => Promise<unknown>;
|
|
172
179
|
commands: Record<string, (...params: unknown[]) => Promise<unknown>>;
|
|
173
180
|
};
|
|
174
181
|
|
|
175
|
-
const createSession = (options:
|
|
182
|
+
const createSession = (options: StatewireSessionOptions): Session => {
|
|
176
183
|
const store = options.sessionStore ?? createMemorySessionStore();
|
|
177
184
|
const stored = store.load();
|
|
178
185
|
const record = stored ?? { clientId: newClientId(), nextSeq: 1 };
|
|
@@ -212,6 +219,7 @@ const createSession = (options: StatewireSessionStoreOptions): Session => {
|
|
|
212
219
|
transport: null,
|
|
213
220
|
waiters: [],
|
|
214
221
|
lifecycle: null,
|
|
222
|
+
onCommandSessionLost: options.onCommandSessionLost,
|
|
215
223
|
notify: () => {},
|
|
216
224
|
emitEvent: () => {},
|
|
217
225
|
send: (command) => sendCommand(s, command),
|
|
@@ -274,10 +282,12 @@ const settleAll = (
|
|
|
274
282
|
if (state.at === "awaiting-result")
|
|
275
283
|
return { fate: "result-unavailable", cause };
|
|
276
284
|
if (state.at === "queued") {
|
|
277
|
-
const
|
|
285
|
+
const predecessorLost = batch.some(
|
|
278
286
|
(p) => p.seq < entry.seq && p.state.at !== "queued",
|
|
279
287
|
);
|
|
280
|
-
return
|
|
288
|
+
return predecessorLost
|
|
289
|
+
? { fate: "aborted", cause, predecessorLost }
|
|
290
|
+
: { fate: "aborted", cause };
|
|
281
291
|
}
|
|
282
292
|
return { fate: "lost", cause };
|
|
283
293
|
};
|
|
@@ -288,6 +298,27 @@ const settleAll = (
|
|
|
288
298
|
}
|
|
289
299
|
};
|
|
290
300
|
|
|
301
|
+
// Offers every unsettled real command for replay, in seq order. Not caught:
|
|
302
|
+
// a throwing callback aborts the eviction before any entry settles; the
|
|
303
|
+
// attach retry evicts again and re-offers.
|
|
304
|
+
const claimReplays = (s: Session): Pending[] => {
|
|
305
|
+
const handler = s.onCommandSessionLost;
|
|
306
|
+
if (!handler) return [];
|
|
307
|
+
const claimed = new Set<Pending>();
|
|
308
|
+
for (const entry of s.pending) {
|
|
309
|
+
if (!entry.inv || isResolved(entry.state)) continue;
|
|
310
|
+
const decision = handler({
|
|
311
|
+
method: entry.inv.method,
|
|
312
|
+
params: [...entry.inv.params],
|
|
313
|
+
seq: entry.seq,
|
|
314
|
+
});
|
|
315
|
+
if (decision === "replay") claimed.add(entry);
|
|
316
|
+
}
|
|
317
|
+
if (claimed.size === 0) return [];
|
|
318
|
+
s.pending = s.pending.filter((p) => !claimed.has(p));
|
|
319
|
+
return [...claimed];
|
|
320
|
+
};
|
|
321
|
+
|
|
291
322
|
const evict = (
|
|
292
323
|
s: Session,
|
|
293
324
|
verdict?: {
|
|
@@ -295,6 +326,7 @@ const evict = (
|
|
|
295
326
|
failure: StatewireClient.SendFailure;
|
|
296
327
|
},
|
|
297
328
|
) => {
|
|
329
|
+
const replays = claimReplays(s);
|
|
298
330
|
settleAll(s, "evicted", verdict);
|
|
299
331
|
s.record = { clientId: newClientId(), nextSeq: 1 };
|
|
300
332
|
s.store.save(s.record);
|
|
@@ -302,6 +334,10 @@ const evict = (
|
|
|
302
334
|
// The server dropped this client's record and its stored context with it:
|
|
303
335
|
// the new session opens with a fresh full root replace.
|
|
304
336
|
if (s.context !== undefined) void s.send({ context: s.context });
|
|
337
|
+
for (const old of replays) {
|
|
338
|
+
if (old.inv) s.lifecycle?.onCovered?.(old.inv);
|
|
339
|
+
enqueue(s, old.command, old.resolve, old.reject);
|
|
340
|
+
}
|
|
305
341
|
publish(s);
|
|
306
342
|
wake(s);
|
|
307
343
|
};
|
|
@@ -645,16 +681,12 @@ const setTransport = (s: Session, transport: RemoteLink) => {
|
|
|
645
681
|
wake(s);
|
|
646
682
|
};
|
|
647
683
|
|
|
648
|
-
const
|
|
684
|
+
const enqueue = (
|
|
649
685
|
s: Session,
|
|
650
686
|
command: StatewireWireCommand,
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (isFinished(s)) {
|
|
656
|
-
return rejectedSend({ fate: "aborted", cause: "stream-gone" });
|
|
657
|
-
}
|
|
687
|
+
resolve: (payload: unknown) => void,
|
|
688
|
+
reject: (error: StatewireSendError) => void,
|
|
689
|
+
) => {
|
|
658
690
|
const seq = s.record.nextSeq;
|
|
659
691
|
s.record = { ...s.record, nextSeq: seq + 1 };
|
|
660
692
|
s.store.save(s.record);
|
|
@@ -672,17 +704,28 @@ const sendCommand = (
|
|
|
672
704
|
clientId: s.record.clientId,
|
|
673
705
|
},
|
|
674
706
|
state: { at: "queued", pendingMarked: false },
|
|
675
|
-
resolve
|
|
676
|
-
reject
|
|
707
|
+
resolve,
|
|
708
|
+
reject,
|
|
677
709
|
};
|
|
678
|
-
const result = new Promise<unknown>((resolve, reject) => {
|
|
679
|
-
entry.resolve = resolve;
|
|
680
|
-
entry.reject = reject;
|
|
681
|
-
});
|
|
682
|
-
result.catch(() => {});
|
|
683
710
|
s.pending.push(entry);
|
|
684
711
|
if (entry.inv) s.lifecycle?.onStart(entry.inv);
|
|
685
712
|
void pump(s);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const sendCommand = (
|
|
716
|
+
s: Session,
|
|
717
|
+
command: StatewireWireCommand,
|
|
718
|
+
): Promise<unknown> => {
|
|
719
|
+
if (!s.running) {
|
|
720
|
+
return rejectedSend({ fate: "aborted", cause: "unmount" });
|
|
721
|
+
}
|
|
722
|
+
if (isFinished(s)) {
|
|
723
|
+
return rejectedSend({ fate: "aborted", cause: "stream-gone" });
|
|
724
|
+
}
|
|
725
|
+
const result = new Promise<unknown>((resolve, reject) => {
|
|
726
|
+
enqueue(s, command, resolve, reject);
|
|
727
|
+
});
|
|
728
|
+
result.catch(() => {});
|
|
686
729
|
return result;
|
|
687
730
|
};
|
|
688
731
|
|
|
@@ -698,10 +741,11 @@ const setContext = (
|
|
|
698
741
|
};
|
|
699
742
|
|
|
700
743
|
export const useStatewireSession = (
|
|
701
|
-
options:
|
|
744
|
+
options: StatewireSessionOptions,
|
|
702
745
|
emit: (event: unknown) => void,
|
|
703
746
|
): Session => {
|
|
704
747
|
const [s] = useState(() => createSession(options));
|
|
748
|
+
s.onCommandSessionLost = options.onCommandSessionLost;
|
|
705
749
|
useEffect(() => {
|
|
706
750
|
s.emitEvent = emit;
|
|
707
751
|
return () => {
|
package/src/host/host.ts
CHANGED
|
@@ -102,82 +102,10 @@ export const useStatewireState = <TState>(
|
|
|
102
102
|
|
|
103
103
|
// ─── commands ───────────────────────────────────────────
|
|
104
104
|
|
|
105
|
-
type
|
|
105
|
+
type Settled =
|
|
106
106
|
| { type: "resolved"; value: unknown }
|
|
107
107
|
| { type: "rejected"; error: unknown };
|
|
108
108
|
|
|
109
|
-
type ExecutionBind = {
|
|
110
|
-
onAck: () => void;
|
|
111
|
-
onSettle: (settled: ExecutionSettled) => void;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
type ExecutionState = {
|
|
115
|
-
acked: boolean;
|
|
116
|
-
settled: ExecutionSettled | null;
|
|
117
|
-
bind: ExecutionBind | null;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const executionStates = new WeakMap<object, ExecutionState>();
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* `CommandExecution(executor)` runs the (possibly async) `executor({ ack })`
|
|
124
|
-
* immediately: its return value resolves the execution, a throw/rejection
|
|
125
|
-
* rejects it; `ack()` releases the client's ack watermark before the executor
|
|
126
|
-
* finishes. `CommandExecution.withResolvers()` returns `[execution,
|
|
127
|
-
* { ack, resolve, reject }]` instead — settle them from your own spawned work.
|
|
128
|
-
*/
|
|
129
|
-
export const CommandExecution = Object.assign(
|
|
130
|
-
<T = void>(
|
|
131
|
-
executor: (resolvers: {
|
|
132
|
-
ack: () => void;
|
|
133
|
-
}) => T | void | PromiseLike<T | void>,
|
|
134
|
-
): StatewireHost.Execution<T> => {
|
|
135
|
-
const state: ExecutionState = { acked: false, settled: null, bind: null };
|
|
136
|
-
const execution = Object.freeze(
|
|
137
|
-
{},
|
|
138
|
-
) as unknown as StatewireHost.Execution<T>;
|
|
139
|
-
executionStates.set(execution, state);
|
|
140
|
-
const settle = (settled: ExecutionSettled) => {
|
|
141
|
-
if (state.settled) return;
|
|
142
|
-
state.settled = settled;
|
|
143
|
-
state.acked = true;
|
|
144
|
-
state.bind?.onSettle(settled);
|
|
145
|
-
};
|
|
146
|
-
const ack = () => {
|
|
147
|
-
if (state.acked) return;
|
|
148
|
-
state.acked = true;
|
|
149
|
-
state.bind?.onAck();
|
|
150
|
-
};
|
|
151
|
-
void (async () => {
|
|
152
|
-
try {
|
|
153
|
-
settle({ type: "resolved", value: await executor({ ack }) });
|
|
154
|
-
} catch (error) {
|
|
155
|
-
settle({ type: "rejected", error });
|
|
156
|
-
}
|
|
157
|
-
})();
|
|
158
|
-
return execution;
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
withResolvers: <T = void>(): [
|
|
162
|
-
StatewireHost.Execution<T>,
|
|
163
|
-
StatewireHost.ExecutionResolvers<T>,
|
|
164
|
-
] => {
|
|
165
|
-
let resolve!: (value: T) => void;
|
|
166
|
-
let reject!: (error: unknown) => void;
|
|
167
|
-
const promise = new Promise<T>((res, rej) => {
|
|
168
|
-
resolve = res;
|
|
169
|
-
reject = rej;
|
|
170
|
-
});
|
|
171
|
-
let ack!: () => void;
|
|
172
|
-
const execution = CommandExecution<T>(({ ack: a }) => {
|
|
173
|
-
ack = a;
|
|
174
|
-
return promise;
|
|
175
|
-
});
|
|
176
|
-
return [execution, { ack: () => ack(), resolve, reject }];
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
|
|
181
109
|
export type CommandsInvocation = {
|
|
182
110
|
readonly method: string;
|
|
183
111
|
readonly params: readonly unknown[];
|
|
@@ -250,6 +178,9 @@ const STALL_WARN_MS = 5000;
|
|
|
250
178
|
|
|
251
179
|
// In-process dispatch has no wire client: never any ambient context, trivially attached.
|
|
252
180
|
const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
|
|
181
|
+
get clientId(): string {
|
|
182
|
+
throw new Error("statewire: in-process dispatch has no client id");
|
|
183
|
+
},
|
|
253
184
|
context: null,
|
|
254
185
|
isConnected: true,
|
|
255
186
|
});
|
|
@@ -276,7 +207,7 @@ const settleInvocation = (
|
|
|
276
207
|
cell: CommandsCell,
|
|
277
208
|
inv: ActiveInvocation,
|
|
278
209
|
call: QueuedCall,
|
|
279
|
-
settled:
|
|
210
|
+
settled: Settled,
|
|
280
211
|
) => {
|
|
281
212
|
ackInvocation(cell, inv);
|
|
282
213
|
if (settled.type === "resolved") {
|
|
@@ -296,14 +227,13 @@ const settleInvocation = (
|
|
|
296
227
|
}
|
|
297
228
|
};
|
|
298
229
|
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const handler = cell.handlers[call.method];
|
|
230
|
+
const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
231
|
+
typeof value === "object" &&
|
|
232
|
+
value !== null &&
|
|
233
|
+
typeof (value as { then?: unknown }).then === "function";
|
|
234
|
+
|
|
235
|
+
const dispatch = (cell: CommandsCell, call: QueuedCall) => {
|
|
236
|
+
const handler = cell.handlers![call.method];
|
|
307
237
|
if (handler === undefined) {
|
|
308
238
|
const error = new StatewireSendError({
|
|
309
239
|
fate: "unknown-command",
|
|
@@ -311,7 +241,6 @@ const pump = (cell: CommandsCell) => {
|
|
|
311
241
|
});
|
|
312
242
|
call.origin?.onSettle?.({ type: "rejected", error });
|
|
313
243
|
call.reject(error);
|
|
314
|
-
pump(cell);
|
|
315
244
|
return;
|
|
316
245
|
}
|
|
317
246
|
const inv: ActiveInvocation = {
|
|
@@ -325,39 +254,55 @@ const pump = (cell: CommandsCell) => {
|
|
|
325
254
|
clientId: call.origin.clientId,
|
|
326
255
|
}),
|
|
327
256
|
};
|
|
328
|
-
cell.running = true;
|
|
329
257
|
cell.listener?.onStart(inv);
|
|
330
258
|
cell.listener?.onPosted?.(inv);
|
|
331
259
|
cell.listener?.onCovered?.(inv);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
260
|
+
const ctx: StatewireHost.Ctx = {
|
|
261
|
+
caller: call.origin?.caller ?? LOCAL_CALLER,
|
|
262
|
+
ack: () => ackInvocation(cell, inv),
|
|
263
|
+
};
|
|
264
|
+
try {
|
|
265
|
+
const value = (
|
|
266
|
+
handler as (
|
|
267
|
+
this: StatewireHost.Ctx,
|
|
268
|
+
...args: readonly unknown[]
|
|
269
|
+
) => unknown
|
|
270
|
+
).call(ctx, ...call.params);
|
|
271
|
+
if (isThenable(value)) {
|
|
272
|
+
value.then(
|
|
273
|
+
(resolved) =>
|
|
274
|
+
settleInvocation(cell, inv, call, {
|
|
275
|
+
type: "resolved",
|
|
276
|
+
value: resolved,
|
|
277
|
+
}),
|
|
278
|
+
(error: unknown) =>
|
|
279
|
+
settleInvocation(cell, inv, call, { type: "rejected", error }),
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
settleInvocation(cell, inv, call, { type: "resolved", value });
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
settleInvocation(cell, inv, call, { type: "rejected", error });
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Prefix-only advancement: each handler runs synchronously to its first
|
|
290
|
+
// await, then the pump moves on; settlement rides the returned promise.
|
|
291
|
+
const pump = (cell: CommandsCell) => {
|
|
292
|
+
if (cell.running || !cell.handlers) {
|
|
293
|
+
watchStall(cell);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
cell.running = true;
|
|
297
|
+
try {
|
|
298
|
+
while (cell.handlers) {
|
|
299
|
+
const call = cell.queue.shift();
|
|
300
|
+
if (!call) break;
|
|
301
|
+
dispatch(cell, call);
|
|
359
302
|
}
|
|
360
|
-
}
|
|
303
|
+
} finally {
|
|
304
|
+
cell.running = false;
|
|
305
|
+
}
|
|
361
306
|
};
|
|
362
307
|
|
|
363
308
|
const watchStall = (cell: CommandsCell) => {
|
|
@@ -375,7 +320,7 @@ const watchStall = (cell: CommandsCell) => {
|
|
|
375
320
|
|
|
376
321
|
export const useStatewireCommands = <THandlers extends Statewire.Commands>(
|
|
377
322
|
commands:
|
|
378
|
-
| (THandlers & ThisType<StatewireHost.
|
|
323
|
+
| (THandlers & ThisType<StatewireHost.Ctx>)
|
|
379
324
|
| undefined
|
|
380
325
|
| null
|
|
381
326
|
| false,
|
package/src/host/socket.ts
CHANGED
package/src/host.ts
CHANGED
|
@@ -4,17 +4,12 @@ import type { ServerOp } from "./host/draft";
|
|
|
4
4
|
export { StatewireReject } from "./host/engine";
|
|
5
5
|
export { append } from "./host/draft";
|
|
6
6
|
export {
|
|
7
|
-
CommandExecution,
|
|
8
7
|
getStateHost,
|
|
9
8
|
useStatewireState,
|
|
10
9
|
useStatewireCommands,
|
|
11
10
|
useStatewireEmit,
|
|
12
11
|
} from "./host/host";
|
|
13
12
|
|
|
14
|
-
declare const executionResult: unique symbol;
|
|
15
|
-
|
|
16
|
-
type UnwrapExecution<V> = V extends StatewireHost.Execution<infer T> ? T : V;
|
|
17
|
-
|
|
18
13
|
export namespace StatewireHost {
|
|
19
14
|
export type StateCtl<TState = unknown> = {
|
|
20
15
|
/** Apply server-vocabulary ops (root replace = whole-state swap). */
|
|
@@ -31,28 +26,25 @@ export namespace StatewireHost {
|
|
|
31
26
|
export type StateSource = Statewire.StateSource;
|
|
32
27
|
|
|
33
28
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
29
|
+
* Per-invocation handle bound as `this` of a command handler: `caller` is
|
|
30
|
+
* the submitting client, `ack()` releases the client's optimistic overlay /
|
|
31
|
+
* ack watermark before the handler settles (idempotent, a no-op after
|
|
32
|
+
* settle). The command settles when the handler's returned promise does.
|
|
38
33
|
*/
|
|
39
|
-
export type
|
|
40
|
-
readonly
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export type ExecutionResolvers<T = void> = {
|
|
44
|
-
ack: () => void;
|
|
45
|
-
resolve: (value: T) => void;
|
|
46
|
-
reject: (error: unknown) => void;
|
|
34
|
+
export type Ctx = {
|
|
35
|
+
readonly caller: Caller;
|
|
36
|
+
readonly ack: () => void;
|
|
47
37
|
};
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
|
-
* The submitting client
|
|
51
|
-
* across a long run: `
|
|
52
|
-
*
|
|
53
|
-
* client
|
|
40
|
+
* The submitting client (`this.caller` in a command handler). Holdable
|
|
41
|
+
* across a long run: `clientId` is the client's `Statewire-Client-Id`
|
|
42
|
+
* (in-process dispatch has none — reading it throws), `context` reads the
|
|
43
|
+
* client's current ambient context live (null until the client sends one),
|
|
44
|
+
* `isConnected` reports whether the client has an active stream attachment.
|
|
54
45
|
*/
|
|
55
46
|
export type Caller = {
|
|
47
|
+
readonly clientId: string;
|
|
56
48
|
readonly context: Record<string, unknown> | null;
|
|
57
49
|
readonly isConnected: boolean;
|
|
58
50
|
};
|
|
@@ -60,7 +52,7 @@ export namespace StatewireHost {
|
|
|
60
52
|
export type HandledCommands<THandlers extends Statewire.Commands> = {
|
|
61
53
|
[K in keyof THandlers]: (
|
|
62
54
|
...args: Parameters<THandlers[K]>
|
|
63
|
-
) =>
|
|
55
|
+
) => Awaited<ReturnType<THandlers[K]>>;
|
|
64
56
|
};
|
|
65
57
|
|
|
66
58
|
export type Fin = {
|
package/src/statewire.ts
CHANGED
|
@@ -194,23 +194,31 @@ export namespace StatewireClient {
|
|
|
194
194
|
}
|
|
195
195
|
| {
|
|
196
196
|
fate: "aborted";
|
|
197
|
-
cause:
|
|
198
|
-
|
|
199
|
-
| "unmount"
|
|
200
|
-
| "stream-gone"
|
|
201
|
-
| "session-lost"
|
|
202
|
-
| "superseded";
|
|
197
|
+
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
198
|
+
predecessorLost?: true;
|
|
203
199
|
}
|
|
204
200
|
| {
|
|
205
201
|
fate: "lost";
|
|
206
202
|
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
207
203
|
};
|
|
208
204
|
|
|
205
|
+
/**
|
|
206
|
+
* A pending command offered for replay while the session is being evicted.
|
|
207
|
+
* Return "replay" to re-enqueue it under the rotated client id instead of
|
|
208
|
+
* settling it as a session-lost failure.
|
|
209
|
+
*/
|
|
210
|
+
export type SessionLostCommand = {
|
|
211
|
+
method: string;
|
|
212
|
+
params: unknown[];
|
|
213
|
+
seq: number;
|
|
214
|
+
};
|
|
215
|
+
|
|
209
216
|
export type SSEOptions = StatewireSessionStoreOptions & {
|
|
210
217
|
url: string;
|
|
211
218
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
212
219
|
fetch?: typeof globalThis.fetch;
|
|
213
220
|
onChange?: (connection: Connection) => void;
|
|
221
|
+
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
214
222
|
};
|
|
215
223
|
|
|
216
224
|
export type WSOptions = StatewireSessionStoreOptions & {
|
|
@@ -221,5 +229,6 @@ export namespace StatewireClient {
|
|
|
221
229
|
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
222
230
|
webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
223
231
|
onChange?: (connection: Connection) => void;
|
|
232
|
+
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
224
233
|
};
|
|
225
234
|
}
|
package/src/transports/remote.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
setTransport,
|
|
7
7
|
useStatewireSession,
|
|
8
8
|
} from "../client/session";
|
|
9
|
-
import type {
|
|
9
|
+
import type { StatewireSessionOptions } from "../client/session";
|
|
10
10
|
import type { StatewireClient } from "../statewire";
|
|
11
11
|
import type { RemoteLink, StatewireRemoteProps } from "./sse";
|
|
12
12
|
|
|
@@ -14,13 +14,19 @@ export const useStatewireRemote = <
|
|
|
14
14
|
TOptions extends {
|
|
15
15
|
url: string;
|
|
16
16
|
onChange?: (connection: StatewireClient.Connection) => void;
|
|
17
|
-
} &
|
|
17
|
+
} & StatewireSessionOptions,
|
|
18
18
|
>(
|
|
19
19
|
options: TOptions,
|
|
20
20
|
useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,
|
|
21
21
|
): Statewire<any, any, any> => {
|
|
22
22
|
const emit = useStatewireEmit<any>();
|
|
23
|
-
const s = useStatewireSession(
|
|
23
|
+
const s = useStatewireSession(
|
|
24
|
+
{
|
|
25
|
+
sessionStore: options.sessionStore,
|
|
26
|
+
onCommandSessionLost: options.onCommandSessionLost,
|
|
27
|
+
},
|
|
28
|
+
emit,
|
|
29
|
+
);
|
|
24
30
|
const conn = useLink(options, s.ctx);
|
|
25
31
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
26
32
|
|