statewire 0.0.1 → 0.0.2
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 +14 -13
- package/dist/client/changes.d.ts +2 -2
- package/dist/client/changes.d.ts.map +1 -1
- package/dist/client/changes.js +3 -10
- package/dist/client/changes.js.map +1 -1
- package/dist/client/connection.d.ts +18 -39
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +61 -54
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +7 -11
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +58 -55
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +8 -25
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +56 -36
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +5 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/adapter.d.ts +8 -0
- package/dist/host/adapter.d.ts.map +1 -0
- package/dist/host/adapter.js +23 -0
- package/dist/host/adapter.js.map +1 -0
- package/dist/host/draft.d.ts +21 -7
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js +190 -103
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +18 -35
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +81 -53
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +14 -0
- package/dist/host/socket.d.ts.map +1 -0
- package/dist/host/socket.js +459 -0
- package/dist/host/socket.js.map +1 -0
- package/dist/host-internal.d.ts +27 -0
- package/dist/host-internal.d.ts.map +1 -0
- package/dist/host-internal.js +3 -0
- package/dist/host.d.ts +44 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +5 -28
- package/dist/index.js +2 -3
- package/dist/statewire.d.ts +94 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +23 -35
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.d.ts +12 -0
- package/dist/transports/fake-sse.d.ts.map +1 -0
- package/dist/transports/fake-sse.js +39 -0
- package/dist/transports/fake-sse.js.map +1 -0
- package/dist/transports/fixtures.d.ts +33 -0
- package/dist/transports/fixtures.d.ts.map +1 -0
- package/dist/transports/fixtures.js +50 -0
- package/dist/transports/fixtures.js.map +1 -0
- package/dist/transports/remote.d.ts +6 -13
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +5 -24
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +10 -20
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +42 -41
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +4 -3
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -46
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +4 -13
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +57 -60
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +53 -100
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/draftTraps.d.ts +6 -0
- package/dist/util/draftTraps.d.ts.map +1 -0
- package/dist/util/draftTraps.js +25 -0
- package/dist/util/draftTraps.js.map +1 -0
- package/dist/util/macrotask.d.ts +5 -0
- package/dist/util/macrotask.d.ts.map +1 -0
- package/dist/util/macrotask.js +25 -0
- package/dist/util/macrotask.js.map +1 -0
- package/dist/util/methodProxy.d.ts +5 -0
- package/dist/util/methodProxy.d.ts.map +1 -0
- package/dist/util/methodProxy.js +17 -0
- package/dist/util/methodProxy.js.map +1 -0
- package/dist/util/randomId.d.ts +5 -0
- package/dist/util/randomId.d.ts.map +1 -0
- package/dist/util/randomId.js +6 -0
- package/dist/util/randomId.js.map +1 -0
- package/dist/{client/optimistic.d.ts → util/recordMutations.d.ts} +2 -2
- package/dist/util/recordMutations.d.ts.map +1 -0
- package/dist/{client/optimistic.js → util/recordMutations.js} +5 -24
- package/dist/util/recordMutations.js.map +1 -0
- package/dist/util/statePath.d.ts +14 -0
- package/dist/util/statePath.d.ts.map +1 -0
- package/dist/util/statePath.js +31 -0
- package/dist/util/statePath.js.map +1 -0
- package/dist/wire/meta.d.ts +6 -1
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +30 -28
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts +5 -26
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +67 -80
- package/dist/wire/ops.js.map +1 -1
- package/package.json +15 -11
- package/src/client/changes.ts +5 -15
- package/src/client/connection.ts +87 -113
- package/src/client/core.ts +57 -91
- package/src/client/session.ts +87 -70
- package/src/client/sessionStore.ts +1 -1
- package/src/client/timing.ts +6 -0
- package/src/host/adapter.ts +33 -0
- package/src/host/draft.ts +293 -125
- package/src/host/host.ts +107 -106
- package/src/host/socket.ts +542 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +61 -0
- package/src/index.ts +2 -62
- package/src/statewire.ts +154 -61
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +12 -54
- package/src/transports/sse.ts +68 -81
- package/src/transports/testing.ts +27 -59
- package/src/transports/ws.ts +76 -92
- package/src/types.ts +50 -131
- package/src/util/draftTraps.ts +32 -0
- package/src/util/macrotask.ts +25 -0
- package/src/util/methodProxy.ts +16 -0
- package/src/util/randomId.ts +4 -0
- package/src/{client/optimistic.ts → util/recordMutations.ts} +0 -0
- package/src/util/statePath.ts +61 -0
- package/src/wire/meta.ts +42 -47
- package/src/wire/ops.ts +107 -106
- package/LICENSE +0 -21
- package/dist/client/optimistic.d.ts.map +0 -1
- package/dist/client/optimistic.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server.d.ts +0 -15
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -3
- package/src/server.ts +0 -32
package/src/client/connection.ts
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { flushTapSync } from "@assistant-ui/tap";
|
|
3
3
|
import { reviveOps, reviveResponse, reviveValue } from "../wire/meta";
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
StatewireConnection,
|
|
9
|
-
StatewireTransportProps,
|
|
10
|
-
} from "../transports/sse";
|
|
4
|
+
import { retryDelay } from "./timing";
|
|
5
|
+
import type { ConnectionState, StatewireRemoteProps } from "../transports/sse";
|
|
6
|
+
import type { StatewireClient } from "../statewire";
|
|
7
|
+
import type { StatewireHost } from "../host";
|
|
11
8
|
|
|
12
9
|
export type Episode = {
|
|
13
|
-
cause: "dropped" | "
|
|
10
|
+
cause: "dropped" | "evicted" | "error";
|
|
14
11
|
message?: string;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
export type StatewireFinish = {
|
|
18
|
-
reason: "ended" | "error" | "gone" | "superseded" | (string & {});
|
|
19
|
-
message?: string;
|
|
20
|
-
payload?: unknown;
|
|
21
|
-
meta?: unknown;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
14
|
export type StatewireResponse = {
|
|
25
15
|
seq: number;
|
|
26
16
|
type: "accepted" | "rejected" | "pending" | "crashed" | "result-unavailable";
|
|
@@ -40,10 +30,10 @@ export type StatewireEnvelope = {
|
|
|
40
30
|
res?: StatewireResponse[];
|
|
41
31
|
ack?: number;
|
|
42
32
|
syn?: StatewireSyn;
|
|
43
|
-
fin?:
|
|
33
|
+
fin?: StatewireHost.Fin;
|
|
44
34
|
};
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
const createGate = () => {
|
|
47
37
|
let latched = false;
|
|
48
38
|
let wake: (() => void) | undefined;
|
|
49
39
|
return {
|
|
@@ -81,8 +71,8 @@ export const createGate = () => {
|
|
|
81
71
|
|
|
82
72
|
export type Gate = ReturnType<typeof createGate>;
|
|
83
73
|
|
|
84
|
-
export type
|
|
85
|
-
connection:
|
|
74
|
+
export type Conn = {
|
|
75
|
+
connection: StatewireClient.Connection;
|
|
86
76
|
hasSnapshot: boolean;
|
|
87
77
|
lease: string | null;
|
|
88
78
|
attempt: number;
|
|
@@ -90,37 +80,33 @@ export type ConnBase = {
|
|
|
90
80
|
gate: Gate;
|
|
91
81
|
abort: AbortController | null;
|
|
92
82
|
lifetime: AbortController | null;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
options: { onChange?: (connection: StatewireConnection) => void };
|
|
96
|
-
ctx: StatewireTransportProps;
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
notify: (connection: StatewireConnection) => void;
|
|
100
|
-
reconnect: () => void;
|
|
83
|
+
ctx: StatewireRemoteProps;
|
|
84
|
+
onChange: (connection: StatewireClient.Connection) => void;
|
|
101
85
|
attemptOnce: (
|
|
102
86
|
abort: AbortController,
|
|
103
87
|
) => Promise<"stop" | "reattach" | "ended">;
|
|
88
|
+
notify: (connection: StatewireClient.Connection) => void;
|
|
89
|
+
reconnect: () => void;
|
|
104
90
|
};
|
|
105
91
|
|
|
106
|
-
|
|
92
|
+
const setStatus = (c: Conn, next: ConnectionState) => {
|
|
107
93
|
const changed = next.status !== c.connection.status;
|
|
108
94
|
c.connection = { ...next, reconnect: c.reconnect };
|
|
109
95
|
c.notify(c.connection);
|
|
110
|
-
if (changed) c.
|
|
96
|
+
if (changed) c.onChange(c.connection);
|
|
111
97
|
};
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
c:
|
|
99
|
+
const deliverEnvelope = (
|
|
100
|
+
c: Conn,
|
|
115
101
|
env: Pick<StatewireEnvelope, "ops" | "res" | "ack" | "syn">,
|
|
116
|
-
attached: boolean,
|
|
117
102
|
) => {
|
|
118
103
|
if (env.syn?.lease !== undefined) c.lease = env.syn.lease;
|
|
119
|
-
const ctx = c.
|
|
104
|
+
const ctx = c.ctx;
|
|
120
105
|
const ops = env.ops === undefined ? [] : reviveOps(env.ops);
|
|
121
106
|
const res = env.res?.map(reviveResponse);
|
|
122
|
-
if (
|
|
123
|
-
ctx.onSnapshot({ ops, lastSeq: env.syn
|
|
107
|
+
if (env.syn !== undefined) {
|
|
108
|
+
ctx.onSnapshot({ ops, lastSeq: env.syn.lastSeq });
|
|
109
|
+
c.hasSnapshot = true;
|
|
124
110
|
} else if (
|
|
125
111
|
env.ops !== undefined ||
|
|
126
112
|
env.res !== undefined ||
|
|
@@ -132,15 +118,14 @@ export const deliverEnvelope = (
|
|
|
132
118
|
...(env.ack !== undefined && { ack: env.ack }),
|
|
133
119
|
});
|
|
134
120
|
}
|
|
135
|
-
c.hasSnapshot = true;
|
|
136
121
|
};
|
|
137
122
|
|
|
138
|
-
|
|
139
|
-
reason:
|
|
140
|
-
): reason is "
|
|
123
|
+
const isTransientFinish = (
|
|
124
|
+
reason: StatewireHost.Fin["reason"],
|
|
125
|
+
): reason is "evicted" | "error" => reason === "evicted" || reason === "error";
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
c:
|
|
127
|
+
const settleEnvelope = (
|
|
128
|
+
c: Conn,
|
|
144
129
|
env: StatewireEnvelope,
|
|
145
130
|
): "stop" | "reattach" | null => {
|
|
146
131
|
let finish = env.fin;
|
|
@@ -149,7 +134,7 @@ export const settleEnvelope = (
|
|
|
149
134
|
const { meta, ...rest } = finish;
|
|
150
135
|
finish = { ...rest, payload: reviveValue(rest.payload, meta) };
|
|
151
136
|
}
|
|
152
|
-
c.
|
|
137
|
+
c.ctx.onFinish(finish);
|
|
153
138
|
if (isTransientFinish(finish.reason)) {
|
|
154
139
|
c.episode = {
|
|
155
140
|
cause: finish.reason,
|
|
@@ -173,23 +158,22 @@ export const settleEnvelope = (
|
|
|
173
158
|
};
|
|
174
159
|
|
|
175
160
|
export const handleEnvelope = (
|
|
176
|
-
c:
|
|
161
|
+
c: Conn,
|
|
177
162
|
env: StatewireEnvelope,
|
|
178
|
-
attached: boolean,
|
|
179
163
|
): "stop" | "reattach" | null => {
|
|
180
|
-
deliverEnvelope(c, env
|
|
164
|
+
deliverEnvelope(c, env);
|
|
181
165
|
return settleEnvelope(c, env);
|
|
182
166
|
};
|
|
183
167
|
|
|
184
|
-
|
|
185
|
-
c:
|
|
168
|
+
const scheduleRetry = (
|
|
169
|
+
c: Conn,
|
|
186
170
|
failure: string | undefined,
|
|
187
171
|
immediate: boolean,
|
|
188
|
-
|
|
172
|
+
cause?: unknown,
|
|
189
173
|
) => {
|
|
190
174
|
c.attempt += 1;
|
|
191
175
|
c.episode ??= { cause: "dropped" };
|
|
192
|
-
const delay = immediate ? 0 : (
|
|
176
|
+
const delay = immediate ? 0 : retryDelay(cause, c.attempt);
|
|
193
177
|
if (c.hasSnapshot) {
|
|
194
178
|
setStatus(c, {
|
|
195
179
|
status: "reconnecting",
|
|
@@ -202,7 +186,7 @@ export const scheduleRetry = (
|
|
|
202
186
|
return delay;
|
|
203
187
|
};
|
|
204
188
|
|
|
205
|
-
|
|
189
|
+
const reconnectConn = (c: Conn) => {
|
|
206
190
|
if (!c.lifetime) return;
|
|
207
191
|
if (c.connection.status === "gone") {
|
|
208
192
|
c.attempt = 0;
|
|
@@ -216,13 +200,13 @@ export const reconnectConn = (c: ConnBase) => {
|
|
|
216
200
|
c.abort?.abort();
|
|
217
201
|
};
|
|
218
202
|
|
|
219
|
-
const run = async (c:
|
|
203
|
+
const run = async (c: Conn, lifetime: AbortController, gate: Gate) => {
|
|
220
204
|
const alive = () => c.lifetime === lifetime;
|
|
221
205
|
while (alive()) {
|
|
222
206
|
const abort = new AbortController();
|
|
223
207
|
c.abort = abort;
|
|
224
208
|
let failure: string | undefined;
|
|
225
|
-
let
|
|
209
|
+
let cause: unknown;
|
|
226
210
|
let gone = false;
|
|
227
211
|
try {
|
|
228
212
|
gone = (await c.attemptOnce(abort)) === "stop";
|
|
@@ -230,13 +214,7 @@ const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
|
|
|
230
214
|
if (!abort.signal.aborted) {
|
|
231
215
|
failure =
|
|
232
216
|
error instanceof Error && error.message ? error.message : "network";
|
|
233
|
-
|
|
234
|
-
error instanceof Error &&
|
|
235
|
-
"retryAfterMs" in error &&
|
|
236
|
-
typeof error.retryAfterMs === "number"
|
|
237
|
-
) {
|
|
238
|
-
retryAfterMs = error.retryAfterMs;
|
|
239
|
-
}
|
|
217
|
+
cause = error;
|
|
240
218
|
}
|
|
241
219
|
}
|
|
242
220
|
abort.abort();
|
|
@@ -247,69 +225,65 @@ const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
|
|
|
247
225
|
}
|
|
248
226
|
const immediate = gate.take();
|
|
249
227
|
const delay = flushTapSync(() =>
|
|
250
|
-
scheduleRetry(c, failure, immediate,
|
|
228
|
+
scheduleRetry(c, failure, immediate, cause),
|
|
251
229
|
);
|
|
252
230
|
if (delay === 0) continue;
|
|
253
231
|
await gate.wait(delay);
|
|
254
232
|
}
|
|
255
233
|
};
|
|
256
234
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
latest: {
|
|
287
|
-
readonly current: {
|
|
288
|
-
options: TOptions;
|
|
289
|
-
ctx: StatewireTransportProps;
|
|
290
|
-
};
|
|
291
|
-
},
|
|
292
|
-
notify: (connection: StatewireConnection) => void,
|
|
293
|
-
) => ConnBase & { send: RemoteLink["send"] },
|
|
294
|
-
): RemoteLink => {
|
|
295
|
-
const latest = useRef({ options, ctx });
|
|
296
|
-
const [connection, setConnection] = useState<StatewireConnection | null>(
|
|
297
|
-
null,
|
|
298
|
-
);
|
|
299
|
-
const [c] = useState(() => create(latest, setConnection));
|
|
300
|
-
|
|
301
|
-
useEffect(() => {
|
|
302
|
-
latest.current = { options, ctx };
|
|
235
|
+
export const useStatewireConn = (props: {
|
|
236
|
+
url: string;
|
|
237
|
+
onChange?: ((connection: StatewireClient.Connection) => void) | undefined;
|
|
238
|
+
ctx: StatewireRemoteProps;
|
|
239
|
+
attemptOnce: (
|
|
240
|
+
c: Conn,
|
|
241
|
+
abort: AbortController,
|
|
242
|
+
) => Promise<"stop" | "reattach" | "ended">;
|
|
243
|
+
}): Conn => {
|
|
244
|
+
const latest = useRef(props);
|
|
245
|
+
latest.current = props;
|
|
246
|
+
const [, setConnection] = useState<StatewireClient.Connection | null>(null);
|
|
247
|
+
const [c] = useState((): Conn => {
|
|
248
|
+
const conn: Conn = {
|
|
249
|
+
hasSnapshot: false,
|
|
250
|
+
lease: null,
|
|
251
|
+
attempt: 0,
|
|
252
|
+
episode: null,
|
|
253
|
+
abort: null,
|
|
254
|
+
gate: createGate(),
|
|
255
|
+
lifetime: null,
|
|
256
|
+
ctx: props.ctx,
|
|
257
|
+
onChange: (connection) => latest.current.onChange?.(connection),
|
|
258
|
+
attemptOnce: (abort) => latest.current.attemptOnce(conn, abort),
|
|
259
|
+
notify: setConnection,
|
|
260
|
+
reconnect: () => reconnectConn(conn),
|
|
261
|
+
connection: { status: "connecting", reconnect: () => conn.reconnect() },
|
|
262
|
+
};
|
|
263
|
+
return conn;
|
|
303
264
|
});
|
|
265
|
+
|
|
304
266
|
useEffect(() => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
267
|
+
c.hasSnapshot = false;
|
|
268
|
+
c.lease = null;
|
|
269
|
+
c.attempt = 0;
|
|
270
|
+
c.episode = null;
|
|
271
|
+
if (c.connection.status !== "connecting") {
|
|
272
|
+
setStatus(c, { status: "connecting" });
|
|
273
|
+
}
|
|
274
|
+
const lifetime = new AbortController();
|
|
275
|
+
c.lifetime = lifetime;
|
|
276
|
+
c.gate = createGate();
|
|
277
|
+
void run(c, lifetime, c.gate);
|
|
278
|
+
return () => {
|
|
279
|
+
c.abort?.abort();
|
|
280
|
+
lifetime.abort();
|
|
281
|
+
if (c.lifetime === lifetime) c.lifetime = null;
|
|
282
|
+
c.gate.open();
|
|
283
|
+
};
|
|
284
|
+
}, [c, props.url]);
|
|
308
285
|
|
|
309
|
-
return
|
|
310
|
-
connection: connection ?? c.connection,
|
|
311
|
-
send: c.send,
|
|
312
|
-
};
|
|
286
|
+
return c;
|
|
313
287
|
};
|
|
314
288
|
|
|
315
289
|
export const classify = (timeout: AbortSignal) =>
|
package/src/client/core.ts
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import { createChanges } from "./changes";
|
|
2
3
|
import { getCommandsHost, getEmitHost, getStateHost } from "../host/host";
|
|
3
4
|
import type { CommandsInvocation, CommandsListener } from "../host/host";
|
|
4
|
-
import { recordMutations } from "
|
|
5
|
+
import { recordMutations } from "../util/recordMutations";
|
|
6
|
+
import { createMacrotaskPoster } from "../util/macrotask";
|
|
7
|
+
import { createMethodProxy } from "../util/methodProxy";
|
|
5
8
|
import type { StatewirePath } from "../wire/ops";
|
|
6
9
|
import { StatewireSendError } from "../types";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
StatewireStateSource,
|
|
10
|
-
StatewireTransport,
|
|
11
|
-
StatewireCommand,
|
|
12
|
-
StatewireCommandsProxy,
|
|
13
|
-
StatewireCommandUpdate,
|
|
14
|
-
StatewireOptimisticContext,
|
|
15
|
-
StatewireOptions,
|
|
16
|
-
} from "../types";
|
|
17
|
-
import type { StatewireConnection } from "../transports/sse";
|
|
10
|
+
import type { Statewire } from "../types";
|
|
11
|
+
import type { StatewireClient } from "../statewire";
|
|
18
12
|
|
|
19
|
-
export const LIVE_CONNECTION:
|
|
13
|
+
export const LIVE_CONNECTION: StatewireClient.Connection = {
|
|
20
14
|
status: "live",
|
|
21
15
|
reconnect: () => {},
|
|
22
16
|
};
|
|
23
17
|
|
|
24
|
-
const failureOf = (error: unknown):
|
|
18
|
+
const failureOf = (error: unknown): StatewireClient.SendFailure =>
|
|
25
19
|
error instanceof StatewireSendError
|
|
26
20
|
? error.failure
|
|
27
21
|
: {
|
|
@@ -30,43 +24,15 @@ const failureOf = (error: unknown): StatewireSendFailure =>
|
|
|
30
24
|
message: error instanceof Error ? error.message : String(error),
|
|
31
25
|
};
|
|
32
26
|
|
|
33
|
-
// MessageChannel schedules the flush as a macrotask (tap's pattern): it runs
|
|
34
|
-
// after the whole microtask drain, so every producer in the current turn
|
|
35
|
-
// coalesces. The port is ref'd only while a flush is pending — an active
|
|
36
|
-
// MessagePort holds the Node event loop open. ref/unref are Node-only.
|
|
37
|
-
const createMacrotaskPoster = (callback: () => void): (() => void) => {
|
|
38
|
-
if (typeof MessageChannel !== "undefined") {
|
|
39
|
-
let port1: (MessagePort & { ref?: () => void; unref?: () => void }) | null =
|
|
40
|
-
null;
|
|
41
|
-
let port2: MessagePort;
|
|
42
|
-
return () => {
|
|
43
|
-
if (!port1) {
|
|
44
|
-
const channel = new MessageChannel();
|
|
45
|
-
channel.port1.onmessage = () => {
|
|
46
|
-
port1?.unref?.();
|
|
47
|
-
callback();
|
|
48
|
-
};
|
|
49
|
-
port1 = channel.port1;
|
|
50
|
-
port2 = channel.port2;
|
|
51
|
-
}
|
|
52
|
-
port1.ref?.();
|
|
53
|
-
port2!.postMessage(null);
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return () => setTimeout(callback, 0);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
27
|
type Overlay = { inv: CommandsInvocation; dropped: boolean };
|
|
60
28
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export const createStatewireCore = (
|
|
64
|
-
getOptions: () => StatewireOptions<any, any, any>,
|
|
29
|
+
const createCore = (
|
|
30
|
+
getOptions: () => StatewireClient.Options<any, any, any>,
|
|
65
31
|
) => {
|
|
66
|
-
let value:
|
|
67
|
-
let source:
|
|
32
|
+
let value: Statewire<unknown> | null = null;
|
|
33
|
+
let source: Statewire.StateSource | null = null;
|
|
68
34
|
let overlays: Overlay[] = [];
|
|
69
|
-
let updates:
|
|
35
|
+
let updates: Statewire.CommandUpdate[] = [];
|
|
70
36
|
let events: unknown[] = [];
|
|
71
37
|
let pendingChanged: StatewirePath[] = [];
|
|
72
38
|
let canonical: unknown;
|
|
@@ -74,11 +40,10 @@ export const createStatewireCore = (
|
|
|
74
40
|
let overlayPaths: readonly StatewirePath[] = [];
|
|
75
41
|
let notifiedPublished: unknown;
|
|
76
42
|
let notifiedOverlayPaths: readonly StatewirePath[] = [];
|
|
77
|
-
let notifiedConnection:
|
|
43
|
+
let notifiedConnection: StatewireClient.Connection | null = null;
|
|
78
44
|
let stale = false;
|
|
79
45
|
let overlaysDirty = false;
|
|
80
46
|
let flushing = false;
|
|
81
|
-
let disposed = false;
|
|
82
47
|
const listeners = new Set<() => void>();
|
|
83
48
|
|
|
84
49
|
const post = createMacrotaskPoster(() => {
|
|
@@ -87,7 +52,7 @@ export const createStatewireCore = (
|
|
|
87
52
|
});
|
|
88
53
|
let scheduled = false;
|
|
89
54
|
const schedule = () => {
|
|
90
|
-
if (scheduled
|
|
55
|
+
if (scheduled) return;
|
|
91
56
|
scheduled = true;
|
|
92
57
|
post();
|
|
93
58
|
};
|
|
@@ -100,7 +65,7 @@ export const createStatewireCore = (
|
|
|
100
65
|
const drainCanonical = () => {
|
|
101
66
|
if (source) {
|
|
102
67
|
pendingChanged.push(...source.takeChanged());
|
|
103
|
-
canonical = source.
|
|
68
|
+
canonical = source.snapshot();
|
|
104
69
|
} else if (value && value.state !== canonical) {
|
|
105
70
|
pendingChanged.push([]);
|
|
106
71
|
canonical = value.state;
|
|
@@ -121,7 +86,7 @@ export const createStatewireCore = (
|
|
|
121
86
|
for (const overlay of overlays) {
|
|
122
87
|
if (overlay.dropped) continue;
|
|
123
88
|
const { inv } = overlay;
|
|
124
|
-
const ctx:
|
|
89
|
+
const ctx: StatewireClient.OptimisticContext = {
|
|
125
90
|
ts: inv.ts,
|
|
126
91
|
seq: inv.seq,
|
|
127
92
|
...(inv.clientId !== undefined && { clientId: inv.clientId }),
|
|
@@ -130,7 +95,7 @@ export const createStatewireCore = (
|
|
|
130
95
|
const recorded = recordMutations(next, (draft) =>
|
|
131
96
|
optimistic(
|
|
132
97
|
draft as never,
|
|
133
|
-
{ method: inv.method, params: inv.params } as
|
|
98
|
+
{ method: inv.method, params: inv.params } as Statewire.Command,
|
|
134
99
|
ctx,
|
|
135
100
|
),
|
|
136
101
|
);
|
|
@@ -149,11 +114,11 @@ export const createStatewireCore = (
|
|
|
149
114
|
overlayPaths = paths;
|
|
150
115
|
};
|
|
151
116
|
|
|
152
|
-
const getConnection = ():
|
|
117
|
+
const getConnection = (): StatewireClient.Connection =>
|
|
153
118
|
value?.connection ?? LIVE_CONNECTION;
|
|
154
119
|
|
|
155
120
|
const flush = () => {
|
|
156
|
-
if (
|
|
121
|
+
if (!value) return;
|
|
157
122
|
if (flushing) {
|
|
158
123
|
schedule();
|
|
159
124
|
return;
|
|
@@ -184,7 +149,7 @@ export const createStatewireCore = (
|
|
|
184
149
|
options.onCommandChange?.(update, { state: published as never });
|
|
185
150
|
}
|
|
186
151
|
}
|
|
187
|
-
if (events.length > 0) {
|
|
152
|
+
if (events.length > 0 && published !== undefined) {
|
|
188
153
|
const batch = events;
|
|
189
154
|
events = [];
|
|
190
155
|
for (const event of batch) {
|
|
@@ -204,8 +169,8 @@ export const createStatewireCore = (
|
|
|
204
169
|
|
|
205
170
|
const note = (
|
|
206
171
|
inv: CommandsInvocation,
|
|
207
|
-
status:
|
|
208
|
-
extra?: { result?: unknown; failure?:
|
|
172
|
+
status: Statewire.CommandUpdate["status"],
|
|
173
|
+
extra?: { result?: unknown; failure?: StatewireClient.SendFailure },
|
|
209
174
|
) => {
|
|
210
175
|
if (!getOptions().onCommandChange) return;
|
|
211
176
|
updates.push({
|
|
@@ -251,8 +216,8 @@ export const createStatewireCore = (
|
|
|
251
216
|
schedule();
|
|
252
217
|
};
|
|
253
218
|
|
|
254
|
-
const attach = (next:
|
|
255
|
-
if (
|
|
219
|
+
const attach = (next: Statewire<unknown>) => {
|
|
220
|
+
if (next === value) return;
|
|
256
221
|
const prev = value;
|
|
257
222
|
value = next;
|
|
258
223
|
const nextSource =
|
|
@@ -294,48 +259,33 @@ export const createStatewireCore = (
|
|
|
294
259
|
}
|
|
295
260
|
};
|
|
296
261
|
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
fn = (...params: unknown[]) => {
|
|
304
|
-
if (!value) {
|
|
305
|
-
return Promise.reject(
|
|
306
|
-
new StatewireSendError({ fate: "aborted", cause: "unmount" }),
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const send = (
|
|
310
|
-
value.commands as Record<
|
|
311
|
-
string,
|
|
312
|
-
(...args: unknown[]) => Promise<unknown>
|
|
313
|
-
>
|
|
314
|
-
)[method]!;
|
|
315
|
-
const promise = send(...params);
|
|
316
|
-
flush();
|
|
317
|
-
return promise;
|
|
318
|
-
};
|
|
319
|
-
memo.set(method, fn);
|
|
262
|
+
const commands = createMethodProxy<Statewire.CommandsProxy<any>>(
|
|
263
|
+
(method, params) => {
|
|
264
|
+
if (!value) {
|
|
265
|
+
return Promise.reject(
|
|
266
|
+
new StatewireSendError({ fate: "aborted", cause: "unmount" }),
|
|
267
|
+
);
|
|
320
268
|
}
|
|
321
|
-
|
|
269
|
+
const send = (
|
|
270
|
+
value.commands as Record<
|
|
271
|
+
string,
|
|
272
|
+
(...args: unknown[]) => Promise<unknown>
|
|
273
|
+
>
|
|
274
|
+
)[method]!;
|
|
275
|
+
const promise = send(...params);
|
|
276
|
+
flush();
|
|
277
|
+
return promise;
|
|
322
278
|
},
|
|
323
|
-
|
|
279
|
+
);
|
|
324
280
|
|
|
325
281
|
return {
|
|
326
282
|
attach,
|
|
327
283
|
detach,
|
|
328
|
-
dispose: () => {
|
|
329
|
-
detach();
|
|
330
|
-
disposed = true;
|
|
331
|
-
},
|
|
332
284
|
getState: () => {
|
|
333
285
|
recompute();
|
|
334
286
|
return published;
|
|
335
287
|
},
|
|
336
|
-
getConnection,
|
|
337
288
|
commands,
|
|
338
|
-
flush,
|
|
339
289
|
subscribe: (listener: () => void) => {
|
|
340
290
|
listeners.add(listener);
|
|
341
291
|
return () => {
|
|
@@ -344,3 +294,19 @@ export const createStatewireCore = (
|
|
|
344
294
|
},
|
|
345
295
|
};
|
|
346
296
|
};
|
|
297
|
+
|
|
298
|
+
export const useStatewireCore = (
|
|
299
|
+
getOptions: () => StatewireClient.Options<any, any, any>,
|
|
300
|
+
value: Statewire<unknown>,
|
|
301
|
+
) => {
|
|
302
|
+
const [core] = useState(() => createCore(getOptions));
|
|
303
|
+
// Attach during render so the first snapshot reads the transport's state;
|
|
304
|
+
// re-attaching the same transport is a no-op and the single-slot host
|
|
305
|
+
// listeners make a swap land on the last writer.
|
|
306
|
+
core.attach(value);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
core.attach(value);
|
|
309
|
+
});
|
|
310
|
+
useEffect(() => () => core.detach(), [core]);
|
|
311
|
+
return core;
|
|
312
|
+
};
|