statewire 0.8.1 → 0.9.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 +2 -2
- package/dist/client/connection.d.ts +23 -19
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +143 -103
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +30 -3
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +222 -260
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +34 -39
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +380 -373
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -0
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/client/timing.d.ts +1 -2
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +1 -5
- package/dist/client/timing.js.map +1 -1
- package/dist/host/draft.d.ts +2 -5
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +5 -8
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +23 -32
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +3 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +484 -424
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +4 -0
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +10 -10
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/statewire.d.ts +29 -24
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +8 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.d.ts +1 -1
- package/dist/transports/fake-sse.d.ts.map +1 -1
- package/dist/transports/fake-sse.js +5 -2
- package/dist/transports/fake-sse.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -2
- package/dist/transports/fixtures.d.ts.map +1 -1
- package/dist/transports/fixtures.js +5 -7
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts +2 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +10 -4
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +32 -46
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +56 -59
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +41 -14
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -3
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +25 -80
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -4
- package/dist/types.js.map +1 -1
- package/dist/wire/ops.d.ts +1 -5
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +1 -4
- package/dist/wire/ops.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +175 -128
- package/src/client/core.ts +287 -278
- package/src/client/session.ts +474 -480
- package/src/client/sessionStore.ts +1 -0
- package/src/client/timing.ts +0 -6
- package/src/host/draft.ts +1 -3
- package/src/host/host.ts +41 -58
- package/src/host/socket.ts +653 -448
- package/src/host-internal.ts +4 -0
- package/src/host.ts +13 -10
- package/src/index.ts +0 -1
- package/src/statewire.ts +38 -32
- package/src/transports/fake-sse.ts +2 -1
- package/src/transports/fixtures.ts +4 -9
- package/src/transports/remote.ts +12 -5
- package/src/transports/sse.ts +99 -115
- package/src/transports/testing.ts +47 -19
- package/src/transports/ws.ts +44 -115
- package/src/types.ts +15 -14
- package/src/wire/ops.ts +9 -16
package/src/host-internal.ts
CHANGED
|
@@ -22,6 +22,10 @@ export namespace StatewireHostInternal {
|
|
|
22
22
|
|
|
23
23
|
export type SocketHostOptions = {
|
|
24
24
|
heartbeatIntervalMs?: number;
|
|
25
|
+
/** LRU cap on stored client records; live or in-flight records are exempt. */
|
|
26
|
+
clientRecordLimit?: number;
|
|
27
|
+
/** Hang every attach up with `fin: idle` at the idle flag's rising edge. */
|
|
28
|
+
finOnIdle?: boolean;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export type Fin = StatewireHost.Fin;
|
package/src/host.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Statewire } from "./types";
|
|
2
|
-
import type { ServerOp } from "./host/draft";
|
|
3
2
|
|
|
4
3
|
export { StatewireReject } from "./host/engine";
|
|
5
4
|
export { append } from "./host/draft";
|
|
@@ -7,7 +6,6 @@ export {
|
|
|
7
6
|
getStateHost,
|
|
8
7
|
useStatewireState,
|
|
9
8
|
useStatewireCommands,
|
|
10
|
-
useStatewireEmit,
|
|
11
9
|
} from "./host/host";
|
|
12
10
|
|
|
13
11
|
export namespace StatewireHost {
|
|
@@ -20,20 +18,19 @@ export namespace StatewireHost {
|
|
|
20
18
|
|
|
21
19
|
export type StateHost = Statewire.StateSource & {
|
|
22
20
|
/** Synchronous, per locally-recorded op; the host owns batching. */
|
|
23
|
-
setOnOp: (listener: ((op:
|
|
21
|
+
setOnOp: (listener: ((op: Statewire.Op) => void) | null) => void;
|
|
24
22
|
};
|
|
25
23
|
|
|
26
|
-
export type StateSource = Statewire.StateSource;
|
|
27
|
-
|
|
28
24
|
/**
|
|
29
25
|
* Per-invocation handle bound as `this` of a command handler: `caller` is
|
|
30
|
-
* the submitting client, `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* the submitting client, `applied()` states the broadcast state now contains
|
|
27
|
+
* the command's effects — the client drops its optimistic overlay — before
|
|
28
|
+
* the handler settles (idempotent, a no-op after settle). The command
|
|
29
|
+
* settles when the handler's returned promise does.
|
|
33
30
|
*/
|
|
34
31
|
export type Ctx = {
|
|
35
32
|
readonly caller: Caller;
|
|
36
|
-
readonly
|
|
33
|
+
readonly applied: () => void;
|
|
37
34
|
};
|
|
38
35
|
|
|
39
36
|
/**
|
|
@@ -56,7 +53,13 @@ export namespace StatewireHost {
|
|
|
56
53
|
};
|
|
57
54
|
|
|
58
55
|
export type Fin = {
|
|
59
|
-
reason:
|
|
56
|
+
reason:
|
|
57
|
+
| "evicted"
|
|
58
|
+
| "error"
|
|
59
|
+
| "gone"
|
|
60
|
+
| "superseded"
|
|
61
|
+
| "idle"
|
|
62
|
+
| (string & {});
|
|
60
63
|
message?: string;
|
|
61
64
|
payload?: unknown;
|
|
62
65
|
meta?: unknown;
|
package/src/index.ts
CHANGED
|
@@ -3,4 +3,3 @@ export { StatewireClient, useStatewire, StatewireResource } from "./statewire";
|
|
|
3
3
|
export { StatewireSSE } from "./transports/sse";
|
|
4
4
|
export { StatewireWS } from "./transports/ws";
|
|
5
5
|
export { StatewireSendError } from "./types";
|
|
6
|
-
export { applyOptimistic } from "./util/recordMutations";
|
package/src/statewire.ts
CHANGED
|
@@ -13,9 +13,8 @@ import type { StatewireSessionStoreOptions } from "./client/sessionStore";
|
|
|
13
13
|
export const useStatewire = <
|
|
14
14
|
TState,
|
|
15
15
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
16
|
-
TEvent = any,
|
|
17
16
|
>(
|
|
18
|
-
options: StatewireClient.Options<TState, TCommands
|
|
17
|
+
options: StatewireClient.Options<TState, TCommands>,
|
|
19
18
|
): StatewireClient.Helpers<TState, TCommands> => {
|
|
20
19
|
const latest = useRef(options);
|
|
21
20
|
latest.current = options;
|
|
@@ -32,6 +31,7 @@ export const useStatewire = <
|
|
|
32
31
|
core.getState,
|
|
33
32
|
core.getState,
|
|
34
33
|
) as TState;
|
|
34
|
+
const isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);
|
|
35
35
|
const connection = value.connection ?? LIVE_CONNECTION;
|
|
36
36
|
|
|
37
37
|
return useMemo(
|
|
@@ -40,38 +40,37 @@ export const useStatewire = <
|
|
|
40
40
|
getState: core.getState as () => TState,
|
|
41
41
|
connection,
|
|
42
42
|
commands: core.commands as Statewire.CommandsProxy<TCommands>,
|
|
43
|
+
isBusy,
|
|
43
44
|
setContext: (contextValue: Record<string, unknown>) => {
|
|
44
45
|
if (!value.setContext) {
|
|
45
46
|
throw new Error(
|
|
46
47
|
"statewire: this transport has no client context channel",
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
value.setContext(contextValue);
|
|
50
51
|
},
|
|
51
52
|
}),
|
|
52
|
-
[state, connection, core, value],
|
|
53
|
+
[state, isBusy, connection, core, value],
|
|
53
54
|
);
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export const StatewireResource = <
|
|
57
58
|
TState,
|
|
58
59
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
59
|
-
TEvent = any,
|
|
60
60
|
>(
|
|
61
|
-
options: StatewireClient.Options<TState, TCommands
|
|
61
|
+
options: StatewireClient.Options<TState, TCommands>,
|
|
62
62
|
): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>
|
|
63
|
-
resource(useStatewire<TState, TCommands
|
|
63
|
+
resource(useStatewire<TState, TCommands>)(options);
|
|
64
64
|
|
|
65
65
|
export class StatewireClient<
|
|
66
66
|
TState,
|
|
67
67
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
68
|
-
TEvent = any,
|
|
69
68
|
> {
|
|
70
69
|
private readonly root;
|
|
71
70
|
private disposed = false;
|
|
72
71
|
|
|
73
|
-
constructor(options: StatewireClient.Options<TState, TCommands
|
|
74
|
-
const ClientRoot = () => useStatewire<TState, TCommands
|
|
72
|
+
constructor(options: StatewireClient.Options<TState, TCommands>) {
|
|
73
|
+
const ClientRoot = () => useStatewire<TState, TCommands>(options);
|
|
75
74
|
this.root = createTapRoot(ClientRoot);
|
|
76
75
|
}
|
|
77
76
|
|
|
@@ -87,8 +86,12 @@ export class StatewireClient<
|
|
|
87
86
|
return this.root.getValue().commands;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
return this.root.getValue().
|
|
89
|
+
get isBusy(): boolean {
|
|
90
|
+
return this.root.getValue().isBusy;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setContext(value: Record<string, unknown>): void {
|
|
94
|
+
this.root.getValue().setContext(value);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
subscribe(listener: () => void): () => void {
|
|
@@ -106,14 +109,12 @@ export namespace StatewireClient {
|
|
|
106
109
|
export type Options<
|
|
107
110
|
TState,
|
|
108
111
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
109
|
-
TEvent = any,
|
|
110
112
|
> = {
|
|
111
|
-
transport: ResourceElement<Statewire<TState, TCommands
|
|
113
|
+
transport: ResourceElement<Statewire<TState, TCommands>>;
|
|
112
114
|
/** Served as the canonical state until the attach snapshot; own commands fold over it. */
|
|
113
115
|
initialState?: TState;
|
|
114
116
|
optimistic?: Optimistic<NonNullable<TState>, TCommands>;
|
|
115
117
|
materialize?: Materialize<NonNullable<TState>>;
|
|
116
|
-
onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;
|
|
117
118
|
onStateChange?: (
|
|
118
119
|
state: NonNullable<TState>,
|
|
119
120
|
ctx: { changes: Statewire.Changes },
|
|
@@ -135,13 +136,16 @@ export namespace StatewireClient {
|
|
|
135
136
|
getState: () => TState;
|
|
136
137
|
connection: Connection;
|
|
137
138
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
139
|
+
/** Negation of the server's broadcast idle level (a transport without idle is always busy). */
|
|
140
|
+
isBusy: boolean;
|
|
138
141
|
/** Replace this client's ambient context on the server; throws on transports without a client context channel. */
|
|
139
|
-
setContext: (value: Record<string, unknown>) =>
|
|
142
|
+
setContext: (value: Record<string, unknown>) => void;
|
|
140
143
|
};
|
|
141
144
|
|
|
142
145
|
export type ConnectionState =
|
|
143
146
|
| { status: "connecting" }
|
|
144
147
|
| { status: "live" }
|
|
148
|
+
| { status: "idle" }
|
|
145
149
|
| {
|
|
146
150
|
status: "reconnecting";
|
|
147
151
|
cause: "dropped" | "evicted" | "error" | "unauthorized";
|
|
@@ -181,28 +185,26 @@ export namespace StatewireClient {
|
|
|
181
185
|
export type Materialize<TState> = (draft: TState) => void;
|
|
182
186
|
|
|
183
187
|
/**
|
|
184
|
-
* fate: "aborted" = never sent, "rejected" = the
|
|
185
|
-
* "unknown-command"
|
|
186
|
-
*
|
|
187
|
-
* "
|
|
188
|
-
* but the
|
|
188
|
+
* fate: "aborted" = never sent, "rejected" = the server refused it
|
|
189
|
+
* (`code` "unknown-command" | "invalid-params" for the static rejects,
|
|
190
|
+
* code-less for handler rejects), "lost" = sent but never covered by an
|
|
191
|
+
* applied, "failed" = the handler ran and threw, "crashed" = the handler
|
|
192
|
+
* died involuntarily, "expired" = concluded but the outcome is
|
|
193
|
+
* unavailable.
|
|
189
194
|
* cause is which channel settled it: "server" = the server's answer;
|
|
190
195
|
* the rest are channel deaths the client settled locally.
|
|
191
196
|
*/
|
|
192
197
|
export type SendFailure =
|
|
193
|
-
| { fate: "rejected"; cause: "server"; message: string; payload?: unknown }
|
|
194
|
-
| { fate: "unknown-command"; cause: "server" }
|
|
195
|
-
| { fate: "invalid-params"; cause: "server"; message: string }
|
|
196
|
-
| { fate: "crashed"; cause: "server" }
|
|
197
198
|
| {
|
|
198
|
-
fate: "
|
|
199
|
-
cause:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
| "session-lost"
|
|
204
|
-
| "superseded";
|
|
199
|
+
fate: "rejected";
|
|
200
|
+
cause: "server";
|
|
201
|
+
code?: "unknown-command" | "invalid-params";
|
|
202
|
+
message?: string;
|
|
203
|
+
payload?: unknown;
|
|
205
204
|
}
|
|
205
|
+
| { fate: "failed"; cause: "server"; message?: string }
|
|
206
|
+
| { fate: "crashed"; cause: "server" }
|
|
207
|
+
| { fate: "expired"; cause: "server" }
|
|
206
208
|
| {
|
|
207
209
|
fate: "aborted";
|
|
208
210
|
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
@@ -223,6 +225,8 @@ export namespace StatewireClient {
|
|
|
223
225
|
headers?: HeadersOption;
|
|
224
226
|
fetch?: typeof globalThis.fetch;
|
|
225
227
|
onChange?: (connection: Connection) => void;
|
|
228
|
+
/** The broadcast idle flag's rising edge: the instance went quiet. */
|
|
229
|
+
onDone?: () => void;
|
|
226
230
|
};
|
|
227
231
|
|
|
228
232
|
export type WSOptions = StatewireSessionStoreOptions & {
|
|
@@ -234,5 +238,7 @@ export namespace StatewireClient {
|
|
|
234
238
|
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
235
239
|
webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
236
240
|
onChange?: (connection: Connection) => void;
|
|
241
|
+
/** The broadcast idle flag's rising edge: the instance went quiet. */
|
|
242
|
+
onDone?: () => void;
|
|
237
243
|
};
|
|
238
244
|
}
|
|
@@ -14,6 +14,7 @@ export const openSSEStream = (
|
|
|
14
14
|
clientId: string,
|
|
15
15
|
onOpen: (handle: FakeStreamHandle) => void,
|
|
16
16
|
signal?: AbortSignal,
|
|
17
|
+
headers?: Record<string, string>,
|
|
17
18
|
): Response => {
|
|
18
19
|
const encoder = new TextEncoder();
|
|
19
20
|
let handle: FakeStreamHandle;
|
|
@@ -46,6 +47,6 @@ export const openSSEStream = (
|
|
|
46
47
|
});
|
|
47
48
|
return new Response(body, {
|
|
48
49
|
status: 200,
|
|
49
|
-
headers: { "content-type": "text/event-stream" },
|
|
50
|
+
headers: { "content-type": "text/event-stream", ...headers },
|
|
50
51
|
});
|
|
51
52
|
};
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { resource } from "@assistant-ui/tap";
|
|
2
|
-
import {
|
|
3
|
-
useStatewireCommands,
|
|
4
|
-
useStatewireEmit,
|
|
5
|
-
useStatewireState,
|
|
6
|
-
} from "../host";
|
|
2
|
+
import { useStatewireCommands, useStatewireState } from "../host";
|
|
7
3
|
import { tick } from "./testing";
|
|
8
4
|
|
|
9
5
|
/** Let several rounds of queued microtasks and op batches drain. */
|
|
@@ -15,21 +11,20 @@ export const settle = async () => {
|
|
|
15
11
|
export const createCounterElement = () =>
|
|
16
12
|
resource(() => {
|
|
17
13
|
const [state] = useStatewireState(() => ({ count: 0 }));
|
|
18
|
-
const emit = useStatewireEmit();
|
|
19
14
|
const commands = useStatewireCommands({
|
|
20
15
|
increment: () => {
|
|
21
16
|
state.count += 1;
|
|
22
17
|
},
|
|
23
18
|
});
|
|
24
|
-
return { state, commands
|
|
19
|
+
return { state, commands };
|
|
25
20
|
})();
|
|
26
21
|
|
|
27
|
-
/** Serialize a
|
|
22
|
+
/** Serialize a one-statement cmd-lane frame as a client would send it. */
|
|
28
23
|
export const commandFrame = (
|
|
29
24
|
method: string,
|
|
30
25
|
seq: number,
|
|
31
26
|
params: unknown[] = [],
|
|
32
|
-
) => JSON.stringify({ method, params, seq });
|
|
27
|
+
) => JSON.stringify({ cmd: [{ method, params, seq }] });
|
|
33
28
|
|
|
34
29
|
export type SocketRecorder = {
|
|
35
30
|
sent: string[];
|
package/src/transports/remote.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import { useStatewireEmit } from "../host/host";
|
|
3
2
|
import type { Statewire } from "../types";
|
|
4
3
|
import {
|
|
5
4
|
setContext,
|
|
@@ -14,22 +13,30 @@ export const useStatewireRemote = <
|
|
|
14
13
|
TOptions extends {
|
|
15
14
|
url: string;
|
|
16
15
|
onChange?: (connection: StatewireClient.Connection) => void;
|
|
16
|
+
onDone?: () => void;
|
|
17
17
|
} & StatewireSessionOptions,
|
|
18
18
|
>(
|
|
19
19
|
options: TOptions,
|
|
20
20
|
useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,
|
|
21
|
-
): Statewire<any, any
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
): Statewire<any, any> => {
|
|
22
|
+
const s = useStatewireSession({
|
|
23
|
+
sessionStore: options.sessionStore,
|
|
24
|
+
onDone: options.onDone,
|
|
25
|
+
});
|
|
24
26
|
const conn = useLink(options, s.ctx);
|
|
25
27
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
26
28
|
|
|
27
29
|
return {
|
|
28
30
|
state: s.canonical,
|
|
29
31
|
commands: s.commands,
|
|
30
|
-
emit,
|
|
31
32
|
connection: conn.connection,
|
|
32
33
|
source: s.source,
|
|
34
|
+
get durableSeq() {
|
|
35
|
+
return s.durableSeq;
|
|
36
|
+
},
|
|
37
|
+
get isBusy() {
|
|
38
|
+
return !s.idle;
|
|
39
|
+
},
|
|
33
40
|
setContext: (value) => setContext(s, value),
|
|
34
41
|
};
|
|
35
42
|
};
|