@fitchmultz/pi-subagents 0.52.20 → 0.52.22
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/CHANGELOG.md +46 -2
- package/dist/agent/contracts.js +12 -3
- package/dist/agent/index.js +9 -9
- package/dist/agent/legacy.js +5 -5
- package/dist/agent/notifications.js +21 -14
- package/dist/agent/parent-check.js +38 -0
- package/dist/agent/process.js +45 -52
- package/dist/agent/runner.js +59 -64
- package/dist/agent/storage.js +16 -4
- package/dist/agent/view.js +1 -3
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-wake-state.js +31 -6
- package/dist/pi-intercom/bridge-wake.js +4 -10
- package/dist/pi-intercom/broker/broker.js +4 -26
- package/dist/pi-intercom/broker/client-park.d.ts +1 -1
- package/dist/pi-intercom/broker/client-park.js +22 -6
- package/dist/pi-intercom/broker/client.d.ts +3 -2
- package/dist/pi-intercom/broker/client.js +88 -62
- package/dist/pi-intercom/broker/connection.d.ts +3 -5
- package/dist/pi-intercom/broker/connection.js +48 -33
- package/dist/pi-intercom/broker/framing.d.ts +1 -1
- package/dist/pi-intercom/broker/framing.js +19 -12
- package/dist/pi-intercom/broker/replace-deliveries.js +38 -0
- package/dist/pi-intercom/connection.js +5 -8
- package/dist/pi-intercom/inbound-journal.js +5 -2
- package/dist/shared/journal-frames.js +19 -24
- package/docs/development.md +6 -0
- package/package.json +1 -1
- package/src/agent/contracts.ts +12 -3
- package/src/agent/index.ts +10 -10
- package/src/agent/legacy.ts +5 -5
- package/src/agent/notifications.ts +35 -15
- package/src/agent/parent-check.ts +40 -0
- package/src/agent/process.ts +53 -55
- package/src/agent/runner.ts +65 -65
- package/src/agent/storage.ts +18 -5
- package/src/agent/view.ts +1 -4
- package/src/pi-intercom/bridge-wake-state.ts +39 -8
- package/src/pi-intercom/bridge-wake.ts +4 -9
- package/src/pi-intercom/broker/broker.ts +5 -37
- package/src/pi-intercom/broker/client-park.ts +27 -6
- package/src/pi-intercom/broker/client.ts +91 -61
- package/src/pi-intercom/broker/connection.ts +58 -39
- package/src/pi-intercom/broker/framing.ts +24 -12
- package/src/pi-intercom/broker/replace-deliveries.ts +56 -0
- package/src/pi-intercom/connection.ts +9 -9
- package/src/pi-intercom/inbound-journal.ts +5 -2
- package/src/shared/journal-frames.ts +22 -23
package/dist/agent/storage.js
CHANGED
|
@@ -118,13 +118,25 @@ export function latest(base, initial) {
|
|
|
118
118
|
export function conversation(base, launch) {
|
|
119
119
|
return runDirectory(base, launch.rootId);
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
/** Metadata only: replacing command bytes invalidates the actor's parsed-command cache. */
|
|
122
|
+
export function commandIdentity(file) {
|
|
123
|
+
try {
|
|
124
|
+
const stat = fs.statSync(file, { bigint: true });
|
|
125
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (missing(error)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function enqueue(base, launch, input) {
|
|
135
|
+
const id = input.action === "run" ? launch.id : randomUUID();
|
|
123
136
|
const command = {
|
|
124
137
|
id,
|
|
125
138
|
runId: launch.id,
|
|
126
|
-
|
|
127
|
-
...options,
|
|
139
|
+
...input,
|
|
128
140
|
createdAt: Date.now(),
|
|
129
141
|
};
|
|
130
142
|
write(path.join(conversation(base, launch), "commands", `${id}.json`), command, true);
|
package/dist/agent/view.js
CHANGED
|
@@ -2,12 +2,10 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readOutputPage } from "../shared/output-page.js";
|
|
4
4
|
import { actor, alive, observeAlive } from "./process.js";
|
|
5
|
-
import { admissionSchema,
|
|
5
|
+
import { admissionSchema, commandMetadataSchema, exitSchema, successorSchema, wakeFailureSchema, } from "./contracts.js";
|
|
6
6
|
import { conversation, read, runDirectory } from "./storage.js";
|
|
7
7
|
import { published } from "./notifications.js";
|
|
8
8
|
import { accountingFields, accountingState, readCanonicalResult, readResult } from "./usage.js";
|
|
9
|
-
import { Type } from "./typebox.js";
|
|
10
|
-
const commandMetadataSchema = Type.Pick(commandSchema, ["id", "runId", "action", "createdAt"]);
|
|
11
9
|
function toolResult(value, text) {
|
|
12
10
|
return { content: [{ type: "text", text }], details: value };
|
|
13
11
|
}
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.22","sha256":"fc7e48d1fb96f143f56be9757d30a98bb138b94edd1f5388438ddeb3eed54638"});
|
|
@@ -28,7 +28,7 @@ export class WakeState {
|
|
|
28
28
|
inode;
|
|
29
29
|
device;
|
|
30
30
|
owner;
|
|
31
|
-
|
|
31
|
+
phase = { kind: "idle" };
|
|
32
32
|
constructor(stateDir) {
|
|
33
33
|
this.stateDir = stateDir;
|
|
34
34
|
this.path = join(stateDir, FILE);
|
|
@@ -194,13 +194,37 @@ export class WakeState {
|
|
|
194
194
|
this.validate();
|
|
195
195
|
this.db.prepare("DELETE FROM wake_batches WHERE bucket=?").run(bucket);
|
|
196
196
|
}
|
|
197
|
-
/** Persist quota and uncertainty
|
|
197
|
+
/** Persist quota and uncertainty before exposing a single-use POST reservation. */
|
|
198
198
|
reserve(bucket) {
|
|
199
199
|
this.retrySettlement();
|
|
200
|
-
|
|
200
|
+
if (this.phase.kind !== "idle") {
|
|
201
|
+
throw new Error("A wake reservation is already active or its owner is closed.");
|
|
202
|
+
}
|
|
203
|
+
const until = this.transaction(() => this.reservation(bucket));
|
|
204
|
+
if (until !== undefined) {
|
|
205
|
+
return { kind: "deferred", until };
|
|
206
|
+
}
|
|
207
|
+
const reservation = {
|
|
208
|
+
kind: "reserved",
|
|
209
|
+
post: async (action) => {
|
|
210
|
+
if (this.phase !== reservation) {
|
|
211
|
+
throw new Error("This wake reservation is no longer available.");
|
|
212
|
+
}
|
|
213
|
+
this.phase = { kind: "posting" };
|
|
214
|
+
try {
|
|
215
|
+
await action();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
// Completion, rejection and abort all start the durable expiry window here.
|
|
219
|
+
this.settle();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
this.phase = reservation;
|
|
224
|
+
return reservation;
|
|
201
225
|
}
|
|
202
226
|
retrySettlement() {
|
|
203
|
-
if (this.
|
|
227
|
+
if (this.phase.kind === "settlementPending") {
|
|
204
228
|
this.settle();
|
|
205
229
|
}
|
|
206
230
|
}
|
|
@@ -219,9 +243,9 @@ export class WakeState {
|
|
|
219
243
|
}
|
|
220
244
|
/** A failed settlement stays open and must be retried before this owner can dispatch again. */
|
|
221
245
|
settle() {
|
|
222
|
-
this.
|
|
246
|
+
this.phase = { kind: "settlementPending" };
|
|
223
247
|
this.transaction(() => this.finishOwner(this.owner.name));
|
|
224
|
-
this.
|
|
248
|
+
this.phase = { kind: "idle" };
|
|
225
249
|
}
|
|
226
250
|
finishOwner(owner) {
|
|
227
251
|
const now = Math.max(Date.now(), integer(this.db.prepare("SELECT clock FROM wake_meta").get()?.clock));
|
|
@@ -286,6 +310,7 @@ export class WakeState {
|
|
|
286
310
|
this.settle();
|
|
287
311
|
}
|
|
288
312
|
finally {
|
|
313
|
+
this.phase = { kind: "closed" };
|
|
289
314
|
this.db.close();
|
|
290
315
|
this.owner.close();
|
|
291
316
|
}
|
|
@@ -171,20 +171,14 @@ export class WakeDeliveries {
|
|
|
171
171
|
next = Math.min(next, due);
|
|
172
172
|
continue;
|
|
173
173
|
}
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
next = Math.min(next,
|
|
174
|
+
const reservation = state.reserve(key);
|
|
175
|
+
if (reservation.kind === "deferred") {
|
|
176
|
+
next = Math.min(next, reservation.until);
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
// ponytail: two cold Macs can each admit one wake. Literal cross-Mac single admission
|
|
180
180
|
// requires a neutral quota authority governing every writer, not a URL hash or HTTP 2xx.
|
|
181
|
-
|
|
182
|
-
await this.post(candidate);
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
// Expiry starts after response/error/abort, never before COMMIT or connect/TLS.
|
|
186
|
-
state.settle();
|
|
187
|
-
}
|
|
181
|
+
await reservation.post(() => this.post(candidate));
|
|
188
182
|
// Re-read ACKs and all groups after transport I/O; a snapshot must not wake already consumed mail.
|
|
189
183
|
return Date.now();
|
|
190
184
|
}
|
|
@@ -14,13 +14,13 @@ import { guardedDeliveryError } from "./guard.js";
|
|
|
14
14
|
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
15
15
|
import { errorMessage, isRecord } from "../../shared/unknown.js";
|
|
16
16
|
import { messageSender, snapshotFrames, TopicSnapshot, publicationMessage, preparedPublication, deliveryAcknowledgement, shouldReplaceDelivery, } from "./snapshots.js";
|
|
17
|
+
import { ReplaceDeliveries } from "./replace-deliveries.js";
|
|
17
18
|
const PID_PATH = join(getBrokerRuntimeDir(), "broker.pid");
|
|
18
|
-
const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
19
19
|
class IntercomBroker {
|
|
20
20
|
sessions = new Map();
|
|
21
21
|
lastMessages = new LastMessageRelay(this.sessions);
|
|
22
22
|
parking = new ParkedSessions(this.sessions, this.deliverMessage.bind(this));
|
|
23
|
-
|
|
23
|
+
replacements = new ReplaceDeliveries(this.deliverMessage.bind(this));
|
|
24
24
|
server;
|
|
25
25
|
shutdownTimer = null;
|
|
26
26
|
socketPath;
|
|
@@ -153,7 +153,7 @@ class IntercomBroker {
|
|
|
153
153
|
this.lastMessages.disconnected(sessionId);
|
|
154
154
|
if (!this.parking.closed(session)) {
|
|
155
155
|
this.sessions.delete(sessionId);
|
|
156
|
-
this.
|
|
156
|
+
this.replacements.disconnected(sessionId);
|
|
157
157
|
this.broadcast({ type: "session_left", sessionId }, session.info);
|
|
158
158
|
}
|
|
159
159
|
this.scheduleShutdownCheck();
|
|
@@ -348,25 +348,12 @@ class IntercomBroker {
|
|
|
348
348
|
...(comms ? { lastIntercomActivity: now } : {}),
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
|
-
replaceKey(fromId, toId, threadId) {
|
|
352
|
-
return `${fromId}\0${toId}\0${threadId}`;
|
|
353
|
-
}
|
|
354
351
|
queueReplaceDelivery(fromId, toId, from, message) {
|
|
355
352
|
const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId));
|
|
356
353
|
if (validationError !== null) {
|
|
357
354
|
return { id: message.id, accepted: false, delivered: false, reason: validationError };
|
|
358
355
|
}
|
|
359
|
-
|
|
360
|
-
const existing = this.pendingReplaceDeliveries.get(key);
|
|
361
|
-
if (existing) {
|
|
362
|
-
clearTimeout(existing.timer);
|
|
363
|
-
}
|
|
364
|
-
const timer = setTimeout(() => {
|
|
365
|
-
this.pendingReplaceDeliveries.delete(key);
|
|
366
|
-
this.deliverMessage(toId, from, message);
|
|
367
|
-
}, REPLACE_DELIVERY_DELAY_MS);
|
|
368
|
-
timer.unref();
|
|
369
|
-
this.pendingReplaceDeliveries.set(key, { fromId, toId, message, timer });
|
|
356
|
+
this.replacements.stage(fromId, toId, from, message);
|
|
370
357
|
return {
|
|
371
358
|
id: message.id,
|
|
372
359
|
accepted: true,
|
|
@@ -375,15 +362,6 @@ class IntercomBroker {
|
|
|
375
362
|
reason: "Queued for replace-mode delivery",
|
|
376
363
|
};
|
|
377
364
|
}
|
|
378
|
-
clearPendingReplaceDeliveries(sessionId) {
|
|
379
|
-
for (const [key, pending] of this.pendingReplaceDeliveries) {
|
|
380
|
-
if (pending.toId === sessionId ||
|
|
381
|
-
(pending.fromId === sessionId && pending.message.expectsReply === true)) {
|
|
382
|
-
clearTimeout(pending.timer);
|
|
383
|
-
this.pendingReplaceDeliveries.delete(key);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
365
|
/** One admission rule; retained input is checked as the uncompacted envelope any later
|
|
388
366
|
* transport generation accepts. */
|
|
389
367
|
validateDeliveryPayload(from, message, target) {
|
|
@@ -2,7 +2,7 @@ import type net from "node:net";
|
|
|
2
2
|
/** Parking negotiation and its single outstanding broker acknowledgement. */
|
|
3
3
|
export declare class ParkRequests {
|
|
4
4
|
supported: boolean;
|
|
5
|
-
private
|
|
5
|
+
private state;
|
|
6
6
|
settle(parked: boolean): void;
|
|
7
7
|
/** True once the broker retains later input; false when it cannot or the socket closed. */
|
|
8
8
|
request(socket: () => net.Socket, on: boolean): Promise<boolean>;
|
|
@@ -2,20 +2,36 @@ import { writeMessage } from "./framing.js";
|
|
|
2
2
|
/** Parking negotiation and its single outstanding broker acknowledgement. */
|
|
3
3
|
export class ParkRequests {
|
|
4
4
|
supported = false;
|
|
5
|
-
|
|
5
|
+
state = { kind: "idle" };
|
|
6
6
|
settle(parked) {
|
|
7
|
-
this.
|
|
7
|
+
const state = this.state;
|
|
8
|
+
this.state = { kind: "idle" };
|
|
9
|
+
if (state.kind === "pending") {
|
|
10
|
+
state.resolve(parked);
|
|
11
|
+
}
|
|
8
12
|
}
|
|
9
13
|
/** True once the broker retains later input; false when it cannot or the socket closed. */
|
|
10
14
|
request(socket, on) {
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
if (on && this.state.kind === "pending") {
|
|
16
|
+
return this.state.promise;
|
|
17
|
+
}
|
|
18
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
19
|
+
this.settle(false);
|
|
20
|
+
if (on && this.supported) {
|
|
21
|
+
this.state = { kind: "pending", promise, resolve };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
13
24
|
if (this.supported) {
|
|
14
25
|
writeMessage(socket(), { type: on ? "park" : "unpark" });
|
|
15
26
|
}
|
|
16
|
-
if (this.
|
|
27
|
+
if (this.state.kind === "idle") {
|
|
17
28
|
resolve(false);
|
|
18
29
|
}
|
|
19
|
-
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
this.state = { kind: "idle" };
|
|
33
|
+
reject(error);
|
|
34
|
+
}
|
|
35
|
+
return promise;
|
|
20
36
|
}
|
|
21
37
|
}
|
|
@@ -18,8 +18,7 @@ export declare class IntercomClient extends EventEmitter {
|
|
|
18
18
|
private readonly lastRequests;
|
|
19
19
|
private readonly pendingSends;
|
|
20
20
|
private readonly pendingLists;
|
|
21
|
-
private
|
|
22
|
-
private disconnecting;
|
|
21
|
+
private phase;
|
|
23
22
|
private readonly sendTimeoutMs;
|
|
24
23
|
private readonly listTimeoutMs;
|
|
25
24
|
constructor(options?: IntercomClientOptions);
|
|
@@ -30,6 +29,7 @@ export declare class IntercomClient extends EventEmitter {
|
|
|
30
29
|
isConnected(): boolean;
|
|
31
30
|
private requireActiveSocket;
|
|
32
31
|
connect(session: Omit<SessionInfo, "id">, requestedId?: string): Promise<void>;
|
|
32
|
+
private establish;
|
|
33
33
|
private restoreTopics;
|
|
34
34
|
private handleBrokerMessage;
|
|
35
35
|
private receiveRegistration;
|
|
@@ -39,6 +39,7 @@ export declare class IntercomClient extends EventEmitter {
|
|
|
39
39
|
private receiveSessions;
|
|
40
40
|
park(on?: boolean): Promise<boolean>;
|
|
41
41
|
disconnect(): Promise<void>;
|
|
42
|
+
private close;
|
|
42
43
|
listSessions(): Promise<SessionInfo[]>;
|
|
43
44
|
last(to: string, query?: LastMessageQuery): Promise<LastMessageResult>;
|
|
44
45
|
receipt(to: string, query: ReceiptQuery): Promise<ReceiptResult>;
|
|
@@ -39,8 +39,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
39
39
|
lastRequests;
|
|
40
40
|
pendingSends = new PendingRequests();
|
|
41
41
|
pendingLists = new PendingRequests();
|
|
42
|
-
|
|
43
|
-
disconnecting = false;
|
|
42
|
+
phase = { kind: "idle" };
|
|
44
43
|
sendTimeoutMs;
|
|
45
44
|
listTimeoutMs;
|
|
46
45
|
constructor(options = {}) {
|
|
@@ -65,11 +64,11 @@ export class IntercomClient extends EventEmitter {
|
|
|
65
64
|
}
|
|
66
65
|
isConnected() {
|
|
67
66
|
return ((this._sessionId ?? "").length > 0 &&
|
|
68
|
-
|
|
67
|
+
this.phase.kind !== "closing" &&
|
|
69
68
|
this.connection?.isActive() === true);
|
|
70
69
|
}
|
|
71
70
|
requireActiveSocket() {
|
|
72
|
-
if (this.
|
|
71
|
+
if (this.phase.kind === "closing") {
|
|
73
72
|
throw new Error("Client disconnecting");
|
|
74
73
|
}
|
|
75
74
|
const connection = this.connection;
|
|
@@ -82,62 +81,72 @@ export class IntercomClient extends EventEmitter {
|
|
|
82
81
|
return connection.socket;
|
|
83
82
|
}
|
|
84
83
|
async connect(session, requestedId) {
|
|
85
|
-
if (this.connection || this.
|
|
84
|
+
if (this.connection || this.phase.kind !== "idle") {
|
|
86
85
|
throw new Error("Already connected");
|
|
87
86
|
}
|
|
88
|
-
this.
|
|
87
|
+
const task = this.establish(session, requestedId);
|
|
88
|
+
this.phase = { kind: "connecting", task };
|
|
89
89
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
closed: (error, expected) => {
|
|
97
|
-
if (this.connection !== connection) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const established = this._sessionId !== null;
|
|
101
|
-
this.connection = null;
|
|
102
|
-
this._sessionId = null;
|
|
103
|
-
this._topicsSupported = false;
|
|
104
|
-
this.parking.settle(false);
|
|
105
|
-
this.guardedDeliverySupported = false;
|
|
106
|
-
this.wakes.supported = false;
|
|
107
|
-
this.disconnecting = false;
|
|
108
|
-
this.failPending(error);
|
|
109
|
-
if (established && !expected) {
|
|
110
|
-
this.emit("disconnected", error);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
this.connection = connection;
|
|
115
|
-
const { topics, subscriptions, ...identity } = session;
|
|
116
|
-
try {
|
|
117
|
-
await connection.start({
|
|
118
|
-
type: "register",
|
|
119
|
-
session: identity,
|
|
120
|
-
requestedId,
|
|
121
|
-
topicFrames: true,
|
|
122
|
-
lastMessages: true,
|
|
123
|
-
parking: true,
|
|
124
|
-
receiptReads: 1,
|
|
125
|
-
guardedDelivery: 1,
|
|
126
|
-
...this.wakes.registration(),
|
|
127
|
-
});
|
|
90
|
+
await task;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (this.phase.kind === "connecting" && this.phase.task === task) {
|
|
94
|
+
this.phase = { kind: this.isConnected() ? "connected" : "idle" };
|
|
128
95
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async establish(session, requestedId) {
|
|
99
|
+
const socket = await connectBrokerSocket();
|
|
100
|
+
const connection = new BrokerConnection(socket, {
|
|
101
|
+
message: (message) => this.handleBrokerMessage(message),
|
|
102
|
+
error: (error) => {
|
|
103
|
+
this.emit("error", error);
|
|
104
|
+
},
|
|
105
|
+
closed: (error, expected) => {
|
|
106
|
+
if (this.connection !== connection) {
|
|
107
|
+
return;
|
|
132
108
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
109
|
+
const established = this._sessionId !== null;
|
|
110
|
+
this.connection = null;
|
|
111
|
+
this._sessionId = null;
|
|
112
|
+
this._topicsSupported = false;
|
|
113
|
+
this.parking.settle(false);
|
|
114
|
+
this.guardedDeliverySupported = false;
|
|
115
|
+
this.wakes.supported = false;
|
|
116
|
+
if (this.phase.kind === "connected") {
|
|
117
|
+
this.phase = { kind: "idle" };
|
|
118
|
+
}
|
|
119
|
+
this.failPending(error);
|
|
120
|
+
if (established && !expected) {
|
|
121
|
+
this.emit("disconnected", error);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
this.connection = connection;
|
|
126
|
+
if (this.phase.kind === "closing") {
|
|
127
|
+
await connection.disconnect();
|
|
128
|
+
throw new Error("Client disconnecting");
|
|
138
129
|
}
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
const { topics, subscriptions, ...identity } = session;
|
|
131
|
+
try {
|
|
132
|
+
await connection.start({
|
|
133
|
+
type: "register",
|
|
134
|
+
session: identity,
|
|
135
|
+
requestedId,
|
|
136
|
+
topicFrames: true,
|
|
137
|
+
lastMessages: true,
|
|
138
|
+
parking: true,
|
|
139
|
+
receiptReads: 1,
|
|
140
|
+
guardedDelivery: 1,
|
|
141
|
+
...this.wakes.registration(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await connection.disconnect();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (this.supportsTopics) {
|
|
149
|
+
await this.restoreTopics(topics, subscriptions);
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
async restoreTopics(topics, subscriptions) {
|
|
@@ -271,14 +280,31 @@ export class IntercomClient extends EventEmitter {
|
|
|
271
280
|
park(on = true) {
|
|
272
281
|
return this.parking.request(() => this.requireActiveSocket(), on);
|
|
273
282
|
}
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
283
|
+
disconnect() {
|
|
284
|
+
const phase = this.phase;
|
|
285
|
+
if (phase.kind === "closing") {
|
|
286
|
+
return phase.task;
|
|
287
|
+
}
|
|
288
|
+
const connecting = phase.kind === "connecting" ? phase.task : undefined;
|
|
289
|
+
const task = Promise.resolve().then(() => this.close(connecting));
|
|
290
|
+
this.phase = { kind: "closing", task };
|
|
291
|
+
return task;
|
|
292
|
+
}
|
|
293
|
+
async close(connecting) {
|
|
280
294
|
this.failPending(new Error("Client disconnected"));
|
|
281
|
-
|
|
295
|
+
try {
|
|
296
|
+
await this.connection?.disconnect();
|
|
297
|
+
try {
|
|
298
|
+
await connecting;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// The connect caller owns acquisition failure; close still joins any acquired socket.
|
|
302
|
+
}
|
|
303
|
+
await this.connection?.disconnect();
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
this.phase = { kind: "idle" };
|
|
307
|
+
}
|
|
282
308
|
}
|
|
283
309
|
listSessions() {
|
|
284
310
|
return this.requestSessions().then((snapshot) => [...snapshot.sessions]);
|
|
@@ -399,7 +425,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
399
425
|
});
|
|
400
426
|
}
|
|
401
427
|
updatePresence(updates) {
|
|
402
|
-
if (this.
|
|
428
|
+
if (this.phase.kind === "closing") {
|
|
403
429
|
return;
|
|
404
430
|
}
|
|
405
431
|
const socket = this.connection?.socket;
|
|
@@ -7,22 +7,20 @@ interface ConnectionHandlers {
|
|
|
7
7
|
}
|
|
8
8
|
/** One socket owns registration, read/error listeners, and graceful shutdown. */
|
|
9
9
|
export declare class BrokerConnection {
|
|
10
|
-
private
|
|
11
|
-
private closing;
|
|
10
|
+
private phase;
|
|
12
11
|
private failure;
|
|
13
|
-
private registration;
|
|
14
|
-
private registrationTimer;
|
|
15
12
|
private readonly reader;
|
|
16
13
|
readonly socket: net.Socket;
|
|
17
14
|
private readonly handlers;
|
|
18
15
|
constructor(socket: net.Socket, handlers: ConnectionHandlers);
|
|
19
16
|
start(registration: unknown): Promise<void>;
|
|
20
17
|
confirmRegistration(): void;
|
|
21
|
-
private
|
|
18
|
+
private takeRegistration;
|
|
22
19
|
private failRegistration;
|
|
23
20
|
private protocolError;
|
|
24
21
|
private readonly onError;
|
|
25
22
|
private readonly onClose;
|
|
23
|
+
private get registered();
|
|
26
24
|
isActive(): boolean;
|
|
27
25
|
disconnect(): Promise<void>;
|
|
28
26
|
}
|
|
@@ -34,11 +34,8 @@ export async function connectBrokerSocket() {
|
|
|
34
34
|
}
|
|
35
35
|
/** One socket owns registration, read/error listeners, and graceful shutdown. */
|
|
36
36
|
export class BrokerConnection {
|
|
37
|
-
|
|
38
|
-
closing = false;
|
|
37
|
+
phase = { kind: "acquired" };
|
|
39
38
|
failure = null;
|
|
40
|
-
registration = null;
|
|
41
|
-
registrationTimer = null;
|
|
42
39
|
reader;
|
|
43
40
|
socket;
|
|
44
41
|
handlers;
|
|
@@ -52,9 +49,9 @@ export class BrokerConnection {
|
|
|
52
49
|
}
|
|
53
50
|
start(registration) {
|
|
54
51
|
return new Promise((resolve, reject) => {
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
this.
|
|
52
|
+
const timer = setTimeout(() => this.failRegistration(new Error("Connection timeout")), 10000);
|
|
53
|
+
timer.unref();
|
|
54
|
+
this.phase = { kind: "registering", resolve, reject, timer };
|
|
58
55
|
try {
|
|
59
56
|
writeMessage(this.socket, registration);
|
|
60
57
|
}
|
|
@@ -64,21 +61,26 @@ export class BrokerConnection {
|
|
|
64
61
|
});
|
|
65
62
|
}
|
|
66
63
|
confirmRegistration() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
const pending = this.takeRegistration();
|
|
65
|
+
if (pending !== undefined) {
|
|
66
|
+
this.phase = { kind: "active" };
|
|
67
|
+
pending.resolve();
|
|
68
|
+
}
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
takeRegistration() {
|
|
71
|
+
const pending = this.phase;
|
|
72
|
+
if (pending.kind !== "registering") {
|
|
73
|
+
return;
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
this.
|
|
75
|
+
clearTimeout(pending.timer);
|
|
76
|
+
this.phase = { kind: "acquired" };
|
|
77
|
+
return pending;
|
|
78
78
|
}
|
|
79
79
|
failRegistration(error) {
|
|
80
|
-
const pending = this.
|
|
81
|
-
this.
|
|
80
|
+
const pending = this.takeRegistration();
|
|
81
|
+
if (this.phase.kind !== "closing") {
|
|
82
|
+
this.phase = { kind: "failed" };
|
|
83
|
+
}
|
|
82
84
|
this.failure = error;
|
|
83
85
|
this.socket.destroy();
|
|
84
86
|
pending?.reject(error);
|
|
@@ -102,31 +104,37 @@ export class BrokerConnection {
|
|
|
102
104
|
this.handlers.error(error);
|
|
103
105
|
};
|
|
104
106
|
onClose = () => {
|
|
105
|
-
const
|
|
106
|
-
this.
|
|
107
|
+
const closing = this.phase.kind === "closing";
|
|
108
|
+
const pending = this.takeRegistration();
|
|
109
|
+
this.phase = { kind: "closed" };
|
|
107
110
|
this.socket.off("data", this.reader);
|
|
108
111
|
this.socket.off("error", this.onError);
|
|
109
112
|
this.socket.off("close", this.onClose);
|
|
110
113
|
pending?.reject(new Error("Connection closed before registration"));
|
|
111
|
-
this.handlers.closed(this.failure ?? new Error("Client disconnected"),
|
|
114
|
+
this.handlers.closed(this.failure ?? new Error("Client disconnected"), closing);
|
|
112
115
|
};
|
|
116
|
+
get registered() {
|
|
117
|
+
return this.phase.kind === "active" || (this.phase.kind === "closing" && this.phase.registered);
|
|
118
|
+
}
|
|
113
119
|
isActive() {
|
|
114
|
-
return (this.
|
|
115
|
-
!this.closing &&
|
|
120
|
+
return (this.phase.kind === "active" &&
|
|
116
121
|
!this.socket.destroyed &&
|
|
117
122
|
!this.socket.writableEnded &&
|
|
118
123
|
this.socket.writable);
|
|
119
124
|
}
|
|
120
125
|
disconnect() {
|
|
121
|
-
|
|
126
|
+
const phase = this.phase;
|
|
127
|
+
if (phase.kind === "closing") {
|
|
128
|
+
return phase.task;
|
|
129
|
+
}
|
|
130
|
+
if (phase.kind === "closed") {
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
}
|
|
133
|
+
const pending = this.takeRegistration();
|
|
134
|
+
pending?.reject(new Error("Client disconnected before registration"));
|
|
122
135
|
this.failure = null;
|
|
123
|
-
|
|
124
|
-
let settled = false;
|
|
136
|
+
const task = new Promise((resolve) => {
|
|
125
137
|
const finish = () => {
|
|
126
|
-
if (settled) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
settled = true;
|
|
130
138
|
clearTimeout(timeout);
|
|
131
139
|
this.socket.off("close", finish);
|
|
132
140
|
this.socket.off("error", onError);
|
|
@@ -140,13 +148,20 @@ export class BrokerConnection {
|
|
|
140
148
|
}, 2000);
|
|
141
149
|
this.socket.once("close", finish);
|
|
142
150
|
this.socket.once("error", onError);
|
|
143
|
-
|
|
151
|
+
});
|
|
152
|
+
this.phase = { kind: "closing", task, registered: phase.kind === "active" };
|
|
153
|
+
try {
|
|
154
|
+
if (phase.kind === "active" || phase.kind === "registering") {
|
|
144
155
|
writeMessage(this.socket, { type: "unregister" });
|
|
145
156
|
this.socket.end();
|
|
146
157
|
}
|
|
147
|
-
|
|
158
|
+
else {
|
|
148
159
|
this.socket.destroy();
|
|
149
160
|
}
|
|
150
|
-
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
this.socket.destroy();
|
|
164
|
+
}
|
|
165
|
+
return task;
|
|
151
166
|
}
|
|
152
167
|
}
|
|
@@ -12,6 +12,6 @@ export declare function writeFrame(socket: Socket, json: string, limit?: number)
|
|
|
12
12
|
/**
|
|
13
13
|
* Create a message reader that handles partial reads.
|
|
14
14
|
* Calls onMessage for each complete message received.
|
|
15
|
-
* Protocol or handler errors are reported
|
|
15
|
+
* Protocol or handler errors end reading and are reported so the caller can close the socket.
|
|
16
16
|
*/
|
|
17
17
|
export declare function createMessageReader(onMessage: (msg: unknown) => void, onError: (error: Readonly<Error>) => void, limit?: number): (data: Buffer) => void;
|