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/dist/client/session.js
CHANGED
|
@@ -2,78 +2,14 @@ import { applyOps } from "../wire/ops.js";
|
|
|
2
2
|
import { createMethodProxy } from "../util/methodProxy.js";
|
|
3
3
|
import { StatewireSendError } from "../types.js";
|
|
4
4
|
import { bindCommandsHost } from "../host/host.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { encodeParams, encodeValue, reviveResponse } from "../wire/meta.js";
|
|
6
|
+
import { backoff } from "./timing.js";
|
|
7
7
|
import { createMemorySessionStore } from "./sessionStore.js";
|
|
8
|
+
import { randomId } from "../util/randomId.js";
|
|
8
9
|
import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
9
10
|
//#region src/client/session.ts
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const inFlight = (state) => {
|
|
13
|
-
if (state.at === "queued" || state.at === "done") throw new Error(`statewire: ${state.at} entry observed after a send`);
|
|
14
|
-
return state;
|
|
15
|
-
};
|
|
16
|
-
const legalTransitions = {
|
|
17
|
-
queued: [
|
|
18
|
-
"queued",
|
|
19
|
-
"trying",
|
|
20
|
-
"awaiting-result",
|
|
21
|
-
"settled-live",
|
|
22
|
-
"done"
|
|
23
|
-
],
|
|
24
|
-
trying: [
|
|
25
|
-
"queued",
|
|
26
|
-
"trying",
|
|
27
|
-
"posted",
|
|
28
|
-
"awaiting-result",
|
|
29
|
-
"settled-live",
|
|
30
|
-
"done"
|
|
31
|
-
],
|
|
32
|
-
posted: [
|
|
33
|
-
"posted",
|
|
34
|
-
"trying",
|
|
35
|
-
"awaiting-result",
|
|
36
|
-
"settled-live",
|
|
37
|
-
"done"
|
|
38
|
-
],
|
|
39
|
-
"awaiting-result": ["awaiting-result", "done"],
|
|
40
|
-
"settled-live": ["settled-live", "done"],
|
|
41
|
-
done: []
|
|
42
|
-
};
|
|
43
|
-
const transition = (s, entry, next, outcome) => {
|
|
44
|
-
const prev = entry.state;
|
|
45
|
-
if (!legalTransitions[prev.at].includes(next.at)) throw new Error(`statewire: illegal pending transition ${prev.at} -> ${next.at}`);
|
|
46
|
-
const settles = !isResolved(prev) && isResolved(next);
|
|
47
|
-
if (settles && outcome === void 0) throw new Error(`statewire: settling transition ${prev.at} -> ${next.at} without an outcome`);
|
|
48
|
-
const covers = !isStateSettled(prev) && isStateSettled(next);
|
|
49
|
-
entry.state = next;
|
|
50
|
-
if (covers && entry.inv) s.lifecycle?.onCovered?.(entry.inv);
|
|
51
|
-
if (settles && outcome !== void 0) {
|
|
52
|
-
if (outcome.ok) {
|
|
53
|
-
entry.resolve(outcome.payload);
|
|
54
|
-
if (entry.inv) s.lifecycle?.onSettle(entry.inv, {
|
|
55
|
-
type: "accepted",
|
|
56
|
-
payload: outcome.payload
|
|
57
|
-
});
|
|
58
|
-
} else {
|
|
59
|
-
entry.reject(new StatewireSendError(outcome.failure));
|
|
60
|
-
if (entry.inv) s.lifecycle?.onSettle(entry.inv, {
|
|
61
|
-
type: "rejected",
|
|
62
|
-
error: new StatewireSendError(outcome.failure)
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (next.at === "done") s.pending = s.pending.filter((p) => p !== entry);
|
|
67
|
-
};
|
|
68
|
-
const carried = (state) => state.at === "queued" ? {
|
|
69
|
-
posted: false,
|
|
70
|
-
failedAttempts: 0
|
|
71
|
-
} : state.at === "trying" || state.at === "posted" ? {
|
|
72
|
-
posted: state.at === "posted",
|
|
73
|
-
failedAttempts: state.failedAttempts
|
|
74
|
-
} : {
|
|
75
|
-
posted: state.posted,
|
|
76
|
-
failedAttempts: state.failedAttempts
|
|
11
|
+
const protocolError = (message) => {
|
|
12
|
+
throw new Error(`statewire: protocol error: ${message}`);
|
|
77
13
|
};
|
|
78
14
|
const newClientId = () => `c-${randomId()}`;
|
|
79
15
|
const rejectedSend = (failure) => {
|
|
@@ -81,61 +17,6 @@ const rejectedSend = (failure) => {
|
|
|
81
17
|
result.catch(() => {});
|
|
82
18
|
return result;
|
|
83
19
|
};
|
|
84
|
-
const createSession = (options) => {
|
|
85
|
-
const store = options.sessionStore ?? createMemorySessionStore();
|
|
86
|
-
const stored = store.load();
|
|
87
|
-
const s = {
|
|
88
|
-
record: stored ?? {
|
|
89
|
-
clientId: newClientId(),
|
|
90
|
-
nextSeq: 1
|
|
91
|
-
},
|
|
92
|
-
ctx: {
|
|
93
|
-
get clientId() {
|
|
94
|
-
return s.record.clientId;
|
|
95
|
-
},
|
|
96
|
-
onSnapshot: (args) => handleSnapshot(s, args),
|
|
97
|
-
onOps: (args) => handleOps(s, args),
|
|
98
|
-
onFinish: (args) => handleFinish(s, args)
|
|
99
|
-
},
|
|
100
|
-
recordPersisted: stored !== null,
|
|
101
|
-
store,
|
|
102
|
-
lastAck: -1,
|
|
103
|
-
context: void 0,
|
|
104
|
-
pending: [],
|
|
105
|
-
canonical: void 0,
|
|
106
|
-
lastPublished: void 0,
|
|
107
|
-
changed: [],
|
|
108
|
-
source: {
|
|
109
|
-
snapshot: () => s.canonical,
|
|
110
|
-
takeChanged: () => {
|
|
111
|
-
const changed = s.changed;
|
|
112
|
-
s.changed = [];
|
|
113
|
-
return changed;
|
|
114
|
-
},
|
|
115
|
-
setOnChange: (listener) => {
|
|
116
|
-
s.notify = listener ?? (() => {});
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
attach: "detached",
|
|
120
|
-
attachEpoch: 0,
|
|
121
|
-
running: false,
|
|
122
|
-
pumping: false,
|
|
123
|
-
transport: null,
|
|
124
|
-
waiters: [],
|
|
125
|
-
lifecycle: null,
|
|
126
|
-
notify: () => {},
|
|
127
|
-
emitEvent: () => {},
|
|
128
|
-
send: (command) => sendCommand(s, command),
|
|
129
|
-
commands: createMethodProxy((method, params) => s.send({
|
|
130
|
-
method,
|
|
131
|
-
params
|
|
132
|
-
}))
|
|
133
|
-
};
|
|
134
|
-
bindCommandsHost(s.commands, { setListener: (listener) => {
|
|
135
|
-
s.lifecycle = listener;
|
|
136
|
-
} });
|
|
137
|
-
return s;
|
|
138
|
-
};
|
|
139
20
|
const publish = (s) => {
|
|
140
21
|
if (s.canonical === s.lastPublished) return;
|
|
141
22
|
s.lastPublished = s.canonical;
|
|
@@ -158,22 +39,48 @@ const sleepUntilWake = (s, delayMs) => new Promise((resolve) => {
|
|
|
158
39
|
}, delayMs);
|
|
159
40
|
s.waiters.push(waiter);
|
|
160
41
|
});
|
|
161
|
-
const
|
|
42
|
+
const setRecord = (s, record) => {
|
|
43
|
+
s.record = record;
|
|
44
|
+
if (!s.ephemeral) s.store.save(record);
|
|
45
|
+
};
|
|
46
|
+
const setIdle = (s, level) => {
|
|
47
|
+
if (level === s.idle) return;
|
|
48
|
+
s.idle = level;
|
|
49
|
+
s.notify();
|
|
50
|
+
if (level) s.onDone?.();
|
|
51
|
+
};
|
|
52
|
+
const cover = (s, entry) => {
|
|
53
|
+
if (entry.applied) return;
|
|
54
|
+
entry.applied = true;
|
|
55
|
+
s.lifecycle?.onApplied(entry.inv);
|
|
56
|
+
s.lifecycle?.onCovered?.(entry.inv);
|
|
57
|
+
};
|
|
58
|
+
const settleEntry = (s, entry, outcome) => {
|
|
59
|
+
s.pending = s.pending.filter((p) => p !== entry);
|
|
60
|
+
if (outcome.ok) {
|
|
61
|
+
entry.resolve(outcome.payload);
|
|
62
|
+
s.lifecycle?.onSettle(entry.inv, {
|
|
63
|
+
type: "accepted",
|
|
64
|
+
payload: outcome.payload
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
entry.reject(new StatewireSendError(outcome.failure));
|
|
68
|
+
s.lifecycle?.onSettle(entry.inv, {
|
|
69
|
+
type: "rejected",
|
|
70
|
+
error: new StatewireSendError(outcome.failure)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const settleAll = (s, detach) => {
|
|
162
75
|
const cause = {
|
|
163
|
-
evicted: "session-lost",
|
|
164
76
|
unmount: "unmount",
|
|
165
77
|
gone: "stream-gone",
|
|
166
78
|
superseded: "superseded"
|
|
167
79
|
}[detach];
|
|
168
80
|
const batch = s.pending;
|
|
169
81
|
const detachFailure = (entry) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
fate: "result-unavailable",
|
|
173
|
-
cause
|
|
174
|
-
};
|
|
175
|
-
if (state.at === "queued") {
|
|
176
|
-
const predecessorLost = batch.some((p) => p.seq < entry.seq && p.state.at !== "queued");
|
|
82
|
+
if (!entry.attempted) {
|
|
83
|
+
const predecessorLost = batch.some((p) => p.seq < entry.seq && p.attempted);
|
|
177
84
|
return predecessorLost ? {
|
|
178
85
|
fate: "aborted",
|
|
179
86
|
cause,
|
|
@@ -189,177 +96,198 @@ const settleAll = (s, detach, verdict) => {
|
|
|
189
96
|
};
|
|
190
97
|
};
|
|
191
98
|
for (const entry of [...batch]) {
|
|
192
|
-
|
|
193
|
-
|
|
99
|
+
s.lifecycle?.onCovered?.(entry.inv);
|
|
100
|
+
settleEntry(s, entry, {
|
|
194
101
|
ok: false,
|
|
195
|
-
failure
|
|
102
|
+
failure: detachFailure(entry)
|
|
196
103
|
});
|
|
197
104
|
}
|
|
198
105
|
};
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (const
|
|
216
|
-
|
|
217
|
-
|
|
106
|
+
const restateContext = (s) => {
|
|
107
|
+
s.ctxPending = [];
|
|
108
|
+
if (s.ctxOps === void 0) return;
|
|
109
|
+
const seq = s.record.ctxNextSeq;
|
|
110
|
+
setRecord(s, {
|
|
111
|
+
...s.record,
|
|
112
|
+
ctxNextSeq: seq + 1
|
|
113
|
+
});
|
|
114
|
+
s.ctxPending.push({
|
|
115
|
+
seq,
|
|
116
|
+
ops: s.ctxOps,
|
|
117
|
+
posted: false
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
const resetRecord = (s, clientId) => {
|
|
121
|
+
const survivors = [];
|
|
122
|
+
for (const entry of [...s.pending]) if (entry.attempted || entry.applied) {
|
|
123
|
+
s.lifecycle?.onCovered?.(entry.inv);
|
|
124
|
+
settleEntry(s, entry, {
|
|
125
|
+
ok: false,
|
|
126
|
+
failure: {
|
|
127
|
+
fate: "lost",
|
|
128
|
+
cause: "session-lost"
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
} else survivors.push(entry);
|
|
132
|
+
s.pending = [];
|
|
133
|
+
s.durableSeq = 0;
|
|
134
|
+
setRecord(s, {
|
|
135
|
+
clientId,
|
|
136
|
+
nextSeq: 1,
|
|
137
|
+
ctxNextSeq: 1
|
|
138
|
+
});
|
|
139
|
+
for (const old of survivors) {
|
|
140
|
+
s.lifecycle?.onCovered?.(old.inv);
|
|
141
|
+
enqueue(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
218
142
|
}
|
|
143
|
+
restateContext(s);
|
|
219
144
|
publish(s);
|
|
220
145
|
wake(s);
|
|
221
146
|
};
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
147
|
+
const ephemeralDetach = (s) => {
|
|
148
|
+
s.ephemeral = false;
|
|
149
|
+
s.sessionKnown = false;
|
|
150
|
+
resetRecord(s, newClientId());
|
|
227
151
|
};
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
else transition(s, entry, {
|
|
238
|
-
at: "awaiting-result",
|
|
239
|
-
...carried(state)
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
const protocolError = (message) => {
|
|
244
|
-
throw new Error(`statewire: protocol error: ${message}`);
|
|
152
|
+
const rejectedFailure = (cmd) => {
|
|
153
|
+
const code = cmd.code === void 0 || cmd.code === "unknown-command" || cmd.code === "invalid-params" ? cmd.code : protocolError(`unknown rejected code: ${cmd.code}`);
|
|
154
|
+
return {
|
|
155
|
+
fate: "rejected",
|
|
156
|
+
cause: "server",
|
|
157
|
+
...code !== void 0 && { code },
|
|
158
|
+
...cmd.message !== void 0 && { message: cmd.message },
|
|
159
|
+
...cmd.payload !== void 0 && { payload: cmd.payload }
|
|
160
|
+
};
|
|
245
161
|
};
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
break;
|
|
258
|
-
case "accepted":
|
|
259
|
-
if (state.at === "settled-live") break;
|
|
260
|
-
transition(s, entry, state.at === "awaiting-result" ? { at: "done" } : {
|
|
261
|
-
at: "settled-live",
|
|
262
|
-
...carried(state)
|
|
263
|
-
}, {
|
|
264
|
-
ok: true,
|
|
265
|
-
payload: response.payload
|
|
266
|
-
});
|
|
267
|
-
break;
|
|
268
|
-
case "rejected":
|
|
269
|
-
transition(s, entry, { at: "done" }, {
|
|
270
|
-
ok: false,
|
|
271
|
-
failure: response.code === "unknown-command" ? {
|
|
272
|
-
fate: "unknown-command",
|
|
273
|
-
cause: "server"
|
|
274
|
-
} : response.code === "invalid-params" ? {
|
|
275
|
-
fate: "invalid-params",
|
|
276
|
-
cause: "server",
|
|
277
|
-
message: response.message ?? protocolError("rejected response missing message")
|
|
278
|
-
} : {
|
|
279
|
-
fate: "rejected",
|
|
280
|
-
cause: "server",
|
|
281
|
-
message: response.message ?? protocolError("rejected response missing message"),
|
|
282
|
-
...response.payload !== void 0 && { payload: response.payload }
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
break;
|
|
286
|
-
case "crashed":
|
|
287
|
-
case "result-unavailable":
|
|
288
|
-
if (state.at === "settled-live") break;
|
|
289
|
-
transition(s, entry, state.at === "awaiting-result" ? { at: "done" } : {
|
|
290
|
-
at: "settled-live",
|
|
291
|
-
...carried(state)
|
|
292
|
-
}, {
|
|
293
|
-
ok: false,
|
|
294
|
-
failure: {
|
|
295
|
-
fate: response.type,
|
|
296
|
-
cause: "server"
|
|
297
|
-
}
|
|
298
|
-
});
|
|
162
|
+
const handleCmd = (s, cmd) => {
|
|
163
|
+
if (typeof cmd.seq !== "number") protocolError("cmd entry missing seq");
|
|
164
|
+
if (cmd.dur === true) {
|
|
165
|
+
s.durableSeq = Math.max(s.durableSeq, cmd.seq);
|
|
166
|
+
for (const entry of s.pending) if (entry.seq <= s.durableSeq) entry.posted = true;
|
|
167
|
+
}
|
|
168
|
+
if (cmd.type === void 0) return;
|
|
169
|
+
const entry = s.pending.find((p) => p.seq === cmd.seq);
|
|
170
|
+
if (cmd.type === "applied") {
|
|
171
|
+
if (entry) cover(s, entry);
|
|
172
|
+
return;
|
|
299
173
|
}
|
|
174
|
+
if (!entry) return;
|
|
175
|
+
cover(s, entry);
|
|
176
|
+
const outcome = cmd.type === "result" ? {
|
|
177
|
+
ok: true,
|
|
178
|
+
payload: reviveResponse(cmd).payload
|
|
179
|
+
} : cmd.type === "rejected" ? {
|
|
180
|
+
ok: false,
|
|
181
|
+
failure: rejectedFailure(reviveResponse(cmd))
|
|
182
|
+
} : cmd.type === "failed" ? {
|
|
183
|
+
ok: false,
|
|
184
|
+
failure: {
|
|
185
|
+
fate: "failed",
|
|
186
|
+
cause: "server",
|
|
187
|
+
...cmd.message !== void 0 && { message: cmd.message }
|
|
188
|
+
}
|
|
189
|
+
} : cmd.type === "crashed" ? {
|
|
190
|
+
ok: false,
|
|
191
|
+
failure: {
|
|
192
|
+
fate: "crashed",
|
|
193
|
+
cause: "server"
|
|
194
|
+
}
|
|
195
|
+
} : cmd.type === "expired" ? {
|
|
196
|
+
ok: false,
|
|
197
|
+
failure: {
|
|
198
|
+
fate: "expired",
|
|
199
|
+
cause: "server"
|
|
200
|
+
}
|
|
201
|
+
} : protocolError(`unknown cmd type: ${String(cmd.type)}`);
|
|
202
|
+
settleEntry(s, entry, outcome);
|
|
203
|
+
};
|
|
204
|
+
const handleCmds = (s, cmds) => {
|
|
205
|
+
for (const cmd of cmds) handleCmd(s, cmd);
|
|
206
|
+
wake(s);
|
|
300
207
|
};
|
|
301
208
|
const ingestOps = (s, ops) => {
|
|
302
209
|
const applied = applyOps(s.canonical, ops);
|
|
303
210
|
s.canonical = applied.state;
|
|
304
211
|
s.changed.push(...applied.changedPaths);
|
|
305
|
-
return applied.events;
|
|
306
212
|
};
|
|
307
|
-
const publishIngested = (s
|
|
213
|
+
const publishIngested = (s) => {
|
|
308
214
|
publish(s);
|
|
309
|
-
for (const event of events) s.emitEvent(event);
|
|
310
215
|
wake(s);
|
|
311
216
|
};
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
217
|
+
const RECORDS = [
|
|
218
|
+
"fresh",
|
|
219
|
+
"resumed",
|
|
220
|
+
"ephemeral"
|
|
221
|
+
];
|
|
222
|
+
const snapClocks = (s, seqWm, ctxWm) => {
|
|
223
|
+
const tail = s.pending.filter((p) => p.seq > seqWm);
|
|
224
|
+
if (!(tail.every((p, i) => p.seq === seqWm + 1 + i) && s.record.nextSeq === seqWm + tail.length + 1)) {
|
|
225
|
+
s.pending = s.pending.filter((p) => p.seq <= seqWm);
|
|
226
|
+
setRecord(s, {
|
|
227
|
+
...s.record,
|
|
228
|
+
nextSeq: seqWm + 1
|
|
229
|
+
});
|
|
230
|
+
for (const old of tail) {
|
|
231
|
+
s.lifecycle?.onCovered?.(old.inv);
|
|
232
|
+
enqueue(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!(s.ctxPending.every((c, i) => c.seq === ctxWm + 1 + i) && s.record.ctxNextSeq === ctxWm + s.ctxPending.length + 1)) {
|
|
236
|
+
setRecord(s, {
|
|
237
|
+
...s.record,
|
|
238
|
+
ctxNextSeq: ctxWm + 1
|
|
239
|
+
});
|
|
240
|
+
restateContext(s);
|
|
318
241
|
}
|
|
319
|
-
|
|
242
|
+
};
|
|
243
|
+
const handleHello = (s, { ops, syn }) => {
|
|
244
|
+
const record = syn.record ?? "fresh";
|
|
245
|
+
if (!RECORDS.includes(record)) protocolError(`unknown record: ${String(record)}`);
|
|
246
|
+
ingestOps(s, ops);
|
|
320
247
|
s.attach = "attached";
|
|
321
248
|
s.attachEpoch += 1;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
s.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
failedAttempts: state.failedAttempts
|
|
338
|
-
});
|
|
339
|
-
pump(s);
|
|
340
|
-
} else if (state.at === "settled-live" && state.posted) {
|
|
341
|
-
transition(s, entry, {
|
|
342
|
-
...state,
|
|
343
|
-
posted: false
|
|
344
|
-
});
|
|
345
|
-
pump(s);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
publishIngested(s, events);
|
|
249
|
+
s.frameFailures = 0;
|
|
250
|
+
s.resumeAt = 0;
|
|
251
|
+
s.ephemeral = record === "ephemeral";
|
|
252
|
+
const [attachCmd, attachCtx] = s.attachLaneSeqs;
|
|
253
|
+
s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
|
|
254
|
+
if (record === "resumed") {
|
|
255
|
+
for (const entry of s.pending) entry.posted = attachCmd.has(entry.seq);
|
|
256
|
+
const ctxWatermark = syn.ctxSeq ?? 0;
|
|
257
|
+
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
|
|
258
|
+
for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
|
|
259
|
+
snapClocks(s, syn.seq ?? 0, ctxWatermark);
|
|
260
|
+
} else resetRecord(s, s.record.clientId);
|
|
261
|
+
s.sessionKnown = true;
|
|
262
|
+
publishIngested(s);
|
|
263
|
+
pump(s);
|
|
349
264
|
};
|
|
350
|
-
const handleOps = (s, { ops,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
265
|
+
const handleOps = (s, { ops, cmds }) => {
|
|
266
|
+
ingestOps(s, ops);
|
|
267
|
+
for (const cmd of cmds) handleCmd(s, cmd);
|
|
268
|
+
publishIngested(s);
|
|
269
|
+
};
|
|
270
|
+
const handleRefusal = (s, syn) => {
|
|
271
|
+
const { seq, ctxSeq } = syn;
|
|
272
|
+
if (typeof seq === "number") {
|
|
273
|
+
const watermark = Math.max(seq, s.durableSeq);
|
|
274
|
+
for (const entry of s.pending) entry.posted = entry.seq <= watermark;
|
|
354
275
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (ack !== void 0) {
|
|
359
|
-
spliceAcked(s, ack, true);
|
|
360
|
-
s.lastAck = Math.max(s.lastAck, ack);
|
|
276
|
+
if (typeof ctxSeq === "number") {
|
|
277
|
+
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
|
|
278
|
+
for (const c of s.ctxPending) c.posted = false;
|
|
361
279
|
}
|
|
362
|
-
|
|
280
|
+
if (typeof syn.retry === "number") s.resumeAt = Date.now() + Math.max(0, syn.retry);
|
|
281
|
+
const firstCmd = s.pending.find((p) => !p.posted);
|
|
282
|
+
const firstCtx = s.ctxPending.find((c) => !c.posted);
|
|
283
|
+
if (typeof syn.seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(syn.seq, s.durableSeq) + 1 || typeof syn.ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > syn.ctxSeq + 1) s.transport?.connection.reconnect();
|
|
284
|
+
wake(s);
|
|
285
|
+
pump(s);
|
|
286
|
+
};
|
|
287
|
+
const handleDetach = (s) => {
|
|
288
|
+
if (s.attach === "attached") s.attach = "detached";
|
|
289
|
+
if (s.ephemeral) ephemeralDetach(s);
|
|
290
|
+
wake(s);
|
|
363
291
|
};
|
|
364
292
|
const handleFinish = (s, { reason }) => {
|
|
365
293
|
if (reason === "evicted" || reason === "error") return;
|
|
@@ -369,100 +297,98 @@ const handleFinish = (s, { reason }) => {
|
|
|
369
297
|
wake(s);
|
|
370
298
|
};
|
|
371
299
|
const isFinished = (s) => s.attach === "finished";
|
|
372
|
-
const
|
|
300
|
+
const buildLanes = (s) => {
|
|
301
|
+
const ctxEntries = [];
|
|
302
|
+
for (const entry of s.ctxPending) {
|
|
303
|
+
if (entry.posted) continue;
|
|
304
|
+
if (ctxEntries.length > 0 && entry.seq !== ctxEntries.at(-1).seq + 1) break;
|
|
305
|
+
ctxEntries.push(entry);
|
|
306
|
+
}
|
|
307
|
+
const cmdEntries = [];
|
|
308
|
+
for (const entry of s.pending) {
|
|
309
|
+
if (entry.posted) continue;
|
|
310
|
+
if (cmdEntries.length > 0 && entry.seq !== cmdEntries.at(-1).seq + 1) break;
|
|
311
|
+
cmdEntries.push(entry);
|
|
312
|
+
}
|
|
313
|
+
if (ctxEntries.length === 0 && cmdEntries.length === 0) return null;
|
|
314
|
+
return {
|
|
315
|
+
frame: {
|
|
316
|
+
...ctxEntries.length > 0 && { ctx: ctxEntries.map((c) => ({
|
|
317
|
+
ops: c.ops,
|
|
318
|
+
seq: c.seq
|
|
319
|
+
})) },
|
|
320
|
+
...cmdEntries.length > 0 && { cmd: cmdEntries.map((p) => ({
|
|
321
|
+
...p.command,
|
|
322
|
+
seq: p.seq
|
|
323
|
+
})) }
|
|
324
|
+
},
|
|
325
|
+
cmdEntries,
|
|
326
|
+
ctxEntries
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
const takeAttachLanes = (s) => {
|
|
330
|
+
s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
|
|
331
|
+
const wanted = s.lanesOnAttach;
|
|
332
|
+
s.lanesOnAttach = false;
|
|
333
|
+
if (!wanted || !s.sessionKnown) return null;
|
|
334
|
+
const lanes = buildLanes(s);
|
|
335
|
+
if (!lanes) return null;
|
|
336
|
+
s.attachLaneSeqs = [new Set(lanes.cmdEntries.map((p) => p.seq)), new Set(lanes.ctxEntries.map((c) => c.seq))];
|
|
337
|
+
for (const entry of lanes.cmdEntries) entry.attempted = true;
|
|
338
|
+
return lanes.frame;
|
|
339
|
+
};
|
|
373
340
|
const pump = async (s) => {
|
|
374
341
|
if (s.pumping) return;
|
|
375
342
|
s.pumping = true;
|
|
376
343
|
try {
|
|
377
344
|
while (s.running && !isFinished(s)) {
|
|
378
|
-
const entry = s.pending.find(sendable);
|
|
379
|
-
if (!entry) return;
|
|
380
345
|
const transport = s.transport;
|
|
346
|
+
if (buildLanes(s) === null) return;
|
|
381
347
|
if (s.attach !== "attached" || !transport) {
|
|
348
|
+
if (transport?.connection.status === "idle") transport.connection.reconnect();
|
|
382
349
|
await waitForWake(s);
|
|
383
350
|
continue;
|
|
384
351
|
}
|
|
385
|
-
const
|
|
352
|
+
const resumeDelay = s.resumeAt - Date.now();
|
|
353
|
+
if (resumeDelay > 0) {
|
|
354
|
+
await sleepUntilWake(s, resumeDelay);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const lanes = buildLanes(s);
|
|
358
|
+
if (!lanes) continue;
|
|
386
359
|
const epoch = s.attachEpoch;
|
|
387
|
-
|
|
388
|
-
at: "trying",
|
|
389
|
-
pendingMarked: entry.state.pendingMarked,
|
|
390
|
-
failedAttempts: 0
|
|
391
|
-
});
|
|
360
|
+
for (const entry of lanes.cmdEntries) entry.attempted = true;
|
|
392
361
|
let result;
|
|
393
362
|
try {
|
|
394
|
-
result = await transport.send(
|
|
395
|
-
|
|
396
|
-
command: entry.command
|
|
397
|
-
});
|
|
398
|
-
} catch (error) {
|
|
363
|
+
result = await transport.send(lanes.frame);
|
|
364
|
+
} catch {
|
|
399
365
|
if (!s.running || isFinished(s)) return;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
transition(s, entry, {
|
|
403
|
-
...state,
|
|
404
|
-
failedAttempts: state.failedAttempts + 1
|
|
405
|
-
});
|
|
406
|
-
await sleepUntilWake(s, retryDelay(error, state.failedAttempts + 1));
|
|
366
|
+
s.frameFailures += 1;
|
|
367
|
+
await sleepUntilWake(s, backoff(s.frameFailures));
|
|
407
368
|
continue;
|
|
408
369
|
}
|
|
409
370
|
if (!s.running) return;
|
|
410
|
-
|
|
411
|
-
if (result.type === "
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
pendingMarked: state.pendingMarked,
|
|
417
|
-
failedAttempts: state.failedAttempts
|
|
418
|
-
});
|
|
419
|
-
else if (state.at === "awaiting-result" || state.at === "settled-live") transition(s, entry, {
|
|
420
|
-
...state,
|
|
421
|
-
posted: true
|
|
422
|
-
});
|
|
423
|
-
if (entry.inv) s.lifecycle?.onPosted?.(entry.inv);
|
|
424
|
-
if (response !== void 0) {
|
|
425
|
-
applyResponse(s, response);
|
|
426
|
-
publish(s);
|
|
427
|
-
}
|
|
428
|
-
} else if (result.type === "rejected") {
|
|
429
|
-
transition(s, entry, { at: "done" }, {
|
|
430
|
-
ok: false,
|
|
431
|
-
failure: {
|
|
432
|
-
fate: "rejected",
|
|
433
|
-
cause: "server",
|
|
434
|
-
message: result.message
|
|
371
|
+
s.frameFailures = 0;
|
|
372
|
+
if (result.type === "ok") {
|
|
373
|
+
if (epoch === s.attachEpoch) {
|
|
374
|
+
for (const entry of lanes.cmdEntries) if (s.pending.includes(entry)) {
|
|
375
|
+
entry.posted = true;
|
|
376
|
+
s.lifecycle?.onPosted?.(entry.inv);
|
|
435
377
|
}
|
|
436
|
-
|
|
437
|
-
|
|
378
|
+
for (const entry of lanes.ctxEntries) entry.posted = true;
|
|
379
|
+
}
|
|
380
|
+
} else if (result.type === "refusal") {
|
|
381
|
+
if (epoch === s.attachEpoch) handleRefusal(s, result.syn);
|
|
438
382
|
} else if (result.type === "stale-lease") {
|
|
439
|
-
if (epoch === s.attachEpoch) await waitForWake(s);
|
|
440
|
-
} else if (result.type === "unknown-client") {
|
|
441
383
|
if (epoch === s.attachEpoch) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
at: "queued",
|
|
445
|
-
pendingMarked: state.pendingMarked
|
|
446
|
-
});
|
|
447
|
-
s.transport?.connection.reconnect();
|
|
448
|
-
while (s.running && !isFinished(s) && epoch === s.attachEpoch && clientId === s.record.clientId) await waitForWake(s);
|
|
384
|
+
transport.connection.reconnect();
|
|
385
|
+
await waitForWake(s);
|
|
449
386
|
}
|
|
450
|
-
} else if (result.type === "
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
fate: "result-unavailable",
|
|
456
|
-
cause: "session-lost"
|
|
457
|
-
} : {
|
|
458
|
-
fate: state.failedAttempts > 0 ? "lost" : "aborted",
|
|
459
|
-
cause: "session-lost"
|
|
460
|
-
};
|
|
461
|
-
evict(s, {
|
|
462
|
-
entry,
|
|
463
|
-
failure
|
|
464
|
-
});
|
|
465
|
-
s.transport?.connection.reconnect();
|
|
387
|
+
} else if (result.type === "no-lease") {
|
|
388
|
+
if (epoch === s.attachEpoch) {
|
|
389
|
+
s.lanesOnAttach = true;
|
|
390
|
+
transport.connection.reconnect();
|
|
391
|
+
while (s.running && !isFinished(s) && epoch === s.attachEpoch) await waitForWake(s);
|
|
466
392
|
}
|
|
467
393
|
}
|
|
468
394
|
}
|
|
@@ -488,35 +414,33 @@ const setTransport = (s, transport) => {
|
|
|
488
414
|
s.transport = transport;
|
|
489
415
|
wake(s);
|
|
490
416
|
};
|
|
491
|
-
const enqueue = (s, command, resolve, reject) => {
|
|
417
|
+
const enqueue = (s, command, params, resolve, reject) => {
|
|
492
418
|
const seq = s.record.nextSeq;
|
|
493
|
-
s
|
|
419
|
+
setRecord(s, {
|
|
494
420
|
...s.record,
|
|
495
421
|
nextSeq: seq + 1
|
|
496
|
-
};
|
|
497
|
-
s.store.save(s.record);
|
|
422
|
+
});
|
|
498
423
|
const entry = {
|
|
499
424
|
seq,
|
|
500
425
|
command,
|
|
501
|
-
inv:
|
|
426
|
+
inv: {
|
|
502
427
|
method: command.method,
|
|
503
|
-
params
|
|
428
|
+
params,
|
|
504
429
|
ts: Date.now(),
|
|
505
430
|
seq,
|
|
506
431
|
clientId: s.record.clientId
|
|
507
432
|
},
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
},
|
|
433
|
+
attempted: false,
|
|
434
|
+
posted: false,
|
|
435
|
+
applied: false,
|
|
512
436
|
resolve,
|
|
513
437
|
reject
|
|
514
438
|
};
|
|
515
439
|
s.pending.push(entry);
|
|
516
|
-
|
|
440
|
+
s.lifecycle?.onStart(entry.inv);
|
|
517
441
|
pump(s);
|
|
518
442
|
};
|
|
519
|
-
const sendCommand = (s,
|
|
443
|
+
const sendCommand = (s, method, params) => {
|
|
520
444
|
if (!s.running) return rejectedSend({
|
|
521
445
|
fate: "aborted",
|
|
522
446
|
cause: "unmount"
|
|
@@ -526,24 +450,107 @@ const sendCommand = (s, command) => {
|
|
|
526
450
|
cause: "stream-gone"
|
|
527
451
|
});
|
|
528
452
|
const result = new Promise((resolve, reject) => {
|
|
529
|
-
|
|
453
|
+
const { params: encoded, meta } = encodeParams(params);
|
|
454
|
+
enqueue(s, {
|
|
455
|
+
method,
|
|
456
|
+
params: encoded,
|
|
457
|
+
...meta !== void 0 && { meta }
|
|
458
|
+
}, params, resolve, reject);
|
|
530
459
|
});
|
|
531
460
|
result.catch(() => {});
|
|
532
461
|
return result;
|
|
533
462
|
};
|
|
534
463
|
const setContext = (s, value) => {
|
|
535
464
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
|
|
536
|
-
|
|
537
|
-
|
|
465
|
+
const { value: encoded, meta } = encodeValue(value);
|
|
466
|
+
s.ctxOps = [{
|
|
467
|
+
op: "replace",
|
|
468
|
+
path: [],
|
|
469
|
+
value: encoded,
|
|
470
|
+
...meta !== void 0 && { meta }
|
|
471
|
+
}];
|
|
472
|
+
const seq = s.record.ctxNextSeq;
|
|
473
|
+
setRecord(s, {
|
|
474
|
+
...s.record,
|
|
475
|
+
ctxNextSeq: seq + 1
|
|
476
|
+
});
|
|
477
|
+
s.ctxPending.push({
|
|
478
|
+
seq,
|
|
479
|
+
ops: s.ctxOps,
|
|
480
|
+
posted: false
|
|
481
|
+
});
|
|
482
|
+
wake(s);
|
|
483
|
+
pump(s);
|
|
538
484
|
};
|
|
539
|
-
const useStatewireSession = (options
|
|
540
|
-
const [
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
485
|
+
const useStatewireSession = (options) => {
|
|
486
|
+
const [defaultStore] = useState(createMemorySessionStore);
|
|
487
|
+
const store = options.sessionStore ?? defaultStore;
|
|
488
|
+
const [s] = useState(() => {
|
|
489
|
+
const stored = store.load();
|
|
490
|
+
const session = {
|
|
491
|
+
record: stored ?? {
|
|
492
|
+
clientId: newClientId(),
|
|
493
|
+
nextSeq: 1,
|
|
494
|
+
ctxNextSeq: 1
|
|
495
|
+
},
|
|
496
|
+
ctx: {
|
|
497
|
+
get clientId() {
|
|
498
|
+
return session.record.clientId;
|
|
499
|
+
},
|
|
500
|
+
takeAttachLanes: () => takeAttachLanes(session),
|
|
501
|
+
onHello: (args) => handleHello(session, args),
|
|
502
|
+
onOps: (args_0) => handleOps(session, args_0),
|
|
503
|
+
onCmds: (cmds) => handleCmds(session, cmds),
|
|
504
|
+
onRefusal: (syn) => handleRefusal(session, syn),
|
|
505
|
+
onIdle: (level) => setIdle(session, level),
|
|
506
|
+
onDetach: () => handleDetach(session),
|
|
507
|
+
onFinish: (args_1) => handleFinish(session, args_1)
|
|
508
|
+
},
|
|
509
|
+
recordPersisted: stored !== null,
|
|
510
|
+
store,
|
|
511
|
+
ephemeral: false,
|
|
512
|
+
sessionKnown: false,
|
|
513
|
+
durableSeq: 0,
|
|
514
|
+
resumeAt: 0,
|
|
515
|
+
idle: false,
|
|
516
|
+
onDone: void 0,
|
|
517
|
+
ctxOps: void 0,
|
|
518
|
+
ctxPending: [],
|
|
519
|
+
lanesOnAttach: false,
|
|
520
|
+
attachLaneSeqs: [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()],
|
|
521
|
+
frameFailures: 0,
|
|
522
|
+
pending: [],
|
|
523
|
+
canonical: void 0,
|
|
524
|
+
lastPublished: void 0,
|
|
525
|
+
changed: [],
|
|
526
|
+
source: {
|
|
527
|
+
snapshot: () => session.canonical,
|
|
528
|
+
takeChanged: () => {
|
|
529
|
+
const changed = session.changed;
|
|
530
|
+
session.changed = [];
|
|
531
|
+
return changed;
|
|
532
|
+
},
|
|
533
|
+
setOnChange: (listener) => {
|
|
534
|
+
session.notify = listener ?? (() => {});
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
attach: "detached",
|
|
538
|
+
attachEpoch: 0,
|
|
539
|
+
running: false,
|
|
540
|
+
pumping: false,
|
|
541
|
+
transport: null,
|
|
542
|
+
waiters: [],
|
|
543
|
+
lifecycle: null,
|
|
544
|
+
notify: () => {},
|
|
545
|
+
commands: createMethodProxy((method, params) => sendCommand(session, method, params))
|
|
545
546
|
};
|
|
546
|
-
|
|
547
|
+
bindCommandsHost(session.commands, { setListener: (listener_0) => {
|
|
548
|
+
session.lifecycle = listener_0;
|
|
549
|
+
} });
|
|
550
|
+
return session;
|
|
551
|
+
});
|
|
552
|
+
s.store = store;
|
|
553
|
+
s.onDone = options.onDone;
|
|
547
554
|
useEffect(() => {
|
|
548
555
|
start(s);
|
|
549
556
|
return () => stop(s);
|