mandala-computer-mcp 0.1.1 → 0.4.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 +139 -24
- package/dist/api.d.ts +19 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +355 -76
- package/dist/api.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +113 -8
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +100 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +169 -29
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +45 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +422 -114
- package/dist/events.js.map +1 -1
- package/dist/format.d.ts +48 -0
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +111 -4
- package/dist/format.js.map +1 -1
- package/dist/http-body.d.ts +17 -0
- package/dist/http-body.d.ts.map +1 -0
- package/dist/http-body.js +48 -0
- package/dist/http-body.js.map +1 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +177 -51
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +17 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +17 -0
- package/dist/limits.js.map +1 -0
- package/dist/paths.d.ts +30 -20
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +89 -25
- package/dist/paths.js.map +1 -1
- package/dist/poll.d.ts +107 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +233 -0
- package/dist/poll.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +72 -5
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/computers.d.ts.map +1 -1
- package/dist/tools/computers.js +559 -233
- package/dist/tools/computers.js.map +1 -1
- package/dist/tools/events.d.ts.map +1 -1
- package/dist/tools/events.js +359 -69
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/guest.d.ts.map +1 -1
- package/dist/tools/guest.js +234 -33
- package/dist/tools/guest.js.map +1 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +92 -8
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +501 -33
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/templates.d.ts.map +1 -1
- package/dist/tools/templates.js +61 -26
- package/dist/tools/templates.js.map +1 -1
- package/dist/tools/webhooks.d.ts.map +1 -1
- package/dist/tools/webhooks.js +116 -17
- package/dist/tools/webhooks.js.map +1 -1
- package/package.json +3 -2
package/dist/tools/events.js
CHANGED
|
@@ -23,7 +23,7 @@ const KNOWN_TYPES = [
|
|
|
23
23
|
'window.focused',
|
|
24
24
|
'window.blurred',
|
|
25
25
|
'clipboard.changed',
|
|
26
|
-
// The one type nobody is sent unasked (
|
|
26
|
+
// The one type nobody is sent unasked (OPL-3927). It is here so a
|
|
27
27
|
// model reading this list knows it exists; a wait that names it and nothing
|
|
28
28
|
// else, on a stream watching nothing, is answered with the sentence that says
|
|
29
29
|
// how to make one arrive rather than with a timeout.
|
|
@@ -56,6 +56,8 @@ const KNOWN_TYPES = [
|
|
|
56
56
|
const MAX_WAIT_S = 55;
|
|
57
57
|
/** How long a first call gives the socket to reach its opening frame. */
|
|
58
58
|
const ATTACH_MS = 20_000;
|
|
59
|
+
/** Optional context must not hold an already-consumed event answer indefinitely. */
|
|
60
|
+
const RECONCILE_MS = 2_000;
|
|
59
61
|
/**
|
|
60
62
|
* Wait for a subscription to say something about itself, inside a budget.
|
|
61
63
|
*
|
|
@@ -93,20 +95,87 @@ async function attached(sub, cancel, deadline) {
|
|
|
93
95
|
* the caller already has, and a windows listing that 409s because the guest is
|
|
94
96
|
* busy must not turn a delivered event into a failed tool call.
|
|
95
97
|
*/
|
|
96
|
-
async function reconcile(session, id,
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
api.json('GET', P.computer(id)),
|
|
98
|
+
async function reconcile(session, id, cancel, deadline) {
|
|
99
|
+
const signal = AbortSignal.any([
|
|
100
|
+
AbortSignal.timeout(RECONCILE_MS),
|
|
101
|
+
...(cancel ? [cancel] : []),
|
|
102
|
+
...(deadline ? [deadline] : []),
|
|
102
103
|
]);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const state = {};
|
|
105
|
+
if (!signal.aborted) {
|
|
106
|
+
const api = session.api.with(signal);
|
|
107
|
+
let end = () => { };
|
|
108
|
+
const expired = new Promise((resolve) => {
|
|
109
|
+
end = resolve;
|
|
110
|
+
signal.addEventListener('abort', end, { once: true });
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
// Keep whichever half arrived before the deadline. The race also bounds
|
|
114
|
+
// embedders whose fetch implementation does not honour cancellation.
|
|
115
|
+
await Promise.race([
|
|
116
|
+
expired,
|
|
117
|
+
Promise.allSettled([
|
|
118
|
+
api.json('GET', P.computerAction(id, 'windows')).then((windows) => {
|
|
119
|
+
if (!signal.aborted)
|
|
120
|
+
state.windows_now = windows;
|
|
121
|
+
}),
|
|
122
|
+
api.json('GET', P.computer(id)).then((computer) => {
|
|
123
|
+
if (!signal.aborted) {
|
|
124
|
+
state.computer_now = withoutCredentials(unwrapComputer(computer));
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
]),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
signal.removeEventListener('abort', end);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!('windows_now' in state) || !('computer_now' in state)) {
|
|
135
|
+
state.reconciliation_note = reconciled(state).trim();
|
|
107
136
|
}
|
|
108
137
|
return state;
|
|
109
138
|
}
|
|
139
|
+
/** No continuity claim based solely on capabilities cached from an old socket. */
|
|
140
|
+
function connectionNote(sub, version) {
|
|
141
|
+
if (sub.state.status === 'stopped') {
|
|
142
|
+
return 'The event stream has stopped. These are the events buffered so far; call again for the remaining events and the reason it stopped.';
|
|
143
|
+
}
|
|
144
|
+
if (!sub.connected) {
|
|
145
|
+
return 'The event stream is reconnecting. These are the events buffered so far; more history may arrive on replay, so silence here is not an answer about what the computer did. Call again.';
|
|
146
|
+
}
|
|
147
|
+
if (version === undefined || version !== sub.connectionVersion) {
|
|
148
|
+
return 'The event stream was interrupted during this wait and has reopened. These are the events buffered so far; replayed events will be returned as they arrive. Call again to keep waiting.';
|
|
149
|
+
}
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The sentence that points at the reconciliation — naming only the keys it
|
|
154
|
+
* actually produced.
|
|
155
|
+
*
|
|
156
|
+
* `reconcile` assigns each key on a fulfilled read and swallows the rest, so
|
|
157
|
+
* `{}` is a real return value: the windows read 409s while the guest is busy,
|
|
158
|
+
* the computer read fails, or the caller's own signal aborts both. Naming
|
|
159
|
+
* `windows_now` and `computer_now` regardless sends a model looking for two
|
|
160
|
+
* keys that are not in the payload — a wasted turn on the one answer that is
|
|
161
|
+
* already admitting a hole, and the tool that has to be believed about holes.
|
|
162
|
+
*/
|
|
163
|
+
function reconciled(extras) {
|
|
164
|
+
const reads = [
|
|
165
|
+
{ key: 'windows_now', tool: 'list_windows' },
|
|
166
|
+
{ key: 'computer_now', tool: 'get_computer' },
|
|
167
|
+
];
|
|
168
|
+
const here = reads.filter((r) => r.key in extras).map((r) => r.key);
|
|
169
|
+
const absent = reads.filter((r) => !(r.key in extras)).map((r) => r.tool);
|
|
170
|
+
if (!here.length) {
|
|
171
|
+
return (' See lost for what is known about it. Where the computer actually stands could NOT be read ' +
|
|
172
|
+
'just now, so it is not in this answer — call list_windows and get_computer for what the ' +
|
|
173
|
+
'missing events would have told you.');
|
|
174
|
+
}
|
|
175
|
+
return (` See lost for what is known about it, and ${here.join(' and ')} for where the computer ` +
|
|
176
|
+
`actually stands, which is what the missing events would have told you.` +
|
|
177
|
+
(absent.length ? ` The other half could not be read just now — call ${absent[0]} for it.` : ''));
|
|
178
|
+
}
|
|
110
179
|
/**
|
|
111
180
|
* Why a computer is missing part of the guest half, in words.
|
|
112
181
|
*
|
|
@@ -143,7 +212,7 @@ function guestHalf(can) {
|
|
|
143
212
|
// too, so a computer reporting it plainly has one.
|
|
144
213
|
return ('It does report the desktop half, which needs the same terminal channel a file watch runs ' +
|
|
145
214
|
'over — so the channel is there and it is the file watch that is missing. The host holding ' +
|
|
146
|
-
'this computer predates them (
|
|
215
|
+
'this computer predates them (OPL-3927), and there is nothing to do about that ' +
|
|
147
216
|
'from here.');
|
|
148
217
|
}
|
|
149
218
|
if (files && desktop) {
|
|
@@ -193,7 +262,7 @@ function name(ev) {
|
|
|
193
262
|
return detail ? `${ev.type} (${detail})` : ev.type;
|
|
194
263
|
}
|
|
195
264
|
/** The body these tools answer with, minus the keys there is nothing to say about. */
|
|
196
|
-
function body(id, d, watching) {
|
|
265
|
+
function body(id, d, sub, watching) {
|
|
197
266
|
const out = { computer: id, events: d.events, cursor: d.cursor };
|
|
198
267
|
if (d.more)
|
|
199
268
|
out.more_waiting = d.more;
|
|
@@ -204,14 +273,22 @@ function body(id, d, watching) {
|
|
|
204
273
|
// another call can evict it, and a reconnect can find it disarmed. A reader
|
|
205
274
|
// that had to remember which of four trees was live from a call several turns
|
|
206
275
|
// ago is a reader that will get it wrong.
|
|
207
|
-
if (watching?.length)
|
|
208
|
-
out.watching = watching;
|
|
276
|
+
if (watching?.length) {
|
|
277
|
+
out.watching = watching.map((watch) => ({ ...watch, armed: watch.armed && sub.connected }));
|
|
278
|
+
}
|
|
209
279
|
// Only where it is news. `can_emit` is what stops a model waiting for
|
|
210
280
|
// something this machine will never produce, and the opening frame is the one
|
|
211
281
|
// place that answer exists — but repeating it on every poll would be a field
|
|
212
282
|
// that means nothing on the ninety-ninth call.
|
|
283
|
+
//
|
|
284
|
+
// Read from the LIVE list rather than from the greeting that seeded it. A
|
|
285
|
+
// `capabilities` frame replaces what `hello` advertised and goes both ways —
|
|
286
|
+
// a guest that turns out to have no watcher withdraws the half `hello`
|
|
287
|
+
// promised — and one landing between the opening frame and this subscription's
|
|
288
|
+
// first read would otherwise publish, exactly once and never again, a
|
|
289
|
+
// vocabulary that contradicts the one every refusal path here already acts on.
|
|
213
290
|
if (d.attached && d.hello) {
|
|
214
|
-
out.can_emit = d.hello.events;
|
|
291
|
+
out.can_emit = sub.eventTypes ?? d.hello.events;
|
|
215
292
|
if (d.hello.windows)
|
|
216
293
|
out.windows_on_attach = d.hello.windows;
|
|
217
294
|
}
|
|
@@ -255,8 +332,22 @@ function stopped(session, id, reason, sub, read) {
|
|
|
255
332
|
const d = sub.read(read);
|
|
256
333
|
const n = d.events.length;
|
|
257
334
|
const drained = !d.more;
|
|
335
|
+
// The sub this call drained, named, so the drop cannot take a replacement.
|
|
336
|
+
// Two calls on one computer overlap by design here, and a second one holding
|
|
337
|
+
// a handle this one has already dropped would otherwise close the fresh
|
|
338
|
+
// stream opened in between.
|
|
258
339
|
if (drained)
|
|
259
|
-
session.events.drop(id, reason, true);
|
|
340
|
+
session.events.drop(id, reason, true, sub);
|
|
341
|
+
// KNOWN GAP on the `!drained` side of that same overlap: if this call is the
|
|
342
|
+
// one holding the older handle, "call again for them" is a promise this
|
|
343
|
+
// server cannot keep. The next call resolves through `EventHub.open()`, which
|
|
344
|
+
// returns the subscription now in `#subs` — the replacement — and the events
|
|
345
|
+
// still in THIS one's ring are unreachable from that moment. Reaching it
|
|
346
|
+
// needs two calls overlapping on one computer with the older one parked
|
|
347
|
+
// mid-wait, which is why it has no test. Anything written here has to be
|
|
348
|
+
// narrower than "a fresh stream is running, there is nothing to fix": that
|
|
349
|
+
// sentence was tried, and it both dropped the disclosure above and reported a
|
|
350
|
+
// replacement that had itself stopped as healthy.
|
|
260
351
|
const held = n
|
|
261
352
|
? `${n} event${n === 1 ? '' : 's'} had already arrived before it stopped and ${n === 1 ? 'is' : 'are'} ` +
|
|
262
353
|
(drained
|
|
@@ -275,7 +366,7 @@ function stopped(session, id, reason, sub, read) {
|
|
|
275
366
|
// Without the watch set, deliberately. This stream has stopped, so a tree
|
|
276
367
|
// it was carrying is a tree nothing is watching — and `watching` reports
|
|
277
368
|
// `armed` from the last opening frame, which would read as live.
|
|
278
|
-
n || d.loss ? body(id, d) : undefined);
|
|
369
|
+
n || d.loss ? body(id, d, sub) : undefined);
|
|
279
370
|
}
|
|
280
371
|
/**
|
|
281
372
|
* A stream that has not yet got as far as its opening frame.
|
|
@@ -357,6 +448,21 @@ export const registerEvents = (server, session) => {
|
|
|
357
448
|
// had already taken — the same defect one tool over, with a ring in this
|
|
358
449
|
// session instead of a cursor in the guest. Nothing is created and nothing
|
|
359
450
|
// is destroyed, which is what made the annotation look right.
|
|
451
|
+
// No readOnlyHint, for the reason above — and BOTH other flags set,
|
|
452
|
+
// because the spec's defaults are wrong in one direction and unstated in
|
|
453
|
+
// the other the moment readOnlyHint is absent (OPL-4516).
|
|
454
|
+
//
|
|
455
|
+
// destructiveHint would default to TRUE, so without it this asks a host
|
|
456
|
+
// whether it may perform DESTRUCTIVE UPDATES in order to read.
|
|
457
|
+
// `cursor_position` and `read_file` set it for that reason; these were not
|
|
458
|
+
// revisited with them.
|
|
459
|
+
//
|
|
460
|
+
// idempotentHint is FALSE, which is the difference from those two, and it
|
|
461
|
+
// is set rather than left to its default for the same reason as the other:
|
|
462
|
+
// a host reading an absent flag as "unknown, safe to retry" would retry a
|
|
463
|
+
// timed-out poll and silently drop what the first attempt consumed, which
|
|
464
|
+
// is the harm the paragraph above it is about.
|
|
465
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
360
466
|
}, ({ computer_id, types, pid, timeout_s, since, limit }, extra) => guarded(async () => {
|
|
361
467
|
const id = session.resolve(computer_id);
|
|
362
468
|
const sub = session.events.open(id);
|
|
@@ -374,14 +480,16 @@ export const registerEvents = (server, session) => {
|
|
|
374
480
|
// point in this call, and a `state` captured before the wait is a
|
|
375
481
|
// statement about a moment that has passed.
|
|
376
482
|
const opening = sub.state;
|
|
483
|
+
// A cancelled call must not drain a stopped subscription's final
|
|
484
|
+
// buffered delivery into an answer its caller can no longer receive.
|
|
485
|
+
if (extra.signal?.aborted)
|
|
486
|
+
return cancelledDuringAttach(id);
|
|
377
487
|
if (opening.status === 'stopped') {
|
|
378
488
|
return stopped(session, id, opening.reason, sub, { since, limit });
|
|
379
489
|
}
|
|
380
|
-
// A caller who hung up is not a stream that failed to open.
|
|
381
|
-
if (extra.signal?.aborted)
|
|
382
|
-
return cancelledDuringAttach(id);
|
|
383
490
|
if (!sub.eventTypes)
|
|
384
491
|
return unattached(id, Date.now() - started);
|
|
492
|
+
const connection = sub.connected ? sub.connectionVersion : undefined;
|
|
385
493
|
// A `pid` on its own means the exit of THAT command. Without this the
|
|
386
494
|
// filter below reads "anything that is not a process.exited passes",
|
|
387
495
|
// and `wait_for_event({pid: 99})` — which is how the argument's own
|
|
@@ -504,12 +612,13 @@ export const registerEvents = (server, session) => {
|
|
|
504
612
|
const waited = Math.round((Date.now() - started) / 1000);
|
|
505
613
|
if (hit === undefined) {
|
|
506
614
|
if (extra.signal?.aborted) {
|
|
507
|
-
return refused(`Cancelled while waiting on ${id}.
|
|
508
|
-
|
|
615
|
+
return refused(`Cancelled while waiting on ${id}. Buffered events remain available on the next call. ` +
|
|
616
|
+
(connectionNote(sub, connection) || 'The stream is still open and buffering.'));
|
|
617
|
+
}
|
|
618
|
+
const stoppedWhileWaiting = sub.state;
|
|
619
|
+
if (stoppedWhileWaiting.status === 'stopped') {
|
|
620
|
+
return stopped(session, id, stoppedWhileWaiting.reason, sub, { since, limit });
|
|
509
621
|
}
|
|
510
|
-
const now = sub.state;
|
|
511
|
-
if (now.status === 'stopped')
|
|
512
|
-
return stopped(session, id, now.reason, sub, { since, limit });
|
|
513
622
|
// Asked again, because the wait that just ended is long enough for a
|
|
514
623
|
// `capabilities` frame to have arrived inside it. A withdrawal that
|
|
515
624
|
// happened while parked is still the reason nothing came, and saying
|
|
@@ -525,8 +634,19 @@ export const registerEvents = (server, session) => {
|
|
|
525
634
|
// which is an answer, and it comes with the cursor that makes asking
|
|
526
635
|
// again cost nothing. Reporting it as a failure would teach a model to
|
|
527
636
|
// stop asking — back to the screenshot loop this tool exists to end.
|
|
637
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit });
|
|
638
|
+
const recovered = needsReconciliation
|
|
639
|
+
? await reconcile(session, id, extra.signal, deadline)
|
|
640
|
+
: {};
|
|
641
|
+
if (extra.signal?.aborted) {
|
|
642
|
+
return refused(`Cancelled while waiting on ${id}. Buffered events remain available on the next call. ` +
|
|
643
|
+
(connectionNote(sub, connection) || 'The stream is still open and buffering.'));
|
|
644
|
+
}
|
|
645
|
+
const now = sub.state;
|
|
646
|
+
if (now.status === 'stopped')
|
|
647
|
+
return stopped(session, id, now.reason, sub, { since, limit });
|
|
528
648
|
const d = sub.read({ since, limit });
|
|
529
|
-
const extras = d.loss ?
|
|
649
|
+
const extras = d.loss ? recovered : {};
|
|
530
650
|
// What did NOT match still happened, and this read has just handed it
|
|
531
651
|
// over — so the sentence has to name it. Saying "nothing happened"
|
|
532
652
|
// over a payload holding three events is the one thing a model must
|
|
@@ -537,6 +657,11 @@ export const registerEvents = (server, session) => {
|
|
|
537
657
|
? ` ${d.events.length} other event${d.events.length === 1 ? '' : 's'} did happen and ` +
|
|
538
658
|
`${d.events.length === 1 ? 'is' : 'are'} below.`
|
|
539
659
|
: '';
|
|
660
|
+
const interrupted = connectionNote(sub, connection);
|
|
661
|
+
if (interrupted || d.loss) {
|
|
662
|
+
return said(`${interrupted || `No matching event is buffered on ${id}.`}${others}` +
|
|
663
|
+
(d.loss ? reconciled(extras) : ''), { ...body(id, d, sub, sub.watching), ...extras });
|
|
664
|
+
}
|
|
540
665
|
return said(`Nothing ${wanted ? `matching ${[...wanted].join(' or ')} ` : ''}happened on ${id} in ` +
|
|
541
666
|
`${timeout_s}s.${others} This server kept listening the whole time and is still ` +
|
|
542
667
|
`listening — nothing was missed and nothing is being missed now. Call again to ` +
|
|
@@ -545,20 +670,35 @@ export const registerEvents = (server, session) => {
|
|
|
545
670
|
// wait was real: what must not happen is a model reading "nothing
|
|
546
671
|
// happened" as covering a type nothing was ever going to send.
|
|
547
672
|
(unwatched() ? ` One thing was NOT being waited for: ${nominate()}` : '') +
|
|
548
|
-
(d.loss ? ' Some events were lost before they could be read — see lost.' : ''), { ...body(id, d, sub.watching), ...extras });
|
|
673
|
+
(d.loss ? ' Some events were lost before they could be read — see lost.' : ''), { ...body(id, d, sub, sub.watching), ...extras });
|
|
674
|
+
}
|
|
675
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit, through: hit });
|
|
676
|
+
const recovered = needsReconciliation
|
|
677
|
+
? await reconcile(session, id, extra.signal, deadline)
|
|
678
|
+
: {};
|
|
679
|
+
if (extra.signal?.aborted) {
|
|
680
|
+
return refused(`Cancelled while waiting on ${id}. Buffered events remain available on the next call. ` +
|
|
681
|
+
(connectionNote(sub, connection) || 'The stream is still open and buffering.'));
|
|
549
682
|
}
|
|
550
683
|
const d = sub.read({ since, limit, through: hit });
|
|
551
684
|
const last = d.events[d.events.length - 1];
|
|
552
|
-
const extras = d.loss ?
|
|
685
|
+
const extras = d.loss ? recovered : {};
|
|
553
686
|
const before = d.events.length - 1;
|
|
554
687
|
// Empty when another call on this computer consumed the matched event
|
|
555
688
|
// first: the ring is one buffer with one delivered cursor, and two
|
|
556
689
|
// overlapping waits can both match before either reads. Naming an event
|
|
557
690
|
// over an empty list would be an event the caller is never shown.
|
|
558
691
|
if (!last) {
|
|
692
|
+
if (d.loss) {
|
|
693
|
+
return said(`An event on ${id} matched this wait, but the matching event is no longer in this ` +
|
|
694
|
+
`call's delivery. Some buffered history was lost before it could be read, so do ` +
|
|
695
|
+
`not assume another reader safely received the match.` +
|
|
696
|
+
reconciled(extras) +
|
|
697
|
+
` Call again for the events that remain buffered.`, { ...body(id, d, sub, sub.watching), ...extras });
|
|
698
|
+
}
|
|
559
699
|
return said(`Something happened on ${id} and another call on this computer was handed it before ` +
|
|
560
700
|
`this one could read it — the events are in that call's answer, not below. Nothing ` +
|
|
561
|
-
`is lost; look there, or call again for whatever comes next.`, { ...body(id, d, sub.watching), ...extras });
|
|
701
|
+
`is lost; look there, or call again for whatever comes next.`, { ...body(id, d, sub, sub.watching), ...extras });
|
|
562
702
|
}
|
|
563
703
|
return said(`${name(last)} on ${id} after ${waited}s` +
|
|
564
704
|
(before > 0 ? `, and ${before} before it` : '') +
|
|
@@ -568,7 +708,7 @@ export const registerEvents = (server, session) => {
|
|
|
568
708
|
'and computer.ready is announced once per desktop session — so the real event had ' +
|
|
569
709
|
'happened before there was anything here to hear it, and waiting for it would have ' +
|
|
570
710
|
'waited forever.'
|
|
571
|
-
: ''), { ...body(id, d, sub.watching), ...extras });
|
|
711
|
+
: ''), { ...body(id, d, sub, sub.watching), ...extras });
|
|
572
712
|
}));
|
|
573
713
|
/**
|
|
574
714
|
* What one `file.changed` says, in a sentence.
|
|
@@ -691,6 +831,21 @@ export const registerEvents = (server, session) => {
|
|
|
691
831
|
// tree out of the watch set. A client treating the hint as licence to
|
|
692
832
|
// retry a call that timed out would silently drop events and, on the
|
|
693
833
|
// fifth distinct path, silently stop watching the first.
|
|
834
|
+
// No readOnlyHint, for the two reasons above — and destructiveHint TRUE,
|
|
835
|
+
// which is where this parts company with the three consuming reads beside
|
|
836
|
+
// it (OPL-4516).
|
|
837
|
+
//
|
|
838
|
+
// The spec reads `false` as "performs only additive updates", and this does
|
|
839
|
+
// not: a nomination past MAX_WATCHES evicts the least recent tree and
|
|
840
|
+
// deletes its armed, lost and host state, so a fifth call ends event
|
|
841
|
+
// delivery on the first with nothing said. Claiming additive-only would be
|
|
842
|
+
// the same false all-clear the missing readOnlyHint already avoids — a host
|
|
843
|
+
// that auto-approves non-destructive tools would let the model do it with
|
|
844
|
+
// nobody asked. Same value the absent annotation defaulted to, said out
|
|
845
|
+
// loud so it reads as a decision rather than an omission.
|
|
846
|
+
//
|
|
847
|
+
// idempotentHint false for the reason the others set it: this consumes.
|
|
848
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
694
849
|
}, ({ computer_id, path, timeout_s, since, limit }, extra) => guarded(async () => {
|
|
695
850
|
const id = session.resolve(computer_id);
|
|
696
851
|
// Before anything is opened. A path this host will not accept is a 400
|
|
@@ -715,11 +870,11 @@ export const registerEvents = (server, session) => {
|
|
|
715
870
|
const started = Date.now();
|
|
716
871
|
await attached(sub, extra.signal, deadline);
|
|
717
872
|
const opening = sub.state;
|
|
873
|
+
if (extra.signal?.aborted)
|
|
874
|
+
return cancelledDuringAttach(id);
|
|
718
875
|
if (opening.status === 'stopped') {
|
|
719
876
|
return stopped(session, id, opening.reason, sub, { since, limit });
|
|
720
877
|
}
|
|
721
|
-
if (extra.signal?.aborted)
|
|
722
|
-
return cancelledDuringAttach(id);
|
|
723
878
|
if (!sub.eventTypes)
|
|
724
879
|
return unattached(id, Date.now() - started);
|
|
725
880
|
// Asked before the tree is nominated, because a computer that cannot
|
|
@@ -748,11 +903,11 @@ export const registerEvents = (server, session) => {
|
|
|
748
903
|
if (!sub.nominationLive(root)) {
|
|
749
904
|
await sub.nominated(root, deadline, extra.signal);
|
|
750
905
|
const after = sub.state;
|
|
906
|
+
if (extra.signal?.aborted)
|
|
907
|
+
return cancelledWhileArming(id, root);
|
|
751
908
|
if (after.status === 'stopped') {
|
|
752
909
|
return stopped(session, id, after.reason, sub, { since, limit });
|
|
753
910
|
}
|
|
754
|
-
if (extra.signal?.aborted)
|
|
755
|
-
return cancelledWhileArming(id, root);
|
|
756
911
|
// ASKED FIRST, all four of them, because each is an answer and the
|
|
757
912
|
// timeout below is only the absence of one. A tree the host would not
|
|
758
913
|
// carry, one another call evicted, or a computer that has stopped
|
|
@@ -777,7 +932,7 @@ export const registerEvents = (server, session) => {
|
|
|
777
932
|
if (sub.watchesHonoured === false) {
|
|
778
933
|
return refused(`${id} opened its event stream but said nothing about ${root}, so this server cannot ` +
|
|
779
934
|
`tell whether the tree is being watched — and a wait would be a wait on silence. The ` +
|
|
780
|
-
`host holding this computer may predate file watches (
|
|
935
|
+
`host holding this computer may predate file watches (OPL-3927). Use exec ` +
|
|
781
936
|
`to look at the directory instead.` +
|
|
782
937
|
evicted);
|
|
783
938
|
}
|
|
@@ -786,10 +941,10 @@ export const registerEvents = (server, session) => {
|
|
|
786
941
|
await sub.armedWait(root, deadline, extra.signal);
|
|
787
942
|
if (!sub.isArmed(root)) {
|
|
788
943
|
const now = sub.state;
|
|
789
|
-
if (now.status === 'stopped')
|
|
790
|
-
return stopped(session, id, now.reason, sub, { since, limit });
|
|
791
944
|
if (extra.signal?.aborted)
|
|
792
945
|
return cancelledWhileArming(id, wire);
|
|
946
|
+
if (now.status === 'stopped')
|
|
947
|
+
return stopped(session, id, now.reason, sub, { since, limit });
|
|
793
948
|
// Asked before the sentence below, all four of them, because that
|
|
794
949
|
// sentence says the nomination stands and this tree is still coming
|
|
795
950
|
// up — and every word of it is false when the tree has been evicted
|
|
@@ -817,6 +972,7 @@ export const registerEvents = (server, session) => {
|
|
|
817
972
|
// whatever happened in between was never reported and the tree has to
|
|
818
973
|
// be re-read.
|
|
819
974
|
const generation = sub.armGeneration(root);
|
|
975
|
+
const connection = sub.connected ? sub.connectionVersion : undefined;
|
|
820
976
|
// Said once, on the first answer about a tree this subscription
|
|
821
977
|
// inherited from one that went away. A tree is watched by the
|
|
822
978
|
// CONNECTION, so the idle reap that took the previous subscription also
|
|
@@ -839,6 +995,49 @@ export const registerEvents = (server, session) => {
|
|
|
839
995
|
`a watch lives on the connection. Anything that changed in that window was never ` +
|
|
840
996
|
`reported and cannot be. Re-read the directory with exec if it matters.`
|
|
841
997
|
: '';
|
|
998
|
+
// Kept by the subscription rather than inferred from this call's
|
|
999
|
+
// generation snapshot. A re-arm can arrive while no tool call exists,
|
|
1000
|
+
// and snapshotting after it would otherwise erase the only evidence of
|
|
1001
|
+
// the unreported window. Delete the marker here, where its explanation
|
|
1002
|
+
// is rendered, so unrelated reads and cancelled waits cannot consume it.
|
|
1003
|
+
const rearmNotice = () => `The watch on ${wire} was re-armed after an interruption — a stop and a start, a ` +
|
|
1004
|
+
`guest reboot, a broker replaced. Reporting starts again HERE, and nothing that ` +
|
|
1005
|
+
`happened to the tree while it was down was reported or ever will be. Re-read the ` +
|
|
1006
|
+
`directory with exec if that window matters, then call again to keep waiting.`;
|
|
1007
|
+
const cancelledWait = () => refused(`Cancelled while waiting on ${wire}. Nothing was missed by the cancellation — this ` +
|
|
1008
|
+
`server holds the stream and its buffer between calls — but nothing was checked ` +
|
|
1009
|
+
`about the watch either, so call again for an answer about the tree.` +
|
|
1010
|
+
interrupted());
|
|
1011
|
+
if (sub.hasUndisclosedRearm(root)) {
|
|
1012
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit });
|
|
1013
|
+
const recovered = needsReconciliation
|
|
1014
|
+
? await reconcile(session, id, extra.signal, deadline)
|
|
1015
|
+
: {};
|
|
1016
|
+
if (extra.signal?.aborted)
|
|
1017
|
+
return cancelledWait();
|
|
1018
|
+
const after = sub.state;
|
|
1019
|
+
if (after.status === 'stopped')
|
|
1020
|
+
return stopped(session, id, after.reason, sub, { since, limit });
|
|
1021
|
+
const d = sub.read({ since, limit });
|
|
1022
|
+
const extras = d.loss ? recovered : {};
|
|
1023
|
+
const answer = { ...body(id, d, sub, sub.watching), watch: wire, ...extras };
|
|
1024
|
+
const why = settled(sub, id, root, wire, () => interrupted() + evicted, answer);
|
|
1025
|
+
if (why)
|
|
1026
|
+
return why;
|
|
1027
|
+
// A competing same-tree wait may have explained it while reconcile
|
|
1028
|
+
// was in flight. Only the call that claims the marker repeats it.
|
|
1029
|
+
if (sub.takeUndisclosedRearm(root)) {
|
|
1030
|
+
return said(rearmNotice() + interrupted() + renamed + evicted, answer);
|
|
1031
|
+
}
|
|
1032
|
+
return said(`Another call on ${id} reported the watch interruption while this call was ` +
|
|
1033
|
+
`reconciling it. This call's delivery is below.` +
|
|
1034
|
+
(d.loss
|
|
1035
|
+
? ` Some buffered history was lost before it could be read.${reconciled(extras)}`
|
|
1036
|
+
: ` No buffered event was dropped.`) +
|
|
1037
|
+
interrupted() +
|
|
1038
|
+
renamed +
|
|
1039
|
+
evicted, answer);
|
|
1040
|
+
}
|
|
842
1041
|
// When the waiting actually started, which is not when the call did.
|
|
843
1042
|
// `timeout_s` bounds the whole call — it has to, because a client
|
|
844
1043
|
// cancels a request that outlives its own timeout — so a call that
|
|
@@ -892,38 +1091,45 @@ export const registerEvents = (server, session) => {
|
|
|
892
1091
|
// call did not check and which a cancel racing an eviction or a
|
|
893
1092
|
// shed makes false. What IS true is the part that matters: the
|
|
894
1093
|
// buffer is on this side and nothing in it went anywhere.
|
|
895
|
-
return
|
|
896
|
-
`server holds the stream and its buffer between calls — but nothing was checked ` +
|
|
897
|
-
`about the watch either, so call again for an answer about the tree.` +
|
|
898
|
-
interrupted());
|
|
1094
|
+
return cancelledWait();
|
|
899
1095
|
}
|
|
900
1096
|
const now = sub.state;
|
|
901
1097
|
if (now.status === 'stopped')
|
|
902
1098
|
return stopped(session, id, now.reason, sub, { since, limit });
|
|
1099
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit });
|
|
1100
|
+
const recovered = needsReconciliation
|
|
1101
|
+
? await reconcile(session, id, extra.signal, deadline)
|
|
1102
|
+
: {};
|
|
1103
|
+
if (extra.signal?.aborted)
|
|
1104
|
+
return cancelledWait();
|
|
1105
|
+
const after = sub.state;
|
|
1106
|
+
if (after.status === 'stopped')
|
|
1107
|
+
return stopped(session, id, after.reason, sub, { since, limit });
|
|
903
1108
|
const d = sub.read({ since, limit });
|
|
904
|
-
const extras = d.loss ?
|
|
905
|
-
const answer = { ...body(id, d, sub.watching), watch: wire, ...extras };
|
|
1109
|
+
const extras = d.loss ? recovered : {};
|
|
1110
|
+
const answer = { ...body(id, d, sub, sub.watching), watch: wire, ...extras };
|
|
906
1111
|
// THESE FIRST, and in this order, because each would otherwise be
|
|
907
|
-
// described as something else. Evicting a tree
|
|
908
|
-
// generation
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
1112
|
+
// described as something else. Evicting a tree drops its arm
|
|
1113
|
+
// generation along with the rest of its metadata, so the generation
|
|
1114
|
+
// branch below reads an eviction as a change and would call it a
|
|
1115
|
+
// re-arm — inviting the caller to go on waiting on a tree nothing
|
|
1116
|
+
// nominates. Membership is what that actually is, and `settled` is
|
|
1117
|
+
// where it is asked. A tree the host would not carry is missing from
|
|
912
1118
|
// the set for a quite different reason, which is why the refusal is
|
|
913
1119
|
// asked about ahead of the membership.
|
|
1120
|
+
//
|
|
1121
|
+
// Generations stay unambiguous across a re-nomination because they
|
|
1122
|
+
// come from a subscription-wide counter: a tree that comes back gets
|
|
1123
|
+
// a number no waiter can be holding, rather than restarting at one.
|
|
914
1124
|
const why = settled(sub, id, root, wire, () => interrupted() + evicted, {
|
|
915
|
-
...body(id, d, sub.watching),
|
|
1125
|
+
...body(id, d, sub, sub.watching),
|
|
916
1126
|
...extras,
|
|
917
1127
|
});
|
|
918
1128
|
if (why)
|
|
919
1129
|
return why;
|
|
920
1130
|
if (sub.armGeneration(root) !== generation) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
`happened to the tree while it was down was reported or ever will be. Re-read the ` +
|
|
924
|
-
`directory with exec if that window matters, then call again to keep waiting.` +
|
|
925
|
-
interrupted() +
|
|
926
|
-
evicted, answer);
|
|
1131
|
+
sub.takeUndisclosedRearm(root);
|
|
1132
|
+
return said(rearmNotice() + interrupted() + evicted, answer);
|
|
927
1133
|
}
|
|
928
1134
|
if (!sub.isArmed(root)) {
|
|
929
1135
|
return refused(`${wire} on ${id} stopped being watched while this call was waiting, so NOTHING can ` +
|
|
@@ -959,6 +1165,15 @@ export const registerEvents = (server, session) => {
|
|
|
959
1165
|
interrupted() +
|
|
960
1166
|
evicted, answer);
|
|
961
1167
|
}
|
|
1168
|
+
const connectionChanged = connectionNote(sub, connection);
|
|
1169
|
+
if (connectionChanged || d.loss) {
|
|
1170
|
+
return said(`${connectionChanged || `No file change is buffered for ${wire}.`} ` +
|
|
1171
|
+
`A quiet directory cannot be confirmed for this whole wait.` +
|
|
1172
|
+
(d.loss ? reconciled(extras) : '') +
|
|
1173
|
+
interrupted() +
|
|
1174
|
+
renamed +
|
|
1175
|
+
evicted, answer);
|
|
1176
|
+
}
|
|
962
1177
|
// NOT an error, for the reason wait_for_event's timeout is not: the
|
|
963
1178
|
// tree was being watched for the whole of it, so "nothing changed" is
|
|
964
1179
|
// an answer rather than an absence of one. This is the sentence the
|
|
@@ -977,19 +1192,50 @@ export const registerEvents = (server, session) => {
|
|
|
977
1192
|
evicted +
|
|
978
1193
|
(d.loss ? ' Some events were lost before they could be read — see lost.' : ''), answer);
|
|
979
1194
|
}
|
|
1195
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit, through: hit });
|
|
1196
|
+
const recovered = needsReconciliation
|
|
1197
|
+
? await reconcile(session, id, extra.signal, deadline)
|
|
1198
|
+
: {};
|
|
1199
|
+
if (extra.signal?.aborted)
|
|
1200
|
+
return cancelledWait();
|
|
980
1201
|
const d = sub.read({ since, limit, through: hit });
|
|
981
1202
|
const last = d.events[d.events.length - 1];
|
|
982
|
-
const extras = d.loss ?
|
|
1203
|
+
const extras = d.loss ? recovered : {};
|
|
983
1204
|
const earlier = d.events.length - 1;
|
|
984
1205
|
// The one `lost` that is not a re-read: it says the tree is not being
|
|
985
1206
|
// watched, so it is the request failing rather than the watch reporting.
|
|
986
1207
|
// It arrives as an ordinary file.changed and would otherwise be
|
|
987
1208
|
// described by changeLine, which has nothing useful to say about it.
|
|
988
1209
|
if ((last?.data?.lost ?? '') === 'unwatchable') {
|
|
989
|
-
|
|
1210
|
+
// Only when the tree is not being watched NOW. An `unwatchable` sits
|
|
1211
|
+
// in the ring until something reads it, and a caller who created the
|
|
1212
|
+
// directory and called again re-nominates: the guest answers the new
|
|
1213
|
+
// nomination with an arm, and this marker — already reported to the
|
|
1214
|
+
// call that refused on it — is then describing a watch that has since
|
|
1215
|
+
// been replaced. Handed back as the answer it refuses a second time
|
|
1216
|
+
// over a directory that plainly exists, which is the same false
|
|
1217
|
+
// "not there yet" the re-nomination exists to clear, one call later.
|
|
1218
|
+
//
|
|
1219
|
+
// `isArmed` is what tells the two apart, and it is exact rather than a
|
|
1220
|
+
// proxy: `unwatchable` is the one `lost` that disarms, so a tree still
|
|
1221
|
+
// carrying this marker as its live state reads false here, while one
|
|
1222
|
+
// the guest has since armed reads true. Said with the re-arm's own
|
|
1223
|
+
// sentence, because that is what happened and the window is genuinely
|
|
1224
|
+
// unreported either way.
|
|
1225
|
+
if (!sub.isArmed(root)) {
|
|
1226
|
+
return refused(`${unwatchable(wire)} It was being watched until now; from here it is not.` +
|
|
1227
|
+
interrupted() +
|
|
1228
|
+
renamed +
|
|
1229
|
+
evicted, { ...body(id, d, sub, sub.watching), watch: wire, ...extras });
|
|
1230
|
+
}
|
|
1231
|
+
return said(`${wire} on ${id} was not watchable when this stream last asked — it was reported ` +
|
|
1232
|
+
`missing or unreadable — and it is being watched now: the nomination was retried and ` +
|
|
1233
|
+
`the guest armed it. Reporting starts HERE, so nothing that happened under it before ` +
|
|
1234
|
+
`this was reported or ever will be. Re-read the directory with exec if that window ` +
|
|
1235
|
+
`matters, then call again to wait for what comes next.` +
|
|
990
1236
|
interrupted() +
|
|
991
1237
|
renamed +
|
|
992
|
-
evicted, { ...body(id, d, sub.watching), watch: wire, ...extras });
|
|
1238
|
+
evicted, { ...body(id, d, sub, sub.watching), watch: wire, ...extras });
|
|
993
1239
|
}
|
|
994
1240
|
// A `through` read can come back empty when another call on this
|
|
995
1241
|
// computer consumed the matched event first — the ring is one buffer
|
|
@@ -997,19 +1243,29 @@ export const registerEvents = (server, session) => {
|
|
|
997
1243
|
// before either reads. Announcing "a change" over an empty list would be
|
|
998
1244
|
// a change the caller is never shown.
|
|
999
1245
|
if (!last) {
|
|
1246
|
+
if (d.loss) {
|
|
1247
|
+
return said(`A file change under ${wire} on ${id} matched this wait, but the matching event is ` +
|
|
1248
|
+
`no longer in this call's delivery. Some buffered history was lost before it ` +
|
|
1249
|
+
`could be read, so do not assume another reader safely received the match.` +
|
|
1250
|
+
reconciled(extras) +
|
|
1251
|
+
` Re-read the directory with exec, then call again for whatever comes next.` +
|
|
1252
|
+
interrupted() +
|
|
1253
|
+
renamed +
|
|
1254
|
+
evicted, { ...body(id, d, sub, sub.watching), watch: wire, ...extras });
|
|
1255
|
+
}
|
|
1000
1256
|
return said(`Something changed under ${wire} on ${id}, and another call on this computer was ` +
|
|
1001
1257
|
`handed it before this one could read it — the events are in that call's answer, ` +
|
|
1002
1258
|
`not below. Nothing is lost; look there, or call again for whatever comes next.` +
|
|
1003
1259
|
interrupted() +
|
|
1004
1260
|
renamed +
|
|
1005
|
-
evicted, { ...body(id, d, sub.watching), watch: wire, ...extras });
|
|
1261
|
+
evicted, { ...body(id, d, sub, sub.watching), watch: wire, ...extras });
|
|
1006
1262
|
}
|
|
1007
1263
|
return said(`${changeLine(last, id)} after ${waited}s` +
|
|
1008
1264
|
(earlier > 0 ? `, and ${earlier} event${earlier === 1 ? '' : 's'} before it` : '') +
|
|
1009
1265
|
'.' +
|
|
1010
1266
|
interrupted() +
|
|
1011
1267
|
renamed +
|
|
1012
|
-
evicted, { ...body(id, d, sub.watching), watch: wire, ...extras });
|
|
1268
|
+
evicted, { ...body(id, d, sub, sub.watching), watch: wire, ...extras });
|
|
1013
1269
|
}));
|
|
1014
1270
|
server.registerTool('poll_events', {
|
|
1015
1271
|
title: 'Read what has happened',
|
|
@@ -1036,6 +1292,21 @@ export const registerEvents = (server, session) => {
|
|
|
1036
1292
|
// had already taken — the same defect one tool over, with a ring in this
|
|
1037
1293
|
// session instead of a cursor in the guest. Nothing is created and nothing
|
|
1038
1294
|
// is destroyed, which is what made the annotation look right.
|
|
1295
|
+
// No readOnlyHint, for the reason above — and BOTH other flags set,
|
|
1296
|
+
// because the spec's defaults are wrong in one direction and unstated in
|
|
1297
|
+
// the other the moment readOnlyHint is absent (OPL-4516).
|
|
1298
|
+
//
|
|
1299
|
+
// destructiveHint would default to TRUE, so without it this asks a host
|
|
1300
|
+
// whether it may perform DESTRUCTIVE UPDATES in order to read.
|
|
1301
|
+
// `cursor_position` and `read_file` set it for that reason; these were not
|
|
1302
|
+
// revisited with them.
|
|
1303
|
+
//
|
|
1304
|
+
// idempotentHint is FALSE, which is the difference from those two, and it
|
|
1305
|
+
// is set rather than left to its default for the same reason as the other:
|
|
1306
|
+
// a host reading an absent flag as "unknown, safe to retry" would retry a
|
|
1307
|
+
// timed-out poll and silently drop what the first attempt consumed, which
|
|
1308
|
+
// is the harm the paragraph above it is about.
|
|
1309
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
1039
1310
|
}, ({ computer_id, since, limit }, extra) => guarded(async () => {
|
|
1040
1311
|
const id = session.resolve(computer_id);
|
|
1041
1312
|
const sub = session.events.open(id);
|
|
@@ -1046,15 +1317,34 @@ export const registerEvents = (server, session) => {
|
|
|
1046
1317
|
const started = Date.now();
|
|
1047
1318
|
await attached(sub, extra.signal);
|
|
1048
1319
|
const state = sub.state;
|
|
1320
|
+
if (extra.signal?.aborted)
|
|
1321
|
+
return cancelledDuringAttach(id);
|
|
1049
1322
|
if (state.status === 'stopped') {
|
|
1050
1323
|
return stopped(session, id, state.reason, sub, { since, limit });
|
|
1051
1324
|
}
|
|
1052
|
-
if (extra.signal?.aborted)
|
|
1053
|
-
return cancelledDuringAttach(id);
|
|
1054
1325
|
if (!sub.eventTypes)
|
|
1055
1326
|
return unattached(id, Date.now() - started);
|
|
1327
|
+
const connection = sub.connected ? sub.connectionVersion : undefined;
|
|
1328
|
+
const needsReconciliation = sub.needsReconciliation({ since, limit });
|
|
1329
|
+
const recovered = needsReconciliation ? await reconcile(session, id, extra.signal) : {};
|
|
1330
|
+
if (extra.signal?.aborted) {
|
|
1331
|
+
return refused(`Cancelled while reading events on ${id}. Buffered events remain available on the next ` +
|
|
1332
|
+
`call. ` +
|
|
1333
|
+
(connectionNote(sub, connection) || 'The stream is still open and buffering.'));
|
|
1334
|
+
}
|
|
1335
|
+
const after = sub.state;
|
|
1336
|
+
if (after.status === 'stopped') {
|
|
1337
|
+
return stopped(session, id, after.reason, sub, { since, limit });
|
|
1338
|
+
}
|
|
1056
1339
|
const d = sub.read({ since, limit });
|
|
1057
|
-
const extras = d.loss ?
|
|
1340
|
+
const extras = d.loss ? recovered : {};
|
|
1341
|
+
const interruption = connectionNote(sub, connection);
|
|
1342
|
+
if (interruption) {
|
|
1343
|
+
return said(interruption + (d.loss ? reconciled(extras) : ''), {
|
|
1344
|
+
...body(id, d, sub, sub.watching),
|
|
1345
|
+
...extras,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1058
1348
|
if (!d.events.length) {
|
|
1059
1349
|
// The one case where "this is an answer rather than a gap" is exactly
|
|
1060
1350
|
// wrong: a gap whose surviving events were all read already leaves an
|
|
@@ -1062,16 +1352,16 @@ export const registerEvents = (server, session) => {
|
|
|
1062
1352
|
// either way; the sentence has to agree with it.
|
|
1063
1353
|
if (d.loss) {
|
|
1064
1354
|
return said(`Nothing new on ${id} that survived — there is a hole in the history here, and what ` +
|
|
1065
|
-
`was in it is gone
|
|
1066
|
-
`computer_now for where the computer actually stands, which is what the missing ` +
|
|
1067
|
-
`events would have told you.`, { ...body(id, d, sub.watching), ...extras });
|
|
1355
|
+
`was in it is gone.${reconciled(extras)}`, { ...body(id, d, sub, sub.watching), ...extras });
|
|
1068
1356
|
}
|
|
1069
1357
|
return said(`Nothing new on ${id}. The stream is open and buffering, so this is an answer rather ` +
|
|
1070
|
-
`than a gap: nothing has been reported since you last read.`, { ...body(id, d, sub.watching), ...extras });
|
|
1358
|
+
`than a gap: nothing has been reported since you last read.`, { ...body(id, d, sub, sub.watching), ...extras });
|
|
1071
1359
|
}
|
|
1072
1360
|
const kinds = [...new Set(d.events.map((e) => e.type))].join(', ');
|
|
1073
1361
|
return said(`${d.events.length} event${d.events.length === 1 ? '' : 's'} on ${id}: ${kinds}.` +
|
|
1074
|
-
(d.more
|
|
1362
|
+
(d.more
|
|
1363
|
+
? ` ${d.more} more ${d.more === 1 ? 'is' : 'are'} buffered — call again for ${d.more === 1 ? 'it' : 'them'}.`
|
|
1364
|
+
: ''), { ...body(id, d, sub, sub.watching), ...extras });
|
|
1075
1365
|
}));
|
|
1076
1366
|
};
|
|
1077
1367
|
//# sourceMappingURL=events.js.map
|