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/client/core.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { createChanges } from "./changes";
|
|
3
|
-
import { getCommandsHost,
|
|
3
|
+
import { getCommandsHost, getStateHost } from "../host/host";
|
|
4
4
|
import type { CommandsInvocation, CommandsListener } from "../host/host";
|
|
5
5
|
import { recordMutations } from "../util/recordMutations";
|
|
6
6
|
import { createMacrotaskPoster } from "../util/macrotask";
|
|
@@ -26,319 +26,328 @@ const failureOf = (error: unknown): StatewireClient.SendFailure =>
|
|
|
26
26
|
|
|
27
27
|
type Overlay = { inv: CommandsInvocation; dropped: boolean };
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
getOptions: () => StatewireClient.Options<any, any
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
type Core = {
|
|
30
|
+
getOptions: () => StatewireClient.Options<any, any>;
|
|
31
|
+
value: Statewire<unknown> | null;
|
|
32
|
+
source: Statewire.StateSource | null;
|
|
33
|
+
overlays: Overlay[];
|
|
34
|
+
updates: Statewire.CommandUpdate[];
|
|
35
|
+
pendingChanged: StatewirePath[];
|
|
36
|
+
canonical: unknown;
|
|
37
|
+
published: unknown;
|
|
38
|
+
overlayPaths: readonly StatewirePath[];
|
|
39
|
+
notifiedPublished: unknown;
|
|
40
|
+
notifiedOverlayPaths: readonly StatewirePath[];
|
|
41
|
+
notifiedConnection: StatewireClient.Connection | null;
|
|
42
|
+
notifiedBusy: boolean;
|
|
43
|
+
stale: boolean;
|
|
44
|
+
everAttached: boolean;
|
|
45
|
+
overlaysDirty: boolean;
|
|
46
|
+
flushing: boolean;
|
|
47
|
+
scheduled: boolean;
|
|
48
|
+
listeners: Set<() => void>;
|
|
49
|
+
post: () => void;
|
|
50
|
+
commandsListener: CommandsListener;
|
|
51
|
+
getState: () => unknown;
|
|
52
|
+
getBusy: () => boolean;
|
|
53
|
+
commands: Statewire.CommandsProxy<any>;
|
|
54
|
+
subscribe: (listener: () => void) => () => void;
|
|
55
|
+
};
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const schedule = () => {
|
|
56
|
-
if (scheduled) return;
|
|
57
|
-
scheduled = true;
|
|
58
|
-
post();
|
|
59
|
-
};
|
|
57
|
+
const schedule = (c: Core) => {
|
|
58
|
+
if (c.scheduled) return;
|
|
59
|
+
c.scheduled = true;
|
|
60
|
+
c.post();
|
|
61
|
+
};
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const markStale = (c: Core) => {
|
|
64
|
+
c.stale = true;
|
|
65
|
+
schedule(c);
|
|
66
|
+
};
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
const next = everAttached ? undefined : getOptions().initialState;
|
|
78
|
-
if (next !== canonical) pendingChanged.push([]);
|
|
79
|
-
canonical = next;
|
|
80
|
-
} else if (value) {
|
|
81
|
-
if (value.state !== undefined) everAttached = true;
|
|
82
|
-
const next = everAttached ? value.state : getOptions().initialState;
|
|
83
|
-
if (next !== canonical) {
|
|
84
|
-
pendingChanged.push([]);
|
|
85
|
-
canonical = next;
|
|
86
|
-
}
|
|
68
|
+
// initialState stands in for the canonical state only before the first
|
|
69
|
+
// attach snapshot; replay boundaries after that read as loading, not preload.
|
|
70
|
+
const drainCanonical = (c: Core) => {
|
|
71
|
+
if (c.source) {
|
|
72
|
+
c.pendingChanged.push(...c.source.takeChanged());
|
|
73
|
+
const snapshot = c.source.snapshot();
|
|
74
|
+
if (snapshot !== undefined) {
|
|
75
|
+
c.everAttached = true;
|
|
76
|
+
c.canonical = snapshot;
|
|
77
|
+
return;
|
|
87
78
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const { optimistic, materialize } = getOptions();
|
|
98
|
-
let next = canonical;
|
|
99
|
-
let paths: readonly StatewirePath[] = [];
|
|
100
|
-
if (optimistic && canonical !== undefined) {
|
|
101
|
-
for (const overlay of overlays) {
|
|
102
|
-
if (overlay.dropped) continue;
|
|
103
|
-
const { inv } = overlay;
|
|
104
|
-
const ctx: StatewireClient.OptimisticContext = {
|
|
105
|
-
ts: inv.ts,
|
|
106
|
-
seq: inv.seq,
|
|
107
|
-
...(inv.clientId !== undefined && { clientId: inv.clientId }),
|
|
108
|
-
};
|
|
109
|
-
try {
|
|
110
|
-
const recorded = recordMutations(next, (draft) =>
|
|
111
|
-
optimistic(
|
|
112
|
-
draft as never,
|
|
113
|
-
{ method: inv.method, params: inv.params } as Statewire.Command,
|
|
114
|
-
ctx,
|
|
115
|
-
),
|
|
116
|
-
);
|
|
117
|
-
next = recorded.state;
|
|
118
|
-
paths = [...paths, ...recorded.changedPaths];
|
|
119
|
-
} catch (error) {
|
|
120
|
-
overlay.dropped = true;
|
|
121
|
-
console.error(
|
|
122
|
-
`statewire: optimistic reducer threw for "${inv.method}"; dropping its optimistic update`,
|
|
123
|
-
error,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
79
|
+
const next = c.everAttached ? undefined : c.getOptions().initialState;
|
|
80
|
+
if (next !== c.canonical) c.pendingChanged.push([]);
|
|
81
|
+
c.canonical = next;
|
|
82
|
+
} else if (c.value) {
|
|
83
|
+
if (c.value.state !== undefined) c.everAttached = true;
|
|
84
|
+
const next = c.everAttached ? c.value.state : c.getOptions().initialState;
|
|
85
|
+
if (next !== c.canonical) {
|
|
86
|
+
c.pendingChanged.push([]);
|
|
87
|
+
c.canonical = next;
|
|
127
88
|
}
|
|
128
|
-
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const recompute = (c: Core) => {
|
|
93
|
+
if (!c.stale || !c.value) return;
|
|
94
|
+
c.stale = false;
|
|
95
|
+
const prevCanonical = c.canonical;
|
|
96
|
+
drainCanonical(c);
|
|
97
|
+
if (c.canonical === prevCanonical && !c.overlaysDirty) return;
|
|
98
|
+
c.overlaysDirty = false;
|
|
99
|
+
const { optimistic, materialize } = c.getOptions();
|
|
100
|
+
let next = c.canonical;
|
|
101
|
+
let paths: readonly StatewirePath[] = [];
|
|
102
|
+
if (optimistic && c.canonical !== undefined) {
|
|
103
|
+
for (const overlay of c.overlays) {
|
|
104
|
+
if (overlay.dropped) continue;
|
|
105
|
+
const { inv } = overlay;
|
|
106
|
+
const ctx: StatewireClient.OptimisticContext = {
|
|
107
|
+
ts: inv.ts,
|
|
108
|
+
seq: inv.seq,
|
|
109
|
+
...(inv.clientId !== undefined && { clientId: inv.clientId }),
|
|
110
|
+
};
|
|
129
111
|
try {
|
|
130
112
|
const recorded = recordMutations(next, (draft) =>
|
|
131
|
-
|
|
113
|
+
optimistic(
|
|
114
|
+
draft as never,
|
|
115
|
+
{ method: inv.method, params: inv.params } as Statewire.Command,
|
|
116
|
+
ctx,
|
|
117
|
+
),
|
|
132
118
|
);
|
|
133
119
|
next = recorded.state;
|
|
134
120
|
paths = [...paths, ...recorded.changedPaths];
|
|
135
121
|
} catch (error) {
|
|
122
|
+
overlay.dropped = true;
|
|
136
123
|
console.error(
|
|
137
|
-
|
|
124
|
+
`statewire: optimistic reducer threw for "${inv.method}"; dropping its optimistic update`,
|
|
138
125
|
error,
|
|
139
126
|
);
|
|
140
127
|
}
|
|
141
128
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const getConnection = (): StatewireClient.Connection =>
|
|
147
|
-
value?.connection ?? LIVE_CONNECTION;
|
|
148
|
-
|
|
149
|
-
const flush = () => {
|
|
150
|
-
if (!value) return;
|
|
151
|
-
if (flushing) {
|
|
152
|
-
schedule();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
flushing = true;
|
|
129
|
+
}
|
|
130
|
+
if (materialize && next !== undefined) {
|
|
156
131
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
];
|
|
167
|
-
pendingChanged = [];
|
|
168
|
-
notifiedPublished = published;
|
|
169
|
-
notifiedOverlayPaths = overlayPaths;
|
|
170
|
-
options.onStateChange?.(published as never, {
|
|
171
|
-
changes: createChanges(prev, published, changed),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
if (updates.length > 0 && published !== undefined) {
|
|
175
|
-
const batch = updates;
|
|
176
|
-
updates = [];
|
|
177
|
-
for (const update of batch) {
|
|
178
|
-
options.onCommandChange?.(update, { state: published as never });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (events.length > 0 && published !== undefined) {
|
|
182
|
-
const batch = events;
|
|
183
|
-
events = [];
|
|
184
|
-
for (const event of batch) {
|
|
185
|
-
options.onEvent?.(event, { state: published as never });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const connection = getConnection();
|
|
189
|
-
const connectionChanged = connection !== notifiedConnection;
|
|
190
|
-
notifiedConnection = connection;
|
|
191
|
-
if (connectionChanged) options.onConnectionChange?.(connection);
|
|
192
|
-
if (stateChanged || connectionChanged) {
|
|
193
|
-
for (const listener of [...listeners]) listener();
|
|
194
|
-
}
|
|
195
|
-
} finally {
|
|
196
|
-
flushing = false;
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const note = (
|
|
201
|
-
inv: CommandsInvocation,
|
|
202
|
-
status: Statewire.CommandUpdate["status"],
|
|
203
|
-
extra?: { result?: unknown; failure?: StatewireClient.SendFailure },
|
|
204
|
-
) => {
|
|
205
|
-
if (!getOptions().onCommandChange) return;
|
|
206
|
-
updates.push({
|
|
207
|
-
method: inv.method,
|
|
208
|
-
params: inv.params as never[],
|
|
209
|
-
seq: inv.seq,
|
|
210
|
-
status,
|
|
211
|
-
...extra,
|
|
212
|
-
});
|
|
213
|
-
schedule();
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const commandsListener: CommandsListener = {
|
|
217
|
-
onStart: (inv) => {
|
|
218
|
-
overlays.push({ inv, dropped: false });
|
|
219
|
-
overlaysDirty = true;
|
|
220
|
-
markStale();
|
|
221
|
-
note(inv, "queued");
|
|
222
|
-
},
|
|
223
|
-
onPosted: (inv) => note(inv, "posted"),
|
|
224
|
-
onAck: (inv) => note(inv, "acked"),
|
|
225
|
-
onCovered: (inv) => {
|
|
226
|
-
if (!overlays.some((o) => o.inv === inv)) return;
|
|
227
|
-
overlays = overlays.filter((o) => o.inv !== inv);
|
|
228
|
-
overlaysDirty = true;
|
|
229
|
-
markStale();
|
|
230
|
-
},
|
|
231
|
-
onSettle: (inv, outcome) => {
|
|
232
|
-
note(
|
|
233
|
-
inv,
|
|
234
|
-
"settled",
|
|
235
|
-
outcome.type === "accepted"
|
|
236
|
-
? { result: outcome.payload }
|
|
237
|
-
: { failure: failureOf(outcome.error) },
|
|
132
|
+
const recorded = recordMutations(next, (draft) =>
|
|
133
|
+
materialize(draft as never),
|
|
134
|
+
);
|
|
135
|
+
next = recorded.state;
|
|
136
|
+
paths = [...paths, ...recorded.changedPaths];
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(
|
|
139
|
+
"statewire: materialize threw; publishing without its update",
|
|
140
|
+
error,
|
|
238
141
|
);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
c.published = next;
|
|
145
|
+
c.overlayPaths = paths;
|
|
146
|
+
};
|
|
242
147
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
events.push(event);
|
|
246
|
-
schedule();
|
|
247
|
-
};
|
|
148
|
+
const getConnection = (c: Core): StatewireClient.Connection =>
|
|
149
|
+
c.value?.connection ?? LIVE_CONNECTION;
|
|
248
150
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
151
|
+
const flush = (c: Core) => {
|
|
152
|
+
if (!c.value) return;
|
|
153
|
+
if (c.flushing) {
|
|
154
|
+
schedule(c);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
c.flushing = true;
|
|
158
|
+
try {
|
|
159
|
+
recompute(c);
|
|
160
|
+
const options = c.getOptions();
|
|
161
|
+
const stateChanged = c.published !== c.notifiedPublished;
|
|
162
|
+
if (stateChanged) {
|
|
163
|
+
const prev = c.notifiedPublished;
|
|
164
|
+
const changed = [
|
|
165
|
+
...c.pendingChanged,
|
|
166
|
+
...c.notifiedOverlayPaths,
|
|
167
|
+
...c.overlayPaths,
|
|
168
|
+
];
|
|
169
|
+
c.pendingChanged = [];
|
|
170
|
+
c.notifiedPublished = c.published;
|
|
171
|
+
c.notifiedOverlayPaths = c.overlayPaths;
|
|
172
|
+
options.onStateChange?.(c.published as never, {
|
|
173
|
+
changes: createChanges(prev, c.published, changed),
|
|
174
|
+
});
|
|
266
175
|
}
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
overlaysDirty = true;
|
|
176
|
+
if (c.updates.length > 0 && c.published !== undefined) {
|
|
177
|
+
const batch = c.updates;
|
|
178
|
+
c.updates = [];
|
|
179
|
+
for (const update of batch) {
|
|
180
|
+
options.onCommandChange?.(update, { state: c.published as never });
|
|
273
181
|
}
|
|
274
|
-
markStale();
|
|
275
182
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
183
|
+
const connection = getConnection(c);
|
|
184
|
+
const connectionChanged = connection !== c.notifiedConnection;
|
|
185
|
+
c.notifiedConnection = connection;
|
|
186
|
+
if (connectionChanged) options.onConnectionChange?.(connection);
|
|
187
|
+
const busy = c.getBusy();
|
|
188
|
+
const busyChanged = busy !== c.notifiedBusy;
|
|
189
|
+
c.notifiedBusy = busy;
|
|
190
|
+
if (stateChanged || connectionChanged || busyChanged) {
|
|
191
|
+
for (const listener of [...c.listeners]) listener();
|
|
279
192
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
193
|
+
} finally {
|
|
194
|
+
c.flushing = false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
283
197
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
198
|
+
const note = (
|
|
199
|
+
c: Core,
|
|
200
|
+
inv: CommandsInvocation,
|
|
201
|
+
status: Statewire.CommandUpdate["status"],
|
|
202
|
+
extra?: { result?: unknown; failure?: StatewireClient.SendFailure },
|
|
203
|
+
) => {
|
|
204
|
+
if (!c.getOptions().onCommandChange) return;
|
|
205
|
+
c.updates.push({
|
|
206
|
+
method: inv.method,
|
|
207
|
+
params: inv.params as never[],
|
|
208
|
+
seq: inv.seq,
|
|
209
|
+
status,
|
|
210
|
+
...extra,
|
|
211
|
+
});
|
|
212
|
+
schedule(c);
|
|
213
|
+
};
|
|
293
214
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
215
|
+
const attach = (c: Core, next: Statewire<unknown>) => {
|
|
216
|
+
if (next === c.value) return;
|
|
217
|
+
const prev = c.value;
|
|
218
|
+
c.value = next;
|
|
219
|
+
const nextSource =
|
|
220
|
+
next.source ??
|
|
221
|
+
(typeof next.state === "object" && next.state !== null
|
|
222
|
+
? getStateHost(next.state)
|
|
223
|
+
: undefined) ??
|
|
224
|
+
null;
|
|
225
|
+
if (nextSource !== c.source) {
|
|
226
|
+
c.source?.setOnChange(null);
|
|
227
|
+
c.source = nextSource;
|
|
228
|
+
c.source?.setOnChange(() => markStale(c));
|
|
229
|
+
// A new source is a new attach lifecycle: initialState preloads again.
|
|
230
|
+
c.everAttached = false;
|
|
231
|
+
markStale(c);
|
|
232
|
+
}
|
|
233
|
+
if (next.commands !== prev?.commands) {
|
|
234
|
+
if (prev) getCommandsHost(prev.commands)?.setListener(null);
|
|
235
|
+
getCommandsHost(next.commands)?.setListener(c.commandsListener);
|
|
236
|
+
if (c.overlays.length > 0) {
|
|
237
|
+
c.overlays = [];
|
|
238
|
+
c.overlaysDirty = true;
|
|
239
|
+
}
|
|
240
|
+
markStale(c);
|
|
241
|
+
}
|
|
242
|
+
if (!c.source) markStale(c);
|
|
243
|
+
schedule(c);
|
|
244
|
+
};
|
|
312
245
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
commands,
|
|
321
|
-
subscribe: (listener: () => void) => {
|
|
322
|
-
listeners.add(listener);
|
|
323
|
-
return () => {
|
|
324
|
-
listeners.delete(listener);
|
|
325
|
-
};
|
|
326
|
-
},
|
|
327
|
-
};
|
|
246
|
+
const detach = (c: Core) => {
|
|
247
|
+
c.source?.setOnChange(null);
|
|
248
|
+
c.source = null;
|
|
249
|
+
if (c.value) {
|
|
250
|
+
getCommandsHost(c.value.commands)?.setListener(null);
|
|
251
|
+
c.value = null;
|
|
252
|
+
}
|
|
328
253
|
};
|
|
329
254
|
|
|
330
255
|
export const useStatewireCore = (
|
|
331
|
-
getOptions: () => StatewireClient.Options<any, any
|
|
256
|
+
getOptions: () => StatewireClient.Options<any, any>,
|
|
332
257
|
value: Statewire<unknown>,
|
|
333
258
|
) => {
|
|
334
|
-
const [core] = useState(() =>
|
|
259
|
+
const [core] = useState((): Core => {
|
|
260
|
+
const c: Core = {
|
|
261
|
+
getOptions,
|
|
262
|
+
value: null,
|
|
263
|
+
source: null,
|
|
264
|
+
overlays: [],
|
|
265
|
+
updates: [],
|
|
266
|
+
pendingChanged: [],
|
|
267
|
+
canonical: undefined,
|
|
268
|
+
published: undefined,
|
|
269
|
+
overlayPaths: [],
|
|
270
|
+
notifiedPublished: undefined,
|
|
271
|
+
notifiedOverlayPaths: [],
|
|
272
|
+
notifiedConnection: null,
|
|
273
|
+
notifiedBusy: true,
|
|
274
|
+
stale: false,
|
|
275
|
+
everAttached: false,
|
|
276
|
+
overlaysDirty: false,
|
|
277
|
+
flushing: false,
|
|
278
|
+
scheduled: false,
|
|
279
|
+
listeners: new Set(),
|
|
280
|
+
post: createMacrotaskPoster(() => {
|
|
281
|
+
c.scheduled = false;
|
|
282
|
+
flush(c);
|
|
283
|
+
}),
|
|
284
|
+
commandsListener: {
|
|
285
|
+
onStart: (inv) => {
|
|
286
|
+
c.overlays.push({ inv, dropped: false });
|
|
287
|
+
c.overlaysDirty = true;
|
|
288
|
+
markStale(c);
|
|
289
|
+
note(c, inv, "queued");
|
|
290
|
+
},
|
|
291
|
+
onPosted: (inv) => note(c, inv, "posted"),
|
|
292
|
+
onApplied: (inv) => note(c, inv, "applied"),
|
|
293
|
+
onCovered: (inv) => {
|
|
294
|
+
if (!c.overlays.some((o) => o.inv === inv)) return;
|
|
295
|
+
c.overlays = c.overlays.filter((o) => o.inv !== inv);
|
|
296
|
+
c.overlaysDirty = true;
|
|
297
|
+
markStale(c);
|
|
298
|
+
},
|
|
299
|
+
onSettle: (inv, outcome) => {
|
|
300
|
+
note(
|
|
301
|
+
c,
|
|
302
|
+
inv,
|
|
303
|
+
"settled",
|
|
304
|
+
outcome.type === "accepted"
|
|
305
|
+
? { result: outcome.payload }
|
|
306
|
+
: { failure: failureOf(outcome.error) },
|
|
307
|
+
);
|
|
308
|
+
flush(c);
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
getState: () => {
|
|
312
|
+
recompute(c);
|
|
313
|
+
return c.published;
|
|
314
|
+
},
|
|
315
|
+
getBusy: () => c.value?.isBusy ?? true,
|
|
316
|
+
commands: createMethodProxy<Statewire.CommandsProxy<any>>(
|
|
317
|
+
(method, params) => {
|
|
318
|
+
if (!c.value) {
|
|
319
|
+
return Promise.reject(
|
|
320
|
+
new StatewireSendError({ fate: "aborted", cause: "unmount" }),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const send = (
|
|
324
|
+
c.value.commands as Record<
|
|
325
|
+
string,
|
|
326
|
+
(...args: unknown[]) => Promise<unknown>
|
|
327
|
+
>
|
|
328
|
+
)[method]!;
|
|
329
|
+
const promise = send(...params);
|
|
330
|
+
flush(c);
|
|
331
|
+
return promise;
|
|
332
|
+
},
|
|
333
|
+
),
|
|
334
|
+
subscribe: (listener) => {
|
|
335
|
+
c.listeners.add(listener);
|
|
336
|
+
return () => {
|
|
337
|
+
c.listeners.delete(listener);
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
return c;
|
|
342
|
+
});
|
|
343
|
+
core.getOptions = getOptions;
|
|
335
344
|
// Attach during render so the first snapshot reads the transport's state;
|
|
336
345
|
// re-attaching the same transport is a no-op and the single-slot host
|
|
337
346
|
// listeners make a swap land on the last writer.
|
|
338
|
-
|
|
347
|
+
attach(core, value);
|
|
339
348
|
useEffect(() => {
|
|
340
|
-
|
|
349
|
+
attach(core, value);
|
|
341
350
|
});
|
|
342
|
-
useEffect(() => () =>
|
|
351
|
+
useEffect(() => () => detach(core), [core]);
|
|
343
352
|
return core;
|
|
344
353
|
};
|