@ccmsg/cli 0.12.0 → 0.14.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/package.json +2 -2
- package/src/auth/admin.ts +5 -2
- package/src/auth/auth.ts +17 -17
- package/src/auth/records.ts +64 -42
- package/src/cli.ts +38 -5
- package/src/daemon/snapshot.ts +3 -3
- package/src/daemon/supervise.ts +15 -7
- package/src/dispatch/dispatch.ts +4 -4
- package/src/files/containment.ts +42 -19
- package/src/files/files.ts +123 -104
- package/src/files/sandbox.ts +0 -0
- package/src/instance/instance.ts +43 -16
- package/src/instance/log.ts +29 -13
- package/src/kv/store.ts +104 -56
- package/src/launcher/launcher.ts +8 -5
- package/src/launcher/roots.ts +10 -7
- package/src/launcher/tree.ts +15 -12
- package/src/mesh/instances.ts +5 -5
- package/src/mesh/mesh.ts +11 -11
- package/src/mesh/relay.ts +8 -8
- package/src/messaging/delivery.ts +126 -27
- package/src/messaging/direct.ts +105 -39
- package/src/messaging/inbox.ts +72 -18
- package/src/messaging/notify.ts +7 -2
- package/src/sessions/dump.ts +73 -11
- package/src/sessions/fork.ts +21 -14
- package/src/sessions/handlers.ts +9 -7
- package/src/sessions/items.ts +10 -8
- package/src/sessions/last-live.ts +28 -5
- package/src/sessions/registry.ts +43 -26
- package/src/sessions/search.ts +16 -7
- package/src/sessions/status.ts +3 -3
- package/src/topics/egress.ts +2 -2
- package/src/topics/topics.ts +0 -0
- package/src/transcript/files.ts +65 -23
- package/src/transcript/read.ts +10 -10
- package/src/transcript/scan.ts +49 -0
package/src/mesh/mesh.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type SettledIdentity,
|
|
19
19
|
} from "../dispatch/index.ts";
|
|
20
20
|
import type { TopicValue } from "../topics/index.ts";
|
|
21
|
-
import { AUTH_TOPIC,
|
|
21
|
+
import { AUTH_TOPIC, isMeshTopic, Relay } from "./relay.ts";
|
|
22
22
|
import {
|
|
23
23
|
ALLOWED_ALGS,
|
|
24
24
|
EphemeralKey,
|
|
@@ -388,7 +388,7 @@ export class Mesh {
|
|
|
388
388
|
* whether it can be reached right now (DESIGN §7.5).
|
|
389
389
|
*
|
|
390
390
|
* A peer no handshake has settled yet is listed without an id. The operator
|
|
391
|
-
* configured that endpoint, so it is an entry of the
|
|
391
|
+
* configured that endpoint, so it is an entry of the mesh whether or not
|
|
392
392
|
* anything has answered there — and leaving it out would hide exactly the
|
|
393
393
|
* peer whose link is down, which is the one a reader is looking for. */
|
|
394
394
|
instances(): InstanceInfo[] {
|
|
@@ -408,7 +408,7 @@ export class Mesh {
|
|
|
408
408
|
|
|
409
409
|
/** Whether any peer is currently out of reach.
|
|
410
410
|
*
|
|
411
|
-
* What separates "no instance in the
|
|
411
|
+
* What separates "no instance in the mesh knows this session" from "an
|
|
412
412
|
* instance that might know it cannot be asked" — the one distinction DESIGN §6.6
|
|
413
413
|
* says rests on the mesh's connection state and on nothing else. */
|
|
414
414
|
anyUnreachable(): boolean {
|
|
@@ -417,7 +417,7 @@ export class Mesh {
|
|
|
417
417
|
|
|
418
418
|
/** Which instance should answer for a session this instance does not hold.
|
|
419
419
|
*
|
|
420
|
-
* A session the
|
|
420
|
+
* A session the mesh has named belongs to the instance its `peers` row
|
|
421
421
|
* states. One nobody has named while a peer is out of reach is answered with
|
|
422
422
|
* that peer: forwarding there fails and the caller is told
|
|
423
423
|
* `instance_unreachable`, which is what DESIGN §6.6 asks for in place of deciding
|
|
@@ -532,7 +532,7 @@ export class Mesh {
|
|
|
532
532
|
*
|
|
533
533
|
* The caller the envelope names, on the link it arrived over. A request that
|
|
534
534
|
* names none is dispatched as the link itself, whose role is `instance` —
|
|
535
|
-
* which the attribute table already answers, since no
|
|
535
|
+
* which the attribute table already answers, since no owner_instance op is
|
|
536
536
|
* open to an instance. */
|
|
537
537
|
caller(conn: Requester, caller: CallerIdentity | undefined): Requester {
|
|
538
538
|
const link = this.#linkOf.get(conn);
|
|
@@ -553,12 +553,12 @@ export class Mesh {
|
|
|
553
553
|
|
|
554
554
|
/** The current value of a relayed topic, one entry per instance that has
|
|
555
555
|
* stated one. Handed to a fresh local subscriber beside this instance's own
|
|
556
|
-
* snapshot, so it opens on the
|
|
556
|
+
* snapshot, so it opens on the mesh rather than on us. */
|
|
557
557
|
snapshot(topic: string): readonly TopicValue[] {
|
|
558
558
|
return this.relay.snapshot(topic);
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
/** A local subscriber appeared on a
|
|
561
|
+
/** A local subscriber appeared on a mesh topic, or the last one left.
|
|
562
562
|
*
|
|
563
563
|
* The subscription travels: what a subscriber asks of this instance, this
|
|
564
564
|
* instance asks of every peer, and the frames come back unchanged (DESIGN §7.4).
|
|
@@ -567,7 +567,7 @@ export class Mesh {
|
|
|
567
567
|
// `auth.records` is never given up and never asked for on demand: every
|
|
568
568
|
// instance holds the whole set whether or not anything local is watching
|
|
569
569
|
// it, the way `peers` is also the routing table (DESIGN §7.4, DR-0001 §2.6).
|
|
570
|
-
if (topic === AUTH_TOPIC || !
|
|
570
|
+
if (topic === AUTH_TOPIC || !isMeshTopic(topic)) return;
|
|
571
571
|
if (wanted) {
|
|
572
572
|
if (this.#demanded.has(topic)) return;
|
|
573
573
|
this.#demanded.add(topic);
|
|
@@ -593,7 +593,7 @@ export class Mesh {
|
|
|
593
593
|
topic,
|
|
594
594
|
// The instance asks on behalf of whoever subscribed to it, and what they
|
|
595
595
|
// have in common is that they are this deployment's people rather than
|
|
596
|
-
// any one session: a
|
|
596
|
+
// any one session: a mesh topic is the same value for all of them
|
|
597
597
|
// (DESIGN §6.2), so there is nothing narrower to name.
|
|
598
598
|
//
|
|
599
599
|
// `auth.records` is the exception, and the one topic no person may hear:
|
|
@@ -1090,12 +1090,12 @@ export class Mesh {
|
|
|
1090
1090
|
//
|
|
1091
1091
|
// The far end has no other way to learn this instance is going: it would
|
|
1092
1092
|
// keep the link, keep answering `reachable`, and keep routing
|
|
1093
|
-
// `
|
|
1093
|
+
// `owner_instance` ops here until its own heartbeat gave up minutes later,
|
|
1094
1094
|
// where the disconnection of DESIGN §7.5 is supposed to be immediate. Which side
|
|
1095
1095
|
// dialled a link is decided by the glare rule from a comparison of
|
|
1096
1096
|
// endpoint strings (mesh-peer-auth §8.1), so which of a peer's links this instance
|
|
1097
1097
|
// accepted is not something either end chose — leaving those open makes a
|
|
1098
|
-
// clean stop look like a silent one to whichever half of the
|
|
1098
|
+
// clean stop look like a silent one to whichever half of the mesh the
|
|
1099
1099
|
// comparison put on this side.
|
|
1100
1100
|
//
|
|
1101
1101
|
// An accepted socket is transport's to release (DESIGN §8.5 step 5), and left to
|
package/src/mesh/relay.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@ccmsg/protocol";
|
|
10
10
|
import { Elements, type TopicValue } from "../topics/index.ts";
|
|
11
11
|
|
|
12
|
-
/** The rows of sessions the whole
|
|
12
|
+
/** The rows of sessions the whole mesh is seen through.
|
|
13
13
|
*
|
|
14
14
|
* They are `element`-granular, and an element topic is relayable only when its
|
|
15
15
|
* elements say whose they are: a row here names the instance that holds the
|
|
@@ -19,13 +19,13 @@ import { Elements, type TopicValue } from "../topics/index.ts";
|
|
|
19
19
|
* such name — an `inbox` frame belongs to a session, not to an instance. */
|
|
20
20
|
const ROW_TOPICS: readonly string[] = ["peers", "agents"];
|
|
21
21
|
|
|
22
|
-
/** The topics a subscriber sees the whole
|
|
22
|
+
/** The topics a subscriber sees the whole mesh on.
|
|
23
23
|
*
|
|
24
24
|
* A per-instance whole is relayable by construction (DESIGN §6.2): a frame replaces
|
|
25
25
|
* its own instance's entries and leaves every other instance's alone, so
|
|
26
26
|
* several instances can state the same topic name without colliding. The rows
|
|
27
27
|
* above are relayable for the same reason read one element at a time. */
|
|
28
|
-
export const
|
|
28
|
+
export const MESH_TOPICS: readonly string[] = [
|
|
29
29
|
...PLAIN_TOPICS.filter((topic) => TOPIC_ATTRIBUTES[topic].granularity === "per_instance_whole"),
|
|
30
30
|
...ROW_TOPICS,
|
|
31
31
|
];
|
|
@@ -37,8 +37,8 @@ export const CLUSTER_TOPICS: readonly string[] = [
|
|
|
37
37
|
* the set this instance holds rather than held here (DR-0001 §2.6). */
|
|
38
38
|
export const AUTH_TOPIC = "auth.records";
|
|
39
39
|
|
|
40
|
-
export function
|
|
41
|
-
return
|
|
40
|
+
export function isMeshTopic(topic: string): boolean {
|
|
41
|
+
return MESH_TOPICS.includes(topic);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Whether what a frame of this topic carries is rows to be merged rather than
|
|
@@ -64,7 +64,7 @@ export interface RelayDeps {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** What the peers said, held on this instance so that losing a peer does not
|
|
67
|
-
* empty the
|
|
67
|
+
* empty the mesh view (DESIGN §7.5).
|
|
68
68
|
*
|
|
69
69
|
* Two things live here and nowhere else: the last whole value each instance
|
|
70
70
|
* stated per topic, and whether that instance can be reached right now. The
|
|
@@ -99,7 +99,7 @@ export class Relay {
|
|
|
99
99
|
* unchanged — recomputing it would put the same judgement in two places
|
|
100
100
|
* (DESIGN §7.4). */
|
|
101
101
|
accept(instance: InstanceId, topic: string, data: unknown, snapshot = false): void {
|
|
102
|
-
if (!
|
|
102
|
+
if (!isMeshTopic(topic)) return;
|
|
103
103
|
this.#sweep();
|
|
104
104
|
const held = this.#held.get(instance) ?? new Map<string, unknown>();
|
|
105
105
|
this.#held.set(instance, held);
|
|
@@ -174,7 +174,7 @@ export class Relay {
|
|
|
174
174
|
return values;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/** Which instance a session belongs to, read from the
|
|
177
|
+
/** Which instance a session belongs to, read from the mesh values the
|
|
178
178
|
* peers stated (DESIGN §7.3).
|
|
179
179
|
*
|
|
180
180
|
* `peers` names every session an instance holds, connected and lost alike,
|
|
@@ -2,6 +2,8 @@ import type {
|
|
|
2
2
|
CallerIdentity,
|
|
3
3
|
CandidateSession,
|
|
4
4
|
InboxMessage,
|
|
5
|
+
InboxRemoved,
|
|
6
|
+
InboxRemovedReason,
|
|
5
7
|
InstanceId,
|
|
6
8
|
MessageSendArgs,
|
|
7
9
|
MessageSendResult,
|
|
@@ -21,7 +23,12 @@ import {
|
|
|
21
23
|
OpError,
|
|
22
24
|
type Requester,
|
|
23
25
|
} from "../dispatch/index.ts";
|
|
24
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
PEOPLE,
|
|
28
|
+
type PublishOutcome,
|
|
29
|
+
type TopicValue,
|
|
30
|
+
type UpstreamResource,
|
|
31
|
+
} from "../topics/index.ts";
|
|
25
32
|
import type { DirectRoute } from "./direct.ts";
|
|
26
33
|
import type { Inbox } from "./inbox.ts";
|
|
27
34
|
|
|
@@ -38,15 +45,15 @@ export interface SessionLookup {
|
|
|
38
45
|
peerRows(): PeerInfo[];
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/** The rest of the
|
|
48
|
+
/** The rest of the mesh, for a message addressed outside this instance.
|
|
42
49
|
*
|
|
43
|
-
* `message.send` is
|
|
50
|
+
* `message.send` is an `any_instance` op — any instance may be asked — but a message
|
|
44
51
|
* reaches a session through the session's own connections, which are held by
|
|
45
52
|
* the instance it greeted. So the op is answered here by carrying it there
|
|
46
|
-
* (DESIGN §2.2 step 6 is about `
|
|
53
|
+
* (DESIGN §2.2 step 6 is about `owner_instance` ops; this is the same forwarding for
|
|
47
54
|
* the one op whose subject is elsewhere while its op is not). */
|
|
48
|
-
export interface
|
|
49
|
-
/** Which instance holds this session, or nothing when the
|
|
55
|
+
export interface MeshReach {
|
|
56
|
+
/** Which instance holds this session, or nothing when the mesh has not
|
|
50
57
|
* named it. */
|
|
51
58
|
ownerOf(sid: Sid): InstanceId | undefined;
|
|
52
59
|
/** Whether an instance that might hold it cannot be asked right now. */
|
|
@@ -64,14 +71,19 @@ export interface DeliveryDeps {
|
|
|
64
71
|
readonly sessions: SessionLookup;
|
|
65
72
|
/** Absent on an instance with no mesh, where every session it can name is
|
|
66
73
|
* its own. */
|
|
67
|
-
readonly
|
|
74
|
+
readonly mesh?: MeshReach;
|
|
68
75
|
readonly inbox: Inbox;
|
|
69
76
|
/** Route (a). Off until it is confirmed against a running harness, which is
|
|
70
77
|
* condition 0 of DESIGN §6.5 and is why this is handed in rather than built here. */
|
|
71
78
|
readonly direct: DirectRoute;
|
|
72
79
|
/** The one way a value reaches subscribers (DESIGN §6.1), narrowed to the session a
|
|
73
80
|
* message is for. */
|
|
74
|
-
readonly publish: (
|
|
81
|
+
readonly publish: (
|
|
82
|
+
topic: string,
|
|
83
|
+
data: unknown,
|
|
84
|
+
instance: InstanceId,
|
|
85
|
+
to: Sid | typeof PEOPLE,
|
|
86
|
+
) => PublishOutcome;
|
|
75
87
|
/** How many of that session's connections are listening on `inbox`. */
|
|
76
88
|
readonly listeners: (topic: string, to: Sid) => number;
|
|
77
89
|
}
|
|
@@ -97,6 +109,14 @@ export class Delivery implements UpstreamResource {
|
|
|
97
109
|
|
|
98
110
|
constructor(private readonly deps: DeliveryDeps) {
|
|
99
111
|
this.#counter = deps.inbox.lastCounter(`${deps.self}/`);
|
|
112
|
+
// Every way out of the inbox is stated to the watchers, and the inbox is
|
|
113
|
+
// the one that knows about all of them — handed over, timed out, dropped
|
|
114
|
+
// for a newer message. Read from there rather than published beside each
|
|
115
|
+
// call that removes something, so a way out nobody thought of here is
|
|
116
|
+
// still a removal somebody watching sees (DESIGN §6.7).
|
|
117
|
+
deps.inbox.onRemoved((mid, reason) => {
|
|
118
|
+
this.#removed([mid], reason);
|
|
119
|
+
});
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
/** `message.send`. The op fails only for a sid nobody knows; every other
|
|
@@ -123,47 +143,87 @@ export class Delivery implements UpstreamResource {
|
|
|
123
143
|
if (direct === "refused") {
|
|
124
144
|
// Turned away for now, which is neither delivered nor undeliverable: it
|
|
125
145
|
// waits in the inbox and is offered again (DESIGN §6.8).
|
|
126
|
-
this
|
|
146
|
+
await this.#hold(to, message);
|
|
127
147
|
return { delivered: false, reason: "throttled" };
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
if (this.deps.listeners(INBOX, to) > 0) {
|
|
131
151
|
if (this.deps.publish(INBOX, [message], this.deps.self, to) === "ok") {
|
|
152
|
+
// Route (b) is the topic itself, so the message never waits anywhere:
|
|
153
|
+
// the watchers are told both halves at once — it arrived, and it is
|
|
154
|
+
// gone because the session has it — since a view built from frames
|
|
155
|
+
// alone would otherwise show it waiting for good.
|
|
156
|
+
this.#watchers(to, [message]);
|
|
157
|
+
this.#removed([message.mid], "delivered");
|
|
132
158
|
return { delivered: true };
|
|
133
159
|
}
|
|
134
160
|
// The session is listening but is behind on what it has already been
|
|
135
161
|
// offered, which is the same standing as route (a) turning the message
|
|
136
162
|
// away: it waits in the inbox and is offered again (DESIGN §6.8).
|
|
137
|
-
this
|
|
163
|
+
await this.#hold(to, message);
|
|
138
164
|
return { delivered: false, reason: "throttled" };
|
|
139
165
|
}
|
|
140
166
|
|
|
141
|
-
const { evicted } = this
|
|
167
|
+
const { evicted } = await this.#hold(to, message);
|
|
142
168
|
return this.#undelivered(to, evicted ? "inbox_full" : this.#reason(state));
|
|
143
169
|
};
|
|
144
170
|
|
|
171
|
+
/** Hold a message the session could not take, and say so to the watchers.
|
|
172
|
+
*
|
|
173
|
+
* Stated before it is held, so that a message dropped to make room for it
|
|
174
|
+
* reads in the order the two happened: a removal of something the watcher
|
|
175
|
+
* has, rather than of something it is about to be told about. */
|
|
176
|
+
async #hold(to: Sid, message: InboxMessage): Promise<{ evicted: boolean }> {
|
|
177
|
+
this.#watchers(to, [message]);
|
|
178
|
+
return await this.deps.inbox.hold(to, message);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** What is waiting, as somebody looking at it from outside reads it.
|
|
182
|
+
*
|
|
183
|
+
* A person holds every session's inbox in one subscription, so the rows they
|
|
184
|
+
* are answered with name their recipient; the session's own do not, since
|
|
185
|
+
* its subscription is already the recipient (contract, `InboxMessage.to`). */
|
|
186
|
+
#watchers(to: Sid, messages: readonly InboxMessage[]): void {
|
|
187
|
+
if (messages.length === 0) return;
|
|
188
|
+
this.deps.publish(
|
|
189
|
+
INBOX,
|
|
190
|
+
messages.map((message) => ({ ...message, to })),
|
|
191
|
+
this.deps.self,
|
|
192
|
+
PEOPLE,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A message has left an inbox, which only a watcher has anything left to do
|
|
197
|
+
* with: the session it was for either has it or never will, and neither is
|
|
198
|
+
* something to tell it on the topic it receives messages on. */
|
|
199
|
+
#removed(mids: readonly Mid[], reason: InboxRemovedReason): void {
|
|
200
|
+
if (mids.length === 0) return;
|
|
201
|
+
const gone: InboxRemoved[] = mids.map((mid) => ({ mid, removed: true, reason }));
|
|
202
|
+
this.deps.publish(INBOX, gone, this.deps.self, PEOPLE);
|
|
203
|
+
}
|
|
204
|
+
|
|
145
205
|
/** A session this instance does not hold: carried to the instance that does,
|
|
146
|
-
* or named as one the
|
|
206
|
+
* or named as one the mesh cannot answer for right now.
|
|
147
207
|
*
|
|
148
|
-
* Nothing when the
|
|
208
|
+
* Nothing when the mesh has no such session anywhere and every instance
|
|
149
209
|
* could be asked — which is the only case `session_not_found` covers (DESIGN §6.6).
|
|
150
210
|
* While an instance is out of reach the sid may well be its, so the sender is
|
|
151
211
|
* told the reason rather than that the session does not exist. The message is
|
|
152
212
|
* not held here either: the inbox that would offer it again is the one on the
|
|
153
213
|
* instance that owns the session (DESIGN §6.7). */
|
|
154
214
|
async #elsewhere(to: Sid, input: HandlerInput): Promise<MessageSendResult | undefined> {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
const owner =
|
|
215
|
+
const reach = this.deps.mesh;
|
|
216
|
+
if (reach === undefined) return undefined;
|
|
217
|
+
const owner = reach.ownerOf(to);
|
|
158
218
|
if (owner === undefined || owner === this.deps.self) {
|
|
159
|
-
return
|
|
219
|
+
return reach.anyUnreachable()
|
|
160
220
|
? { delivered: false, reason: "instance_unreachable" }
|
|
161
221
|
: undefined;
|
|
162
222
|
}
|
|
163
223
|
// The sender, as the owning instance will run the op as: the identity the
|
|
164
224
|
// connection greeted with, which is the same thing `message.send` reads to
|
|
165
225
|
// decide who a message is from (DESIGN §6.5).
|
|
166
|
-
const answer = await
|
|
226
|
+
const answer = await reach.forward(owner, input.args, callerOf(input));
|
|
167
227
|
if (answer.kind === "reply") {
|
|
168
228
|
const { ok: _ok, request_id: _id, ...body } = answer.response;
|
|
169
229
|
return body as unknown as MessageSendResult;
|
|
@@ -187,11 +247,18 @@ export class Delivery implements UpstreamResource {
|
|
|
187
247
|
* nothing waiting are not asked about, so the cost of a change nobody is owed
|
|
188
248
|
* anything after is one map read. */
|
|
189
249
|
retry = async (): Promise<void> => {
|
|
250
|
+
const offers: Promise<void>[] = [];
|
|
190
251
|
for (const sid of this.deps.inbox.sids()) {
|
|
191
252
|
const state = this.deps.sessions.classify(sid);
|
|
192
253
|
if (state === undefined || state === "paused" || state === "disappeared") continue;
|
|
193
|
-
|
|
254
|
+
// One session at a time within its own offer, every session at once
|
|
255
|
+
// across them: a session that is slow to answer, or that never does
|
|
256
|
+
// before its deadline, is not a reason the next session waits (DR-0015).
|
|
257
|
+
// The order within a session is what `#offer` holds to, and it guards
|
|
258
|
+
// itself per sid.
|
|
259
|
+
offers.push(this.#offer(sid));
|
|
194
260
|
}
|
|
261
|
+
await Promise.all(offers);
|
|
195
262
|
};
|
|
196
263
|
|
|
197
264
|
/** Hand a session what it is owed, oldest first, over route (a).
|
|
@@ -215,7 +282,7 @@ export class Delivery implements UpstreamResource {
|
|
|
215
282
|
// delivered, and a daemon killed here must not offer those again.
|
|
216
283
|
if (outcome !== "delivered") break;
|
|
217
284
|
this.#claimed.get(to)?.delete(message.mid);
|
|
218
|
-
this.deps.inbox.delivered(to, [message.mid]);
|
|
285
|
+
await this.deps.inbox.delivered(to, [message.mid]);
|
|
219
286
|
}
|
|
220
287
|
} finally {
|
|
221
288
|
this.#claimed.delete(to);
|
|
@@ -243,22 +310,54 @@ export class Delivery implements UpstreamResource {
|
|
|
243
310
|
*
|
|
244
311
|
* A message an offer over route (a) has claimed is left out: it is on its way
|
|
245
312
|
* on the other route, and the session subscribing while that runs must not
|
|
246
|
-
* make it two messages.
|
|
313
|
+
* make it two messages.
|
|
314
|
+
*
|
|
315
|
+
* **A person's subscription is a view, not a delivery.** They are answered
|
|
316
|
+
* with everything waiting anywhere on this instance, each row naming its
|
|
317
|
+
* recipient, and the inbox is left exactly as it was — a person is not who
|
|
318
|
+
* any of it was addressed to, and a view that consumed what it looked at
|
|
319
|
+
* would deliver messages to nobody by being opened. Nothing is left out of
|
|
320
|
+
* it either: a message an offer has claimed is still waiting until that
|
|
321
|
+
* offer says otherwise, and that is what somebody watching wants to see. */
|
|
247
322
|
snapshot(topic: string, conn: Requester): readonly TopicValue[] {
|
|
248
323
|
const identity = conn.identity;
|
|
249
|
-
|
|
250
|
-
|
|
324
|
+
if (topic !== INBOX || identity.state !== "settled") return [];
|
|
325
|
+
const sid = identity.sid;
|
|
326
|
+
// A settled connection that names no session is somebody watching: the
|
|
327
|
+
// topic table lets nobody but a session and a person subscribe here, so
|
|
328
|
+
// the sid is the whole of the difference and no role is read for it (M1).
|
|
329
|
+
if (sid === undefined) return [{ instance: this.deps.self, data: this.#waiting() }];
|
|
251
330
|
const claimed = this.#claimed.get(sid);
|
|
252
331
|
const held = this.deps.inbox
|
|
253
332
|
.undelivered(sid)
|
|
254
333
|
.filter((message) => claimed?.has(message.mid) !== true);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
334
|
+
// The messages leave the inbox here, in memory, and the lines saying so go
|
|
335
|
+
// behind whatever the file already owes (DR-0015): the frame carrying them
|
|
336
|
+
// is queued on the connection before this returns, so what the session has
|
|
337
|
+
// been handed is settled whether or not the line has landed yet.
|
|
338
|
+
void this.deps.inbox
|
|
339
|
+
.delivered(
|
|
340
|
+
sid,
|
|
341
|
+
held.map((message) => message.mid),
|
|
342
|
+
)
|
|
343
|
+
.catch(() => {
|
|
344
|
+
// A line that could not be written costs a message being offered again
|
|
345
|
+
// on the next run, which is what the inbox does about anything it is
|
|
346
|
+
// unsure of.
|
|
347
|
+
});
|
|
259
348
|
return [{ instance: this.deps.self, data: held }];
|
|
260
349
|
}
|
|
261
350
|
|
|
351
|
+
/** Everything still undelivered on this instance, oldest first within each
|
|
352
|
+
* session, each row naming who it is for. */
|
|
353
|
+
#waiting(): InboxMessage[] {
|
|
354
|
+
const rows: InboxMessage[] = [];
|
|
355
|
+
for (const sid of this.deps.inbox.sids()) {
|
|
356
|
+
for (const message of this.deps.inbox.undelivered(sid)) rows.push({ ...message, to: sid });
|
|
357
|
+
}
|
|
358
|
+
return rows;
|
|
359
|
+
}
|
|
360
|
+
|
|
262
361
|
/** The reason a message is waiting, named from the classification alone
|
|
263
362
|
* (DESIGN §6.6). `preparing` is the live session with nowhere to put it: it is there,
|
|
264
363
|
* route (a) did not carry it, and nothing of its is listening yet.
|