@ccmsg/cli 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 +23 -0
- package/package.json +32 -0
- package/src/cli.ts +1074 -0
- package/src/daemon/control.ts +88 -0
- package/src/daemon/index.ts +6 -0
- package/src/daemon/link.ts +93 -0
- package/src/daemon/log.ts +116 -0
- package/src/daemon/registry.ts +285 -0
- package/src/daemon/snapshot.ts +115 -0
- package/src/daemon/supervise.ts +446 -0
- package/src/dispatch/caller.ts +47 -0
- package/src/dispatch/dispatch.ts +128 -0
- package/src/dispatch/handler.ts +55 -0
- package/src/dispatch/identity.ts +22 -0
- package/src/dispatch/index.ts +5 -0
- package/src/dispatch/result.ts +58 -0
- package/src/files/containment.ts +263 -0
- package/src/files/files.ts +421 -0
- package/src/files/index.ts +14 -0
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/hook.ts +48 -0
- package/src/greeting/index.ts +2 -0
- package/src/greeting/meta.ts +66 -0
- package/src/instance/config.ts +424 -0
- package/src/instance/handlers.ts +28 -0
- package/src/instance/identity.ts +44 -0
- package/src/instance/index.ts +8 -0
- package/src/instance/instance.ts +911 -0
- package/src/instance/lock.ts +108 -0
- package/src/instance/log.ts +30 -0
- package/src/instance/paths.ts +200 -0
- package/src/instance/socket.ts +62 -0
- package/src/kv/index.ts +2 -0
- package/src/kv/merge.ts +66 -0
- package/src/kv/store.ts +195 -0
- package/src/launcher/index.ts +4 -0
- package/src/launcher/launcher.ts +190 -0
- package/src/launcher/roots.ts +32 -0
- package/src/launcher/spawn.ts +81 -0
- package/src/launcher/tree.ts +80 -0
- package/src/mesh/index.ts +5 -0
- package/src/mesh/keys.ts +158 -0
- package/src/mesh/mesh.ts +1169 -0
- package/src/mesh/probe.ts +100 -0
- package/src/mesh/relay.ts +147 -0
- package/src/mesh/wire.ts +96 -0
- package/src/messaging/delivery.ts +375 -0
- package/src/messaging/direct.ts +433 -0
- package/src/messaging/handlers.ts +14 -0
- package/src/messaging/inbox.ts +191 -0
- package/src/messaging/index.ts +5 -0
- package/src/messaging/notify.ts +117 -0
- package/src/plugin/claude.ts +148 -0
- package/src/plugin/index.ts +13 -0
- package/src/plugin/install.ts +416 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +359 -0
- package/src/sessions/classify.ts +66 -0
- package/src/sessions/dump.ts +105 -0
- package/src/sessions/fork.ts +127 -0
- package/src/sessions/handlers.ts +158 -0
- package/src/sessions/harness.ts +167 -0
- package/src/sessions/index.ts +26 -0
- package/src/sessions/last-live.ts +111 -0
- package/src/sessions/processes.ts +413 -0
- package/src/sessions/registry.ts +785 -0
- package/src/sessions/search.ts +278 -0
- package/src/sessions/status.ts +209 -0
- package/src/sessions/terminals.ts +72 -0
- package/src/sessions/workspace.ts +140 -0
- package/src/topics/handlers.ts +42 -0
- package/src/topics/index.ts +2 -0
- package/src/topics/topics.ts +290 -0
- package/src/transcript/files.ts +201 -0
- package/src/transcript/fold.ts +833 -0
- package/src/transcript/index.ts +16 -0
- package/src/transcript/read.ts +82 -0
- package/src/transcript/tail.ts +195 -0
- package/src/transcript/transcripts.ts +162 -0
- package/src/translate/helper.ts +87 -0
- package/src/translate/index.ts +2 -0
- package/src/translate/translate.ts +127 -0
- package/src/transport/conn.ts +129 -0
- package/src/transport/dial.ts +65 -0
- package/src/transport/driver.ts +102 -0
- package/src/transport/entry.ts +39 -0
- package/src/transport/framing.ts +131 -0
- package/src/transport/index.ts +8 -0
- package/src/transport/listener.ts +39 -0
- package/src/transport/uds.ts +88 -0
- package/src/transport/ws.ts +170 -0
- package/src/upstream/events.ts +125 -0
- package/src/upstream/gateway.ts +275 -0
- package/src/upstream/index.ts +8 -0
- package/src/upstream/json.ts +81 -0
- package/src/upstream/requests.ts +234 -0
- package/src/upstream/stats.ts +99 -0
- package/src/upstream/status.ts +281 -0
- package/src/upstream/usage.ts +208 -0
- package/src/upstream/webhook.ts +141 -0
- package/src/version.ts +8 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallerIdentity,
|
|
3
|
+
CandidateSession,
|
|
4
|
+
InboxMessage,
|
|
5
|
+
InstanceId,
|
|
6
|
+
LastLiveSession,
|
|
7
|
+
MessageSendArgs,
|
|
8
|
+
MessageSendResult,
|
|
9
|
+
Mid,
|
|
10
|
+
PeerInfo,
|
|
11
|
+
Sender,
|
|
12
|
+
SessionState,
|
|
13
|
+
Sid,
|
|
14
|
+
Timestamp,
|
|
15
|
+
UndeliveredReason,
|
|
16
|
+
} from "@ccmsg/protocol";
|
|
17
|
+
import { USER_SENDER } from "@ccmsg/protocol";
|
|
18
|
+
import {
|
|
19
|
+
type DispatchResult,
|
|
20
|
+
type HandlerInput,
|
|
21
|
+
OpError,
|
|
22
|
+
type Requester,
|
|
23
|
+
} from "../dispatch/index.ts";
|
|
24
|
+
import type { TopicValue, UpstreamResource } from "../topics/index.ts";
|
|
25
|
+
import type { DirectRoute } from "./direct.ts";
|
|
26
|
+
import type { Inbox } from "./inbox.ts";
|
|
27
|
+
|
|
28
|
+
/** The topic a message reaches its session on, and the only route in use while
|
|
29
|
+
* route (a) waits for confirmation (§4.1). */
|
|
30
|
+
const INBOX = "inbox";
|
|
31
|
+
|
|
32
|
+
/** What delivery reads about a session. Two questions, both answered by the
|
|
33
|
+
* sessions domain from the inputs of §5.1: where a session stands, and which
|
|
34
|
+
* sessions are around — neither is asked of anything else, which is what keeps
|
|
35
|
+
* the reasons of §4.2 from growing a source per reason. */
|
|
36
|
+
export interface SessionLookup {
|
|
37
|
+
classify(sid: Sid): SessionState | undefined;
|
|
38
|
+
peers(): { peers: PeerInfo[]; last_live: LastLiveSession[] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The rest of the cluster, for a message addressed outside this instance.
|
|
42
|
+
*
|
|
43
|
+
* `message_send` is a `cluster` op — any instance may be asked — but a message
|
|
44
|
+
* reaches a session through the session's own connections, which are held by
|
|
45
|
+
* the instance it greeted. So the op is answered here by carrying it there
|
|
46
|
+
* (§3.2 step 6 is about `instance-local` ops; this is the same forwarding for
|
|
47
|
+
* the one op whose subject is elsewhere while its op is not). */
|
|
48
|
+
export interface Cluster {
|
|
49
|
+
/** Which instance holds this session, or nothing when the cluster has not
|
|
50
|
+
* named it. */
|
|
51
|
+
ownerOf(sid: Sid): InstanceId | undefined;
|
|
52
|
+
/** Whether an instance that might hold it cannot be asked right now. */
|
|
53
|
+
anyUnreachable(): boolean;
|
|
54
|
+
/** Carry the op to that instance, run there as the caller named here. */
|
|
55
|
+
forward(
|
|
56
|
+
to: InstanceId,
|
|
57
|
+
frame: Record<string, unknown>,
|
|
58
|
+
caller: CallerIdentity | undefined,
|
|
59
|
+
): Promise<DispatchResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DeliveryDeps {
|
|
63
|
+
readonly self: InstanceId;
|
|
64
|
+
readonly sessions: SessionLookup;
|
|
65
|
+
/** Absent on an instance with no mesh, where every session it can name is
|
|
66
|
+
* its own. */
|
|
67
|
+
readonly cluster?: Cluster;
|
|
68
|
+
readonly inbox: Inbox;
|
|
69
|
+
/** Route (a). Off until it is confirmed against a running harness, which is
|
|
70
|
+
* condition 0 of §4.1 and is why this is handed in rather than built here. */
|
|
71
|
+
readonly direct: DirectRoute;
|
|
72
|
+
/** The one way a value reaches subscribers (§6.1), narrowed to the session a
|
|
73
|
+
* message is for. */
|
|
74
|
+
readonly publish: (topic: string, data: unknown, instance: InstanceId, to: Sid) => void;
|
|
75
|
+
/** How many of that session's connections are listening on `inbox`. */
|
|
76
|
+
readonly listeners: (topic: string, to: Sid) => number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Delivery, and the inbox topic it delivers on (§4).
|
|
80
|
+
*
|
|
81
|
+
* Two things, as §4 splits them: the route a message takes, and what the sender
|
|
82
|
+
* is told when it took none. The second reads the classification and the inbox
|
|
83
|
+
* and nothing else (§4.2) — a reason is a name for a state that was already
|
|
84
|
+
* there, never a state of its own. */
|
|
85
|
+
export class Delivery implements UpstreamResource {
|
|
86
|
+
#counter: number;
|
|
87
|
+
|
|
88
|
+
/** The sessions an offer is running for, so two of them cannot run at once
|
|
89
|
+
* and send one message twice or send an older one after a newer. */
|
|
90
|
+
readonly #offering = new Set<Sid>();
|
|
91
|
+
|
|
92
|
+
/** The messages an offer has taken responsibility for, per session. They are
|
|
93
|
+
* still in the inbox — an offer that does not reach the end leaves them
|
|
94
|
+
* there — but they are spoken for, so the snapshot below hands them to
|
|
95
|
+
* nobody: one message goes out on one route (§4.3). */
|
|
96
|
+
readonly #claimed = new Map<Sid, Set<Mid>>();
|
|
97
|
+
|
|
98
|
+
constructor(private readonly deps: DeliveryDeps) {
|
|
99
|
+
this.#counter = deps.inbox.lastCounter(`${deps.self}/`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `message_send`. The op fails only for a sid nobody knows; every other
|
|
103
|
+
* outcome is a success carrying what became of the message. */
|
|
104
|
+
send = async (input: HandlerInput): Promise<MessageSendResult> => {
|
|
105
|
+
const args = input.args as unknown as MessageSendArgs;
|
|
106
|
+
const to = args.to;
|
|
107
|
+
const state = this.deps.sessions.classify(to);
|
|
108
|
+
if (state === undefined) {
|
|
109
|
+
const elsewhere = await this.#elsewhere(to, input);
|
|
110
|
+
if (elsewhere !== undefined) return elsewhere;
|
|
111
|
+
throw new OpError("session_not_found", `no session ${to}`);
|
|
112
|
+
}
|
|
113
|
+
const message = this.#message(args, this.#sender(input));
|
|
114
|
+
|
|
115
|
+
const direct = await this.deps.direct.send(to, message);
|
|
116
|
+
if (direct === "delivered") {
|
|
117
|
+
// Route (a) reaching this session is the session being able to receive,
|
|
118
|
+
// which is what the inbox waits for (§4.3). Whatever is still held for it
|
|
119
|
+
// is offered now, on the route that just worked.
|
|
120
|
+
await this.#offer(to);
|
|
121
|
+
return { delivered: true };
|
|
122
|
+
}
|
|
123
|
+
if (direct === "refused") {
|
|
124
|
+
// Turned away for now, which is neither delivered nor undeliverable: it
|
|
125
|
+
// waits in the inbox and is offered again (§4.4).
|
|
126
|
+
this.deps.inbox.hold(to, message);
|
|
127
|
+
return { delivered: false, reason: "throttled" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.deps.listeners(INBOX, to) > 0) {
|
|
131
|
+
this.deps.publish(INBOX, [message], this.deps.self, to);
|
|
132
|
+
return { delivered: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { evicted } = this.deps.inbox.hold(to, message);
|
|
136
|
+
return this.#undelivered(to, evicted ? "inbox_full" : this.#reason(state));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** A session this instance does not hold: carried to the instance that does,
|
|
140
|
+
* or named as one the cluster cannot answer for right now.
|
|
141
|
+
*
|
|
142
|
+
* Nothing when the cluster has no such session anywhere and every instance
|
|
143
|
+
* could be asked — which is the only case `session_not_found` covers (§4.2).
|
|
144
|
+
* While an instance is out of reach the sid may well be its, so the sender is
|
|
145
|
+
* told the reason rather than that the session does not exist. The message is
|
|
146
|
+
* not held here either: the inbox that would offer it again is the one on the
|
|
147
|
+
* instance that owns the session (§4.3). */
|
|
148
|
+
async #elsewhere(to: Sid, input: HandlerInput): Promise<MessageSendResult | undefined> {
|
|
149
|
+
const cluster = this.deps.cluster;
|
|
150
|
+
if (cluster === undefined) return undefined;
|
|
151
|
+
const owner = cluster.ownerOf(to);
|
|
152
|
+
if (owner === undefined || owner === this.deps.self) {
|
|
153
|
+
return cluster.anyUnreachable()
|
|
154
|
+
? { delivered: false, reason: "instance_unreachable" }
|
|
155
|
+
: undefined;
|
|
156
|
+
}
|
|
157
|
+
// The sender, as the owning instance will run the op as: the identity the
|
|
158
|
+
// connection greeted with, which is the same thing `message_send` reads to
|
|
159
|
+
// decide who a message is from (§4.1).
|
|
160
|
+
const answer = await cluster.forward(owner, input.args, callerOf(input));
|
|
161
|
+
if (answer.kind === "reply") {
|
|
162
|
+
const { ok: _ok, request_id: _id, ...body } = answer.response;
|
|
163
|
+
return body as unknown as MessageSendResult;
|
|
164
|
+
}
|
|
165
|
+
if (answer.kind === "error" && answer.response.error.code === "instance_unreachable") {
|
|
166
|
+
return { delivered: false, reason: "instance_unreachable" };
|
|
167
|
+
}
|
|
168
|
+
// Anything else is the owning instance refusing the op itself, and the
|
|
169
|
+
// refusal is its to state.
|
|
170
|
+
throw new OpError(
|
|
171
|
+
answer.kind === "error" ? answer.response.error.code : "internal_error",
|
|
172
|
+
answer.kind === "error" ? answer.response.error.msg : `${owner} did not answer`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Offer what is held to every session that might take it now.
|
|
177
|
+
*
|
|
178
|
+
* Called where the sessions domain says something about a session changed:
|
|
179
|
+
* one of the things that can have changed is a session being live again, and
|
|
180
|
+
* a session that is back is one route (a) can be tried against. Sessions with
|
|
181
|
+
* nothing waiting are not asked about, so the cost of a change nobody is owed
|
|
182
|
+
* anything after is one map read. */
|
|
183
|
+
retry = async (): Promise<void> => {
|
|
184
|
+
for (const sid of this.deps.inbox.sids()) {
|
|
185
|
+
const state = this.deps.sessions.classify(sid);
|
|
186
|
+
if (state === undefined || state === "paused" || state === "disappeared") continue;
|
|
187
|
+
await this.#offer(sid);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Hand a session what it is owed, oldest first, over route (a).
|
|
192
|
+
*
|
|
193
|
+
* Stops at the first message the route does not carry, whatever it answered:
|
|
194
|
+
* a refusal means the session is taking nothing more for now (§4.4), and an
|
|
195
|
+
* unavailable route means route (b) is the one that applies — either way the
|
|
196
|
+
* rest stay held, in order, for the next time this session becomes able to
|
|
197
|
+
* receive. */
|
|
198
|
+
async #offer(to: Sid): Promise<void> {
|
|
199
|
+
if (this.#offering.has(to)) return;
|
|
200
|
+
const held = this.deps.inbox.undelivered(to);
|
|
201
|
+
if (held.length === 0) return;
|
|
202
|
+
this.#offering.add(to);
|
|
203
|
+
this.#claimed.set(to, new Set(held.map((message) => message.mid)));
|
|
204
|
+
try {
|
|
205
|
+
for (const message of held) {
|
|
206
|
+
const outcome = await this.deps.direct.send(to, message);
|
|
207
|
+
// Out of the inbox one at a time rather than in one batch at the end:
|
|
208
|
+
// an offer interrupted partway through has still delivered what it
|
|
209
|
+
// delivered, and a daemon killed here must not offer those again.
|
|
210
|
+
if (outcome !== "delivered") break;
|
|
211
|
+
this.#claimed.get(to)?.delete(message.mid);
|
|
212
|
+
this.deps.inbox.delivered(to, [message.mid]);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
this.#claimed.delete(to);
|
|
216
|
+
this.#offering.delete(to);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- UpstreamResource (§6.3)
|
|
221
|
+
|
|
222
|
+
/** Nothing upstream to run: what is undelivered is already in hand, and the
|
|
223
|
+
* messages that arrive later come through `send`. */
|
|
224
|
+
start(): void {}
|
|
225
|
+
|
|
226
|
+
stop(): void {}
|
|
227
|
+
|
|
228
|
+
/** The current value of `inbox` for whoever subscribed: everything still
|
|
229
|
+
* undelivered for that session (§6.2, element granularity — the snapshot is
|
|
230
|
+
* every element, a later frame is one).
|
|
231
|
+
*
|
|
232
|
+
* Subscribing is receiving, so the snapshot empties the inbox: the frame is
|
|
233
|
+
* queued on the connection before this returns, and a message the session has
|
|
234
|
+
* been handed is not one that is still waiting for it (§4.3). A connection
|
|
235
|
+
* with no session — a person watching — is handed nothing, because the topic
|
|
236
|
+
* carries what was said to a session and they are not one.
|
|
237
|
+
*
|
|
238
|
+
* A message an offer over route (a) has claimed is left out: it is on its way
|
|
239
|
+
* on the other route, and the session subscribing while that runs must not
|
|
240
|
+
* make it two messages. */
|
|
241
|
+
snapshot(topic: string, conn: Requester): readonly TopicValue[] {
|
|
242
|
+
const identity = conn.identity;
|
|
243
|
+
const sid = identity.state === "settled" ? identity.sid : undefined;
|
|
244
|
+
if (topic !== INBOX || sid === undefined) return [];
|
|
245
|
+
const claimed = this.#claimed.get(sid);
|
|
246
|
+
const held = this.deps.inbox
|
|
247
|
+
.undelivered(sid)
|
|
248
|
+
.filter((message) => claimed?.has(message.mid) !== true);
|
|
249
|
+
this.deps.inbox.delivered(
|
|
250
|
+
sid,
|
|
251
|
+
held.map((message) => message.mid),
|
|
252
|
+
);
|
|
253
|
+
return [{ instance: this.deps.self, data: held }];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The reason a message is waiting, named from the classification alone
|
|
257
|
+
* (§4.2). `preparing` is the live session with nowhere to put it: it is there,
|
|
258
|
+
* route (a) did not carry it, and nothing of its is listening yet.
|
|
259
|
+
*
|
|
260
|
+
* `instance_unreachable` is not here: it is the mesh's answer about an
|
|
261
|
+
* instance, and this instance holds every session it can classify. */
|
|
262
|
+
#reason(state: SessionState): UndeliveredReason {
|
|
263
|
+
switch (state) {
|
|
264
|
+
case "paused":
|
|
265
|
+
return "paused";
|
|
266
|
+
case "disappeared":
|
|
267
|
+
return "disappeared";
|
|
268
|
+
case "live":
|
|
269
|
+
case "live_unmanaged":
|
|
270
|
+
case "waiting":
|
|
271
|
+
return "preparing";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The answer for a message that went to the inbox. Candidates ride along
|
|
276
|
+
* when the addressee is gone, since that is when sending somewhere else is
|
|
277
|
+
* the sender's next move (§4.2). */
|
|
278
|
+
#undelivered(to: Sid, reason: UndeliveredReason): MessageSendResult {
|
|
279
|
+
if (reason !== "paused" && reason !== "disappeared") return { delivered: false, reason };
|
|
280
|
+
const candidates = this.#candidates(to);
|
|
281
|
+
return candidates.length === 0
|
|
282
|
+
? { delivered: false, reason }
|
|
283
|
+
: { delivered: false, reason, candidates };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Sessions live now in the repository the addressee belongs to (§4.2).
|
|
287
|
+
*
|
|
288
|
+
* The repository is `repo_root` as the session named it. A session that named
|
|
289
|
+
* none is left out rather than matched on something derived from its `cwd`:
|
|
290
|
+
* no primary source states that derivation, and the sessions domain does not
|
|
291
|
+
* make one up either. */
|
|
292
|
+
#candidates(to: Sid): CandidateSession[] {
|
|
293
|
+
const { peers, last_live } = this.deps.sessions.peers();
|
|
294
|
+
const root = [...peers, ...last_live].find((row) => row.sid === to)?.repo_root;
|
|
295
|
+
if (root === undefined) return [];
|
|
296
|
+
return peers
|
|
297
|
+
.filter((peer) => peer.sid !== to && peer.repo_root === root)
|
|
298
|
+
.map((peer) => ({
|
|
299
|
+
sid: peer.sid,
|
|
300
|
+
...(peer.ws === "" ? {} : { ws: peer.ws }),
|
|
301
|
+
instance: peer.instance,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Who the message is from: the identity the connection greeted as, never
|
|
306
|
+
* anything the caller put in the arguments (§4.1).
|
|
307
|
+
*
|
|
308
|
+
* A session names itself with its sid. A person greets without one, which is
|
|
309
|
+
* what the sender literal stands for — spelled out so a reader tells "a
|
|
310
|
+
* person sent this" from "a session sent this and the id was lost". The
|
|
311
|
+
* absence is read rather than the role, because the op is open to those two
|
|
312
|
+
* and no other, so a settled greeting with no sid is a person by elimination.
|
|
313
|
+
* An unsettled connection names no sender at all, and a message with no
|
|
314
|
+
* sender is one nobody can answer. */
|
|
315
|
+
#sender(input: HandlerInput): Sender {
|
|
316
|
+
const identity = input.identity;
|
|
317
|
+
if (identity === undefined) {
|
|
318
|
+
throw new OpError("bad_request", "a message is sent by a greeting, and this one made none");
|
|
319
|
+
}
|
|
320
|
+
return identity.sid ?? USER_SENDER;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#message(args: MessageSendArgs, from: Sender, now: Timestamp = Date.now()): InboxMessage {
|
|
324
|
+
return {
|
|
325
|
+
mid: this.#mid(),
|
|
326
|
+
from,
|
|
327
|
+
from_label: this.#label(from),
|
|
328
|
+
text: args.text,
|
|
329
|
+
...(args.reply_to === undefined ? {} : { reply_to: args.reply_to }),
|
|
330
|
+
sent_at: now,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** `<instance>/<counter>`, numbered by this instance. The counter starts
|
|
335
|
+
* above the highest one a held message carries, so a restart cannot reissue a
|
|
336
|
+
* `mid` that a `reply_to` still points at. */
|
|
337
|
+
#mid(): Mid {
|
|
338
|
+
this.#counter += 1;
|
|
339
|
+
return `${this.deps.self}/${this.#counter}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** How the sender is shown. The repository and workspace it greeted from,
|
|
343
|
+
* which is what tells two sessions of one person apart; its sid when it
|
|
344
|
+
* greeted from neither, so the label always names something. A person is
|
|
345
|
+
* shown as the sender literal: there is one person per instance to a
|
|
346
|
+
* session's eye, so there is nothing further to tell apart. */
|
|
347
|
+
#label(from: Sender): string {
|
|
348
|
+
return from === USER_SENDER ? USER_SENDER : sessionLabel(this.deps.sessions, from);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Who is asking, as another instance is told it (contract, `CallerIdentity`).
|
|
353
|
+
* A connection with no settled greeting names nobody, and `message_send`
|
|
354
|
+
* refuses it before this is reached. */
|
|
355
|
+
function callerOf(input: HandlerInput): CallerIdentity | undefined {
|
|
356
|
+
const identity = input.identity;
|
|
357
|
+
if (identity === undefined) return undefined;
|
|
358
|
+
return identity.sid === undefined
|
|
359
|
+
? { role: identity.role }
|
|
360
|
+
: { role: identity.role, sid: identity.sid };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** How a session is shown: the repository and workspace it greeted from, which
|
|
364
|
+
* is what tells two sessions of one person apart, and its sid when it greeted
|
|
365
|
+
* from neither so the label always names something.
|
|
366
|
+
*
|
|
367
|
+
* Written once for every label the instance resolves — a message's sender and a
|
|
368
|
+
* notification's subject are the same session seen from two ops, and a session
|
|
369
|
+
* shown one way there and another way here would read as two. */
|
|
370
|
+
export function sessionLabel(sessions: SessionLookup, sid: Sid): string {
|
|
371
|
+
const peer = sessions.peers().peers.find((row) => row.sid === sid);
|
|
372
|
+
if (peer === undefined) return sid;
|
|
373
|
+
const where = [peer.repo, peer.ws].filter((part) => part !== "").join("/");
|
|
374
|
+
return where === "" ? sid : where;
|
|
375
|
+
}
|