@softov/ahpc 0.1.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/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/** Who is holding which channel open, and what the host is saying on it. */
|
|
2
|
+
function bag(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: null;
|
|
6
|
+
}
|
|
7
|
+
export function openChannels(options) {
|
|
8
|
+
const held = new Map();
|
|
9
|
+
const refused = new Map();
|
|
10
|
+
let client = options.client;
|
|
11
|
+
let seen = 0;
|
|
12
|
+
/**
|
|
13
|
+
* Which connection the current holds belong to.
|
|
14
|
+
*
|
|
15
|
+
* A subscribe in flight when the socket dies would otherwise land on the
|
|
16
|
+
* next connection and open a channel twice; it compares this instead.
|
|
17
|
+
*/
|
|
18
|
+
let generation = 0;
|
|
19
|
+
const linger = options.lingerMs ?? 0;
|
|
20
|
+
const entry = (uri) => {
|
|
21
|
+
const found = held.get(uri);
|
|
22
|
+
if (found)
|
|
23
|
+
return found;
|
|
24
|
+
const made = { uses: 0, consumers: new Set(), opened: false, told: false, waiting: [] };
|
|
25
|
+
held.set(uri, made);
|
|
26
|
+
return made;
|
|
27
|
+
};
|
|
28
|
+
/** Note the host's counter, which advances with state rather than with messages. */
|
|
29
|
+
const note = (event) => {
|
|
30
|
+
if (event.type !== 'action')
|
|
31
|
+
return;
|
|
32
|
+
const seq = bag(event.params)?.serverSeq;
|
|
33
|
+
if (typeof seq === 'number' && seq > seen)
|
|
34
|
+
seen = seq;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* An action the host refused, which is not an action that happened.
|
|
38
|
+
*
|
|
39
|
+
* A rejected envelope carries the action the host declined to apply, so a
|
|
40
|
+
* consumer that reduced it would make the very change it was just told did
|
|
41
|
+
* not happen - worse than the silence, because the screen then disagrees
|
|
42
|
+
* with the host until something else moves it. It is reported and dropped.
|
|
43
|
+
*
|
|
44
|
+
* `serverSeq` is not read off it either. The counter advances with state,
|
|
45
|
+
* and a refusal moved none: the number on the envelope is the one this host
|
|
46
|
+
* is still at, and taking it as progress would leave a gap on the next
|
|
47
|
+
* reconnect that nothing can fill.
|
|
48
|
+
*
|
|
49
|
+
* A rejection answers one client's dispatch. One attributed to another
|
|
50
|
+
* client is nothing to say to the person sitting here, so it is dropped
|
|
51
|
+
* without being reported; one with no origin at all is taken as ours,
|
|
52
|
+
* because the alternative is losing the message a host that omits it sent.
|
|
53
|
+
*/
|
|
54
|
+
const rejected = (uri, event) => {
|
|
55
|
+
if (event.type !== 'action')
|
|
56
|
+
return false;
|
|
57
|
+
const envelope = bag(event.params);
|
|
58
|
+
const why = envelope?.rejectionReason;
|
|
59
|
+
if (typeof why !== 'string')
|
|
60
|
+
return false;
|
|
61
|
+
const from = bag(envelope?.origin)?.clientId;
|
|
62
|
+
const mine = options.clientId === undefined || typeof from !== 'string' || from === options.clientId;
|
|
63
|
+
if (mine)
|
|
64
|
+
options.onRejection?.(uri, why);
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
const deliver = (uri, event) => {
|
|
68
|
+
if (rejected(uri, event))
|
|
69
|
+
return;
|
|
70
|
+
note(event);
|
|
71
|
+
const channel = held.get(uri);
|
|
72
|
+
if (!channel)
|
|
73
|
+
return;
|
|
74
|
+
// Before the snapshot has landed there is nothing to apply this to. The
|
|
75
|
+
// host starts sending the moment it accepts the subscribe, which is
|
|
76
|
+
// earlier than it answers one.
|
|
77
|
+
if (!channel.opened || !channel.told) {
|
|
78
|
+
channel.waiting.push(event);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const consumer of channel.consumers)
|
|
82
|
+
consumer.event(event);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Record a refusal, and give it to whoever is entitled to it.
|
|
86
|
+
*
|
|
87
|
+
* A reader that supplied `refused` has an opinion about what the refusal
|
|
88
|
+
* means to it - a session draws it in the transcript, an automations
|
|
89
|
+
* channel takes it as "this host serves none" - and reporting it to the
|
|
90
|
+
* connection as well makes an expected answer look like a fault. Only a
|
|
91
|
+
* refusal nobody claimed goes to `onRefusal`.
|
|
92
|
+
*/
|
|
93
|
+
const refuse = (uri, message) => {
|
|
94
|
+
refused.set(uri, message);
|
|
95
|
+
const channel = held.get(uri);
|
|
96
|
+
let claimed = false;
|
|
97
|
+
for (const consumer of channel?.consumers ?? []) {
|
|
98
|
+
if (!consumer.refused)
|
|
99
|
+
continue;
|
|
100
|
+
consumer.refused(message);
|
|
101
|
+
claimed = true;
|
|
102
|
+
}
|
|
103
|
+
if (!claimed)
|
|
104
|
+
options.onRefusal?.(uri, message);
|
|
105
|
+
};
|
|
106
|
+
/** Read a connection's whole event stream until it ends or is superseded. */
|
|
107
|
+
const drain = (next, era) => {
|
|
108
|
+
client = next;
|
|
109
|
+
void (async () => {
|
|
110
|
+
try {
|
|
111
|
+
for await (const addressed of next.events()) {
|
|
112
|
+
if (era !== generation)
|
|
113
|
+
return;
|
|
114
|
+
deliver(addressed.channel, addressed.event);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* the connection going is the supervisor's to notice */ }
|
|
118
|
+
})();
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* One `subscribe` per channel at a time, however many readers arrive.
|
|
122
|
+
*
|
|
123
|
+
* Not an optimisation, and specifically about two being *in flight*. The
|
|
124
|
+
* reference host puts a pending marker under the channel while it restores
|
|
125
|
+
* the session; a subscribe arriving while that marker is unresolved
|
|
126
|
+
* replaces it, and the first then finds itself no longer current and is
|
|
127
|
+
* answered `-32001 Resource not found`, naming a channel that exists. A
|
|
128
|
+
* re-subscribe to a channel already open is idempotent there - it is only
|
|
129
|
+
* the overlap that collides. Reading a row's detail and opening the view on
|
|
130
|
+
* it are two readers a keystroke apart, which is exactly that overlap, and
|
|
131
|
+
* each was sending its own. They share this instead.
|
|
132
|
+
*
|
|
133
|
+
* The promise is dropped once it settles rather than kept: it is here to
|
|
134
|
+
* make concurrent readers into one request, not to hand the second reader
|
|
135
|
+
* an answer from before it asked.
|
|
136
|
+
*/
|
|
137
|
+
const ask = (uri) => {
|
|
138
|
+
const channel = entry(uri);
|
|
139
|
+
if (channel.pending)
|
|
140
|
+
return channel.pending;
|
|
141
|
+
const era = generation;
|
|
142
|
+
const asking = client.subscribe(uri).then(({ result }) => {
|
|
143
|
+
if (era === generation && held.get(uri) === channel) {
|
|
144
|
+
channel.opened = true;
|
|
145
|
+
channel.fromSeq = result.snapshot?.fromSeq;
|
|
146
|
+
}
|
|
147
|
+
return bag(result.snapshot?.state);
|
|
148
|
+
});
|
|
149
|
+
channel.pending = asking;
|
|
150
|
+
const done = () => { if (channel.pending === asking)
|
|
151
|
+
channel.pending = undefined; };
|
|
152
|
+
asking.then(done, done);
|
|
153
|
+
return asking;
|
|
154
|
+
};
|
|
155
|
+
/** Ask the host for a channel, and give what comes back to whoever is waiting. */
|
|
156
|
+
const start = (uri, era) => {
|
|
157
|
+
const opening = entry(uri);
|
|
158
|
+
opening.told = false;
|
|
159
|
+
void (async () => {
|
|
160
|
+
try {
|
|
161
|
+
const state = await ask(uri);
|
|
162
|
+
if (era !== generation)
|
|
163
|
+
return;
|
|
164
|
+
const channel = held.get(uri);
|
|
165
|
+
if (!channel || channel.uses === 0 || channel.told)
|
|
166
|
+
return;
|
|
167
|
+
channel.told = true;
|
|
168
|
+
for (const consumer of channel.consumers)
|
|
169
|
+
consumer.opened(state);
|
|
170
|
+
const queued = channel.waiting.splice(0);
|
|
171
|
+
for (const event of queued) {
|
|
172
|
+
const seq = bag(event.params)?.serverSeq;
|
|
173
|
+
if (event.type === 'action' && typeof seq === 'number'
|
|
174
|
+
&& channel.fromSeq !== undefined && seq <= channel.fromSeq)
|
|
175
|
+
continue;
|
|
176
|
+
for (const consumer of channel.consumers)
|
|
177
|
+
consumer.event(event);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (era !== generation)
|
|
182
|
+
return;
|
|
183
|
+
refuse(uri, options.reason(error));
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
};
|
|
187
|
+
/** Let the host know nobody is reading, which is the only handle a watch has. */
|
|
188
|
+
/** Let the host know nobody is reading, once nobody has been for a while. */
|
|
189
|
+
const drop = (uri) => {
|
|
190
|
+
const channel = held.get(uri);
|
|
191
|
+
if (!channel || channel.uses > 0)
|
|
192
|
+
return;
|
|
193
|
+
const era = generation;
|
|
194
|
+
const release = () => {
|
|
195
|
+
if (era !== generation || held.get(uri) !== channel)
|
|
196
|
+
return;
|
|
197
|
+
const now = held.get(uri);
|
|
198
|
+
// Somebody took it back while this was waiting, which is the whole
|
|
199
|
+
// reason for waiting.
|
|
200
|
+
if (!now || now.uses > 0)
|
|
201
|
+
return;
|
|
202
|
+
// Keep the entry until the request answers. A reader returning meanwhile
|
|
203
|
+
// shares it; otherwise its successful subscribe still needs releasing.
|
|
204
|
+
if (now.pending) {
|
|
205
|
+
void now.pending.then(release, release);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
held.delete(uri);
|
|
209
|
+
if (!now.opened)
|
|
210
|
+
return;
|
|
211
|
+
void client.unsubscribe(uri).catch(() => undefined);
|
|
212
|
+
};
|
|
213
|
+
if (linger <= 0) {
|
|
214
|
+
release();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
clearTimeout(channel.leaving);
|
|
218
|
+
channel.leaving = setTimeout(() => { channel.leaving = undefined; release(); }, linger);
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
open: (uri, consumer) => {
|
|
222
|
+
const known = refused.get(uri);
|
|
223
|
+
if (known !== undefined) {
|
|
224
|
+
// Answered already. Told on the way out rather than on the way in, so
|
|
225
|
+
// a caller that only wanted the handle still gets one.
|
|
226
|
+
queueMicrotask(() => consumer.refused?.(known));
|
|
227
|
+
return { release: () => undefined };
|
|
228
|
+
}
|
|
229
|
+
const channel = entry(uri);
|
|
230
|
+
// Taking it back before the release fired is what keeps the
|
|
231
|
+
// subscription alive across a screen closing and opening again.
|
|
232
|
+
clearTimeout(channel.leaving);
|
|
233
|
+
channel.leaving = undefined;
|
|
234
|
+
channel.consumers.add(consumer);
|
|
235
|
+
/*
|
|
236
|
+
* Alone, rather than the only hold. A snapshot read holds the channel
|
|
237
|
+
* too, and counting it as a reader left a view opened during one with
|
|
238
|
+
* nobody to hand it the state: `state` does not deliver to consumers,
|
|
239
|
+
* and the view was not first, so nothing did. What arrived was a pane
|
|
240
|
+
* with a session's name on it and none of the session in it.
|
|
241
|
+
*
|
|
242
|
+
* Still a fresh subscribe, even when the channel was only lingering:
|
|
243
|
+
* asking again is how this reader gets a snapshot, and the host is
|
|
244
|
+
* still holding the channel because no `unsubscribe` went out. Where a
|
|
245
|
+
* subscribe is already out, `ask` hands back that one instead of
|
|
246
|
+
* sending a second - which is the request the host answers by
|
|
247
|
+
* cancelling the first.
|
|
248
|
+
*/
|
|
249
|
+
const alone = channel.consumers.size === 1;
|
|
250
|
+
channel.uses += 1;
|
|
251
|
+
if (alone && channel.initial !== undefined) {
|
|
252
|
+
// The handshake already asked, and this is the answer it got.
|
|
253
|
+
const opening = channel.initial;
|
|
254
|
+
channel.initial = undefined;
|
|
255
|
+
channel.told = true;
|
|
256
|
+
queueMicrotask(() => {
|
|
257
|
+
if (!channel.consumers.has(consumer))
|
|
258
|
+
return;
|
|
259
|
+
consumer.opened(opening);
|
|
260
|
+
const queued = channel.waiting.splice(0);
|
|
261
|
+
for (const event of queued)
|
|
262
|
+
for (const one of channel.consumers)
|
|
263
|
+
one.event(event);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (alone) {
|
|
267
|
+
channel.told = false;
|
|
268
|
+
start(uri, generation);
|
|
269
|
+
}
|
|
270
|
+
else if (channel.told) {
|
|
271
|
+
// The readers keep their own reduced state. Ask once for a fresh
|
|
272
|
+
// baseline and hand it to all of them, buffering events until it lands.
|
|
273
|
+
// A null snapshot would leave the newcomer with an empty conversation.
|
|
274
|
+
start(uri, generation);
|
|
275
|
+
}
|
|
276
|
+
let holding = true;
|
|
277
|
+
return {
|
|
278
|
+
release: () => {
|
|
279
|
+
if (!holding)
|
|
280
|
+
return;
|
|
281
|
+
holding = false;
|
|
282
|
+
channel.consumers.delete(consumer);
|
|
283
|
+
channel.uses -= 1;
|
|
284
|
+
if (channel.uses === 0)
|
|
285
|
+
drop(uri);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
state: async (uri) => {
|
|
290
|
+
const known = refused.get(uri);
|
|
291
|
+
if (known !== undefined)
|
|
292
|
+
return null;
|
|
293
|
+
/*
|
|
294
|
+
* A read is a hold that is given up straight away, not a subscribe
|
|
295
|
+
* followed by an unsubscribe.
|
|
296
|
+
*
|
|
297
|
+
* The difference is what the host sees. Reading a session's snapshot
|
|
298
|
+
* and then opening the view on it are a moment apart, and releasing the
|
|
299
|
+
* first the instant it is done puts an `unsubscribe` between them - at
|
|
300
|
+
* which point a host that evicts on the last subscriber leaving is
|
|
301
|
+
* restoring the session from disk exactly as the view asks for it. Going
|
|
302
|
+
* through the same counting and the same pause as any other reader means
|
|
303
|
+
* the two coalesce into one subscription and nothing is let go in the
|
|
304
|
+
* middle.
|
|
305
|
+
*/
|
|
306
|
+
const channel = entry(uri);
|
|
307
|
+
clearTimeout(channel.leaving);
|
|
308
|
+
channel.leaving = undefined;
|
|
309
|
+
channel.uses += 1;
|
|
310
|
+
try {
|
|
311
|
+
return await ask(uri);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
refuse(uri, options.reason(error));
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
channel.uses -= 1;
|
|
319
|
+
if (channel.uses === 0)
|
|
320
|
+
drop(uri);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
held: () => [...held.keys()].filter((uri) => (held.get(uri)?.uses ?? 0) > 0),
|
|
324
|
+
seq: () => seen,
|
|
325
|
+
refusal: (uri) => refused.get(uri),
|
|
326
|
+
forget: (uri) => { if (uri === undefined)
|
|
327
|
+
refused.clear();
|
|
328
|
+
else
|
|
329
|
+
refused.delete(uri); },
|
|
330
|
+
drain: (next) => { drain(next, generation); },
|
|
331
|
+
resume: (next, result) => {
|
|
332
|
+
generation += 1;
|
|
333
|
+
client = next;
|
|
334
|
+
// Whatever was in flight belonged to the socket that went. Kept, it
|
|
335
|
+
// would be handed to the first reader on the new one as an answer that
|
|
336
|
+
// is never coming.
|
|
337
|
+
for (const channel of held.values())
|
|
338
|
+
channel.pending = undefined;
|
|
339
|
+
// Before anything is applied, so an action the host sends while this is
|
|
340
|
+
// still catching up is queued rather than dropped on the floor.
|
|
341
|
+
drain(next, generation);
|
|
342
|
+
const gone = new Set(result.missing ?? []);
|
|
343
|
+
for (const uri of gone) {
|
|
344
|
+
const channel = held.get(uri);
|
|
345
|
+
if (!channel)
|
|
346
|
+
continue;
|
|
347
|
+
held.delete(uri);
|
|
348
|
+
for (const consumer of channel.consumers) {
|
|
349
|
+
consumer.refused?.('This session is no longer on the host.');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Replay: the host could name everything this client missed, so the
|
|
353
|
+
// mirrors it has are still good and only the gap needs filling.
|
|
354
|
+
for (const envelope of result.replayed ?? []) {
|
|
355
|
+
const uri = bag(envelope)?.channel;
|
|
356
|
+
if (typeof uri !== 'string')
|
|
357
|
+
continue;
|
|
358
|
+
const channel = held.get(uri);
|
|
359
|
+
if (!channel)
|
|
360
|
+
continue;
|
|
361
|
+
const event = { type: 'action', params: envelope };
|
|
362
|
+
note(event);
|
|
363
|
+
for (const consumer of channel.consumers)
|
|
364
|
+
consumer.event(event);
|
|
365
|
+
}
|
|
366
|
+
if (result.replayed !== undefined) {
|
|
367
|
+
// Replay is what the host sends *instead of* a fresh snapshot, so the
|
|
368
|
+
// subscriptions behind it are ones it restored itself and none of them
|
|
369
|
+
// needs asking for again.
|
|
370
|
+
for (const channel of held.values()) {
|
|
371
|
+
if (channel.uses === 0 || channel.opened)
|
|
372
|
+
continue;
|
|
373
|
+
channel.opened = true;
|
|
374
|
+
channel.told = true;
|
|
375
|
+
const queued = channel.waiting.splice(0);
|
|
376
|
+
for (const event of queued) {
|
|
377
|
+
for (const consumer of channel.consumers)
|
|
378
|
+
consumer.event(event);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Snapshot: the gap was longer than the host's buffer, so every mirror
|
|
383
|
+
// is stale and every reader is handed the state rather than a delta.
|
|
384
|
+
for (const snapshot of result.resumed ?? []) {
|
|
385
|
+
const channel = held.get(snapshot.resource);
|
|
386
|
+
if (!channel)
|
|
387
|
+
continue;
|
|
388
|
+
channel.opened = true;
|
|
389
|
+
channel.told = true;
|
|
390
|
+
channel.waiting.length = 0;
|
|
391
|
+
for (const consumer of channel.consumers)
|
|
392
|
+
consumer.opened(bag(snapshot.state));
|
|
393
|
+
}
|
|
394
|
+
for (const [uri, channel] of held) {
|
|
395
|
+
if (channel.uses === 0)
|
|
396
|
+
continue;
|
|
397
|
+
// A channel the host restored but sent no snapshot for is still being
|
|
398
|
+
// served; one it never restored has to be asked for again.
|
|
399
|
+
if (!channel.opened)
|
|
400
|
+
start(uri, generation);
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
adopt: (uri, state) => {
|
|
404
|
+
const channel = entry(uri);
|
|
405
|
+
channel.opened = true;
|
|
406
|
+
channel.told = false;
|
|
407
|
+
channel.initial = state;
|
|
408
|
+
},
|
|
409
|
+
detach: () => {
|
|
410
|
+
generation += 1;
|
|
411
|
+
for (const [uri, channel] of held) {
|
|
412
|
+
if (channel.uses === 0) {
|
|
413
|
+
clearTimeout(channel.leaving);
|
|
414
|
+
held.delete(uri);
|
|
415
|
+
}
|
|
416
|
+
channel.opened = false;
|
|
417
|
+
channel.told = false;
|
|
418
|
+
channel.waiting.length = 0;
|
|
419
|
+
channel.pending = undefined;
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|