@workerdeck/relay-client 3.3.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 +40 -0
- package/build/index.d.mts +213 -0
- package/build/index.mjs +381 -0
- package/build/index.mjs.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tobias Strebitzer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @workerdeck/relay-client
|
|
2
|
+
|
|
3
|
+
The gateway side of the WorkerDeck relay: the relay wire frames, `connectRelay` (dial out, auth,
|
|
4
|
+
heartbeat, reconnect with backoff), the diffing session-registry publisher, and the request
|
|
5
|
+
routing that lets agents on separate gateways list, peek at and message each other.
|
|
6
|
+
|
|
7
|
+
Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). You rarely use it directly: set the
|
|
8
|
+
`relay` option on [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) (or in
|
|
9
|
+
`workerdeck.config.mjs`) and the gateway connects on its own. The relay itself is
|
|
10
|
+
[`@workerdeck/relay`](https://www.npmjs.com/package/@workerdeck/relay).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @workerdeck/relay-client
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { connectRelay } from '@workerdeck/relay-client'
|
|
22
|
+
|
|
23
|
+
const relay = connectRelay(
|
|
24
|
+
{ url: 'ws://relay.local:7777', gateway: 'mac-mini', key: process.env.RELAY_KEY!, allow: ['send', 'peek'] },
|
|
25
|
+
{
|
|
26
|
+
snapshot: async () => sessions(), // every session this gateway publishes
|
|
27
|
+
peek: async (origin, sessionId, recent) => peekLocal(sessionId, recent),
|
|
28
|
+
send: async (origin, sessionId, text) => deliverLocal(origin, sessionId, text),
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const rows = await relay.list('my-session-id')
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See the [cross-gateway peers guide](https://workerdeck.github.io/workerdeck/guides/cross-gateway-peers/).
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT © Tobias Strebitzer - see
|
|
40
|
+
[LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ContextReading, ProfileEngine, SessionStatus } from "@workerdeck/protocol";
|
|
2
|
+
//#region src/frames.d.ts
|
|
3
|
+
declare const RELAY_WIRE_VERSION = 1;
|
|
4
|
+
declare const RELAY_OPS: readonly ["send", "peek"];
|
|
5
|
+
type RelayOp = (typeof RELAY_OPS)[number];
|
|
6
|
+
declare const RELAY_CLOSE: {
|
|
7
|
+
readonly badHello: 4400;
|
|
8
|
+
readonly unauthorized: 4401;
|
|
9
|
+
readonly revoked: 4403;
|
|
10
|
+
readonly versionMismatch: 4426;
|
|
11
|
+
readonly replaced: 4409;
|
|
12
|
+
readonly timeout: 4408;
|
|
13
|
+
};
|
|
14
|
+
declare const RELAY_FRAME_MAX_BYTES: number;
|
|
15
|
+
declare const RELAY_MESSAGE_MAX_CHARS = 16000;
|
|
16
|
+
declare const RELAY_MAX_HOPS = 12;
|
|
17
|
+
declare const RELAY_GATEWAY_NAME: RegExp;
|
|
18
|
+
type RelaySessionEntry = {
|
|
19
|
+
id: string;
|
|
20
|
+
engine?: ProfileEngine;
|
|
21
|
+
status: SessionStatus;
|
|
22
|
+
title?: string;
|
|
23
|
+
project?: {
|
|
24
|
+
name: string;
|
|
25
|
+
root: string;
|
|
26
|
+
};
|
|
27
|
+
cwd: string;
|
|
28
|
+
profile?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
permissionMode?: string;
|
|
31
|
+
contextUsage?: ContextReading;
|
|
32
|
+
checklist?: {
|
|
33
|
+
done: number;
|
|
34
|
+
total: number;
|
|
35
|
+
};
|
|
36
|
+
numTurns?: number;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
lastActivityAt?: number;
|
|
39
|
+
pendingPermissionCount: number;
|
|
40
|
+
live: boolean;
|
|
41
|
+
};
|
|
42
|
+
type RelayPeerRow = RelaySessionEntry & {
|
|
43
|
+
gateway: string;
|
|
44
|
+
allow: RelayOp[];
|
|
45
|
+
};
|
|
46
|
+
type RelayOrigin = {
|
|
47
|
+
gateway: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
engine?: ProfileEngine;
|
|
51
|
+
hops: string[];
|
|
52
|
+
};
|
|
53
|
+
type RelayPeek = RelaySessionEntry & {
|
|
54
|
+
checklistItems?: Array<{
|
|
55
|
+
text: string;
|
|
56
|
+
status: string;
|
|
57
|
+
}>;
|
|
58
|
+
pendingApprovals: string[];
|
|
59
|
+
recent: string[];
|
|
60
|
+
};
|
|
61
|
+
type RelaySendResult = {
|
|
62
|
+
delivered: true;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
queued: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
delivered: false;
|
|
68
|
+
reason: string;
|
|
69
|
+
};
|
|
70
|
+
type RelayTarget = {
|
|
71
|
+
gateway: string;
|
|
72
|
+
id: string;
|
|
73
|
+
};
|
|
74
|
+
type HelloFrame = {
|
|
75
|
+
t: 'hello';
|
|
76
|
+
gateway: string;
|
|
77
|
+
key: string;
|
|
78
|
+
version: number;
|
|
79
|
+
ceiling: {
|
|
80
|
+
ops: RelayOp[];
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
type WelcomeFrame = {
|
|
84
|
+
t: 'welcome';
|
|
85
|
+
relayVersion: number;
|
|
86
|
+
};
|
|
87
|
+
type SnapshotFrame = {
|
|
88
|
+
t: 'registry.snapshot';
|
|
89
|
+
seq: number;
|
|
90
|
+
entries: RelaySessionEntry[];
|
|
91
|
+
};
|
|
92
|
+
type DeltaFrame = {
|
|
93
|
+
t: 'registry.delta';
|
|
94
|
+
seq: number;
|
|
95
|
+
upsert: RelaySessionEntry[];
|
|
96
|
+
remove: string[];
|
|
97
|
+
};
|
|
98
|
+
type DigestFrame = {
|
|
99
|
+
t: 'registry.digest';
|
|
100
|
+
seq: number;
|
|
101
|
+
count: number;
|
|
102
|
+
hash: string;
|
|
103
|
+
};
|
|
104
|
+
type ResyncFrame = {
|
|
105
|
+
t: 'registry.resync';
|
|
106
|
+
};
|
|
107
|
+
type ListRequest = {
|
|
108
|
+
t: 'peer.list';
|
|
109
|
+
id: string;
|
|
110
|
+
from: string;
|
|
111
|
+
};
|
|
112
|
+
type PeekRequest = {
|
|
113
|
+
t: 'peer.peek';
|
|
114
|
+
id: string;
|
|
115
|
+
from: string;
|
|
116
|
+
to: RelayTarget;
|
|
117
|
+
recent?: number;
|
|
118
|
+
};
|
|
119
|
+
type SendRequest = {
|
|
120
|
+
t: 'peer.send';
|
|
121
|
+
id: string;
|
|
122
|
+
from: string;
|
|
123
|
+
to: RelayTarget;
|
|
124
|
+
text: string;
|
|
125
|
+
hops: string[];
|
|
126
|
+
};
|
|
127
|
+
type InboundPeek = {
|
|
128
|
+
t: 'peer.peek';
|
|
129
|
+
id: string;
|
|
130
|
+
origin: RelayOrigin;
|
|
131
|
+
to: string;
|
|
132
|
+
recent?: number;
|
|
133
|
+
};
|
|
134
|
+
type InboundSend = {
|
|
135
|
+
t: 'peer.send';
|
|
136
|
+
id: string;
|
|
137
|
+
origin: RelayOrigin;
|
|
138
|
+
to: string;
|
|
139
|
+
text: string;
|
|
140
|
+
};
|
|
141
|
+
type ResponseFrame = {
|
|
142
|
+
t: 'res';
|
|
143
|
+
id: string;
|
|
144
|
+
ok: true;
|
|
145
|
+
result: unknown;
|
|
146
|
+
} | {
|
|
147
|
+
t: 'res';
|
|
148
|
+
id: string;
|
|
149
|
+
ok: false;
|
|
150
|
+
error: string;
|
|
151
|
+
};
|
|
152
|
+
type GatewayFrame = HelloFrame | SnapshotFrame | DeltaFrame | DigestFrame | ListRequest | PeekRequest | SendRequest | ResponseFrame;
|
|
153
|
+
type RelayFrame = WelcomeFrame | ResyncFrame | InboundPeek | InboundSend | ResponseFrame;
|
|
154
|
+
declare function encodeFrame(frame: GatewayFrame | RelayFrame): string;
|
|
155
|
+
declare function decodeFrame(raw: unknown): {
|
|
156
|
+
t: string;
|
|
157
|
+
[key: string]: unknown;
|
|
158
|
+
} | undefined;
|
|
159
|
+
declare function isRelayOp(value: unknown): value is RelayOp;
|
|
160
|
+
declare function relayPeerId(gateway: string, sessionId: string): string;
|
|
161
|
+
declare function parseRelayPeerId(id: string): RelayTarget | undefined;
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/registry.d.ts
|
|
164
|
+
declare function canonicalJson(value: unknown): string;
|
|
165
|
+
declare function registryHash(entries: Iterable<[string, string]>): string;
|
|
166
|
+
declare function digestOf(entries: readonly RelaySessionEntry[]): {
|
|
167
|
+
count: number;
|
|
168
|
+
hash: string;
|
|
169
|
+
};
|
|
170
|
+
type RegistryPublisher = {
|
|
171
|
+
snapshot(entries: readonly RelaySessionEntry[]): SnapshotFrame;
|
|
172
|
+
delta(entries: readonly RelaySessionEntry[]): DeltaFrame | undefined;
|
|
173
|
+
digest(): DigestFrame;
|
|
174
|
+
};
|
|
175
|
+
declare function createRegistryPublisher(): RegistryPublisher;
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/connection.d.ts
|
|
178
|
+
type RelayHost = {
|
|
179
|
+
snapshot(): Promise<RelaySessionEntry[]>;
|
|
180
|
+
peek(origin: RelayOrigin, sessionId: string, recent?: number): Promise<RelayPeek | undefined>;
|
|
181
|
+
send(origin: RelayOrigin, sessionId: string, text: string): Promise<RelaySendResult>;
|
|
182
|
+
};
|
|
183
|
+
type RelayConnectOptions = {
|
|
184
|
+
url: string;
|
|
185
|
+
gateway: string;
|
|
186
|
+
key: string;
|
|
187
|
+
allow?: readonly RelayOp[];
|
|
188
|
+
ca?: string | Buffer;
|
|
189
|
+
tickMs?: number;
|
|
190
|
+
digestMs?: number;
|
|
191
|
+
heartbeatMs?: number;
|
|
192
|
+
requestTimeoutMs?: number;
|
|
193
|
+
backoffMinMs?: number;
|
|
194
|
+
backoffMaxMs?: number;
|
|
195
|
+
log?: (message: string) => void;
|
|
196
|
+
};
|
|
197
|
+
type RelayState = 'connecting' | 'online' | 'offline' | 'stopped';
|
|
198
|
+
type RelayConnection = {
|
|
199
|
+
state(): RelayState;
|
|
200
|
+
list(from: string): Promise<RelayPeerRow[]>;
|
|
201
|
+
peek(from: string, to: RelayTarget, recent?: number): Promise<RelayPeek | undefined>;
|
|
202
|
+
send(from: string, to: RelayTarget, text: string, hops: string[]): Promise<RelaySendResult>;
|
|
203
|
+
nudge(): void;
|
|
204
|
+
setHost(host: RelayHost): void;
|
|
205
|
+
close(): void;
|
|
206
|
+
};
|
|
207
|
+
declare class RelayUnavailableError extends Error {
|
|
208
|
+
constructor(message?: string);
|
|
209
|
+
}
|
|
210
|
+
declare function connectRelay(options: RelayConnectOptions, initialHost: RelayHost): RelayConnection;
|
|
211
|
+
//#endregion
|
|
212
|
+
export { DeltaFrame, DigestFrame, GatewayFrame, HelloFrame, InboundPeek, InboundSend, ListRequest, PeekRequest, RELAY_CLOSE, RELAY_FRAME_MAX_BYTES, RELAY_GATEWAY_NAME, RELAY_MAX_HOPS, RELAY_MESSAGE_MAX_CHARS, RELAY_OPS, RELAY_WIRE_VERSION, RegistryPublisher, RelayConnectOptions, RelayConnection, RelayFrame, RelayHost, RelayOp, RelayOrigin, RelayPeek, RelayPeerRow, RelaySendResult, RelaySessionEntry, RelayState, RelayTarget, RelayUnavailableError, ResponseFrame, ResyncFrame, SendRequest, SnapshotFrame, WelcomeFrame, canonicalJson, connectRelay, createRegistryPublisher, decodeFrame, digestOf, encodeFrame, isRelayOp, parseRelayPeerId, registryHash, relayPeerId };
|
|
213
|
+
//# sourceMappingURL=index.d.mts.map
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
//#region src/frames.ts
|
|
4
|
+
const RELAY_WIRE_VERSION = 1;
|
|
5
|
+
const RELAY_OPS = ["send", "peek"];
|
|
6
|
+
const RELAY_CLOSE = {
|
|
7
|
+
badHello: 4400,
|
|
8
|
+
unauthorized: 4401,
|
|
9
|
+
revoked: 4403,
|
|
10
|
+
versionMismatch: 4426,
|
|
11
|
+
replaced: 4409,
|
|
12
|
+
timeout: 4408
|
|
13
|
+
};
|
|
14
|
+
const RELAY_FRAME_MAX_BYTES = 4194304;
|
|
15
|
+
const RELAY_MESSAGE_MAX_CHARS = 16e3;
|
|
16
|
+
const RELAY_MAX_HOPS = 12;
|
|
17
|
+
const RELAY_GATEWAY_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
18
|
+
function encodeFrame(frame) {
|
|
19
|
+
return JSON.stringify(frame);
|
|
20
|
+
}
|
|
21
|
+
function decodeFrame(raw) {
|
|
22
|
+
const text = typeof raw === "string" ? raw : raw instanceof Buffer ? raw.toString("utf8") : void 0;
|
|
23
|
+
if (text === void 0) return;
|
|
24
|
+
try {
|
|
25
|
+
const value = JSON.parse(text);
|
|
26
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || typeof value.t !== "string") return;
|
|
27
|
+
return value;
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isRelayOp(value) {
|
|
33
|
+
return value === "send" || value === "peek";
|
|
34
|
+
}
|
|
35
|
+
function relayPeerId(gateway, sessionId) {
|
|
36
|
+
return `${gateway}:${sessionId}`;
|
|
37
|
+
}
|
|
38
|
+
function parseRelayPeerId(id) {
|
|
39
|
+
const at = id.indexOf(":");
|
|
40
|
+
if (at <= 0 || at === id.length - 1) return;
|
|
41
|
+
const gateway = id.slice(0, at);
|
|
42
|
+
return RELAY_GATEWAY_NAME.test(gateway) ? {
|
|
43
|
+
gateway,
|
|
44
|
+
id: id.slice(at + 1)
|
|
45
|
+
} : void 0;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/registry.ts
|
|
49
|
+
function canonicalJson(value) {
|
|
50
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
51
|
+
if (value !== null && typeof value === "object") return `{${Object.keys(value).filter((key) => value[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
function registryHash(entries) {
|
|
55
|
+
const hash = createHash("sha256");
|
|
56
|
+
for (const [id, json] of [...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) hash.update(id).update("\n").update(json).update("\n");
|
|
57
|
+
return hash.digest("base64url");
|
|
58
|
+
}
|
|
59
|
+
function digestOf(entries) {
|
|
60
|
+
return {
|
|
61
|
+
count: entries.length,
|
|
62
|
+
hash: registryHash(entries.map((entry) => [entry.id, canonicalJson(entry)]))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createRegistryPublisher() {
|
|
66
|
+
let sent = /* @__PURE__ */ new Map();
|
|
67
|
+
let seq = 0;
|
|
68
|
+
const index = (entries) => new Map(entries.map((entry) => [entry.id, canonicalJson(entry)]));
|
|
69
|
+
return {
|
|
70
|
+
snapshot(entries) {
|
|
71
|
+
sent = index(entries);
|
|
72
|
+
seq += 1;
|
|
73
|
+
return {
|
|
74
|
+
t: "registry.snapshot",
|
|
75
|
+
seq,
|
|
76
|
+
entries: [...entries]
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
delta(entries) {
|
|
80
|
+
const next = index(entries);
|
|
81
|
+
const upsert = entries.filter((entry) => sent.get(entry.id) !== next.get(entry.id));
|
|
82
|
+
const remove = [...sent.keys()].filter((id) => !next.has(id));
|
|
83
|
+
if (upsert.length === 0 && remove.length === 0) return;
|
|
84
|
+
sent = next;
|
|
85
|
+
seq += 1;
|
|
86
|
+
return {
|
|
87
|
+
t: "registry.delta",
|
|
88
|
+
seq,
|
|
89
|
+
upsert,
|
|
90
|
+
remove
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
digest() {
|
|
94
|
+
return {
|
|
95
|
+
t: "registry.digest",
|
|
96
|
+
seq,
|
|
97
|
+
count: sent.size,
|
|
98
|
+
hash: registryHash(sent)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/connection.ts
|
|
105
|
+
var RelayUnavailableError = class extends Error {
|
|
106
|
+
constructor(message = "remote gateways unavailable") {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = "RelayUnavailableError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const TERMINAL_CLOSES = /* @__PURE__ */ new Map([
|
|
112
|
+
[RELAY_CLOSE.unauthorized, "the relay refused this gateway key"],
|
|
113
|
+
[RELAY_CLOSE.revoked, "this gateway is not enrolled at the relay"],
|
|
114
|
+
[RELAY_CLOSE.versionMismatch, "the relay speaks a different wire version"],
|
|
115
|
+
[RELAY_CLOSE.badHello, "the relay refused the hello frame"]
|
|
116
|
+
]);
|
|
117
|
+
function connectRelay(options, initialHost) {
|
|
118
|
+
const tickMs = options.tickMs ?? 2e3;
|
|
119
|
+
const digestMs = options.digestMs ?? 3e4;
|
|
120
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
121
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
122
|
+
const backoffMin = options.backoffMinMs ?? 1e3;
|
|
123
|
+
const backoffMax = options.backoffMaxMs ?? 6e4;
|
|
124
|
+
const allow = new Set((options.allow ?? RELAY_OPS).filter(isRelayOp));
|
|
125
|
+
const log = options.log ?? (() => {});
|
|
126
|
+
let host = initialHost;
|
|
127
|
+
let state = "connecting";
|
|
128
|
+
let socket;
|
|
129
|
+
let attempt = 0;
|
|
130
|
+
let reconnectTimer;
|
|
131
|
+
let tickTimer;
|
|
132
|
+
let digestTimer;
|
|
133
|
+
let heartbeatTimer;
|
|
134
|
+
let lastHeard = 0;
|
|
135
|
+
let ticking = false;
|
|
136
|
+
let tickAgain = false;
|
|
137
|
+
let nextId = 0;
|
|
138
|
+
let lastTerminal;
|
|
139
|
+
let publisher = createRegistryPublisher();
|
|
140
|
+
const pending = /* @__PURE__ */ new Map();
|
|
141
|
+
const sendFrame = (frame) => {
|
|
142
|
+
if (socket?.readyState === WebSocket.OPEN) socket.send(encodeFrame(frame));
|
|
143
|
+
};
|
|
144
|
+
const failPending = (reason) => {
|
|
145
|
+
for (const [id, entry] of pending) {
|
|
146
|
+
clearTimeout(entry.timer);
|
|
147
|
+
entry.reject(new RelayUnavailableError(reason));
|
|
148
|
+
pending.delete(id);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const stopTimers = () => {
|
|
152
|
+
clearInterval(tickTimer);
|
|
153
|
+
clearInterval(digestTimer);
|
|
154
|
+
clearInterval(heartbeatTimer);
|
|
155
|
+
tickTimer = digestTimer = heartbeatTimer = void 0;
|
|
156
|
+
};
|
|
157
|
+
const publishSnapshot = async () => {
|
|
158
|
+
const entries = await host.snapshot();
|
|
159
|
+
if (state === "online") sendFrame(publisher.snapshot(entries));
|
|
160
|
+
};
|
|
161
|
+
const tick = async () => {
|
|
162
|
+
if (state !== "online") return;
|
|
163
|
+
if (ticking) {
|
|
164
|
+
tickAgain = true;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
ticking = true;
|
|
168
|
+
try {
|
|
169
|
+
const entries = await host.snapshot();
|
|
170
|
+
const delta = state === "online" ? publisher.delta(entries) : void 0;
|
|
171
|
+
if (delta) sendFrame(delta);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
log(`relay: registry tick failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
} finally {
|
|
175
|
+
ticking = false;
|
|
176
|
+
if (tickAgain) {
|
|
177
|
+
tickAgain = false;
|
|
178
|
+
tick();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const answer = async (id, work) => {
|
|
183
|
+
try {
|
|
184
|
+
sendFrame({
|
|
185
|
+
t: "res",
|
|
186
|
+
id,
|
|
187
|
+
ok: true,
|
|
188
|
+
result: await work() ?? null
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
sendFrame({
|
|
192
|
+
t: "res",
|
|
193
|
+
id,
|
|
194
|
+
ok: false,
|
|
195
|
+
error: error instanceof Error ? error.message : String(error)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const onInbound = (frame) => {
|
|
200
|
+
const id = typeof frame.id === "string" ? frame.id : void 0;
|
|
201
|
+
const origin = frame.origin;
|
|
202
|
+
const to = typeof frame.to === "string" ? frame.to : void 0;
|
|
203
|
+
if (!id || !origin || typeof origin.gateway !== "string" || typeof origin.sessionId !== "string" || !to) return;
|
|
204
|
+
const hops = Array.isArray(origin.hops) ? origin.hops.filter((hop) => typeof hop === "string") : [];
|
|
205
|
+
const stamped = {
|
|
206
|
+
...origin,
|
|
207
|
+
hops
|
|
208
|
+
};
|
|
209
|
+
if (frame.t === "peer.peek") {
|
|
210
|
+
if (!allow.has("peek")) {
|
|
211
|
+
answer(id, async () => null);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const recent = typeof frame.recent === "number" ? frame.recent : void 0;
|
|
215
|
+
answer(id, () => host.peek(stamped, to, recent));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (frame.t === "peer.send") {
|
|
219
|
+
if (!allow.has("send")) {
|
|
220
|
+
answer(id, async () => ({
|
|
221
|
+
delivered: false,
|
|
222
|
+
reason: `no such session: ${to}`
|
|
223
|
+
}));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const text = typeof frame.text === "string" ? frame.text : "";
|
|
227
|
+
answer(id, () => host.send(stamped, to, text));
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const onFrame = (raw) => {
|
|
231
|
+
lastHeard = Date.now();
|
|
232
|
+
const frame = decodeFrame(Array.isArray(raw) ? Buffer.concat(raw) : raw instanceof ArrayBuffer ? Buffer.from(raw) : raw);
|
|
233
|
+
if (!frame) return;
|
|
234
|
+
switch (frame.t) {
|
|
235
|
+
case "welcome":
|
|
236
|
+
if (state !== "connecting") return;
|
|
237
|
+
state = "online";
|
|
238
|
+
attempt = 0;
|
|
239
|
+
lastTerminal = void 0;
|
|
240
|
+
log(`relay: connected to ${options.url} as ${options.gateway}`);
|
|
241
|
+
publisher = createRegistryPublisher();
|
|
242
|
+
publishSnapshot().catch((error) => log(`relay: snapshot failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
243
|
+
tickTimer = setInterval(() => void tick(), tickMs);
|
|
244
|
+
digestTimer = setInterval(() => sendFrame(publisher.digest()), digestMs);
|
|
245
|
+
return;
|
|
246
|
+
case "registry.resync":
|
|
247
|
+
publishSnapshot().catch(() => {});
|
|
248
|
+
return;
|
|
249
|
+
case "res": {
|
|
250
|
+
const id = typeof frame.id === "string" ? frame.id : "";
|
|
251
|
+
const entry = pending.get(id);
|
|
252
|
+
if (!entry) return;
|
|
253
|
+
pending.delete(id);
|
|
254
|
+
clearTimeout(entry.timer);
|
|
255
|
+
if (frame.ok === true) entry.resolve(frame.result);
|
|
256
|
+
else entry.reject(new Error(typeof frame.error === "string" ? frame.error : "relay request failed"));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
case "peer.peek":
|
|
260
|
+
case "peer.send":
|
|
261
|
+
if (state === "online") onInbound(frame);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const scheduleReconnect = () => {
|
|
266
|
+
if (state === "stopped") return;
|
|
267
|
+
attempt += 1;
|
|
268
|
+
const ceiling = lastTerminal !== void 0 ? backoffMax : Math.min(backoffMax, backoffMin * 2 ** (attempt - 1));
|
|
269
|
+
const delay = Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
|
|
270
|
+
reconnectTimer = setTimeout(dial, delay);
|
|
271
|
+
};
|
|
272
|
+
const onClose = (code, reason) => {
|
|
273
|
+
stopTimers();
|
|
274
|
+
socket = void 0;
|
|
275
|
+
failPending("remote gateways unavailable");
|
|
276
|
+
if (state === "stopped") return;
|
|
277
|
+
const wasOnline = state === "online";
|
|
278
|
+
state = "offline";
|
|
279
|
+
const terminal = TERMINAL_CLOSES.get(code);
|
|
280
|
+
if (terminal) {
|
|
281
|
+
if (lastTerminal !== code) log(`relay: ${terminal}${reason ? ` (${reason})` : ""}; retrying every ${Math.round(backoffMax / 1e3)}s`);
|
|
282
|
+
lastTerminal = code;
|
|
283
|
+
} else if (wasOnline) log(`relay: connection lost (${code}${reason ? ` ${reason}` : ""}); reconnecting`);
|
|
284
|
+
scheduleReconnect();
|
|
285
|
+
};
|
|
286
|
+
function dial() {
|
|
287
|
+
reconnectTimer = void 0;
|
|
288
|
+
if (state === "stopped") return;
|
|
289
|
+
state = "connecting";
|
|
290
|
+
const ws = new WebSocket(options.url, {
|
|
291
|
+
maxPayload: RELAY_FRAME_MAX_BYTES,
|
|
292
|
+
handshakeTimeout: requestTimeoutMs,
|
|
293
|
+
ca: options.ca
|
|
294
|
+
});
|
|
295
|
+
socket = ws;
|
|
296
|
+
lastHeard = Date.now();
|
|
297
|
+
ws.on("open", () => {
|
|
298
|
+
sendFrame({
|
|
299
|
+
t: "hello",
|
|
300
|
+
gateway: options.gateway,
|
|
301
|
+
key: options.key,
|
|
302
|
+
version: 1,
|
|
303
|
+
ceiling: { ops: [...allow] }
|
|
304
|
+
});
|
|
305
|
+
heartbeatTimer = setInterval(() => {
|
|
306
|
+
if (Date.now() - lastHeard > heartbeatMs * 2 + 1e3) {
|
|
307
|
+
ws.terminate();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
ws.ping();
|
|
311
|
+
}, heartbeatMs);
|
|
312
|
+
});
|
|
313
|
+
ws.on("message", onFrame);
|
|
314
|
+
ws.on("pong", () => {
|
|
315
|
+
lastHeard = Date.now();
|
|
316
|
+
});
|
|
317
|
+
ws.on("ping", () => {
|
|
318
|
+
lastHeard = Date.now();
|
|
319
|
+
});
|
|
320
|
+
ws.on("error", () => {});
|
|
321
|
+
ws.on("close", (code, reason) => {
|
|
322
|
+
if (socket === ws) onClose(code, reason.toString("utf8"));
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const request = (frame) => {
|
|
326
|
+
if (state !== "online") return Promise.reject(new RelayUnavailableError());
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const timer = setTimeout(() => {
|
|
329
|
+
pending.delete(frame.id);
|
|
330
|
+
reject(new RelayUnavailableError("the relay did not answer in time"));
|
|
331
|
+
}, requestTimeoutMs);
|
|
332
|
+
pending.set(frame.id, {
|
|
333
|
+
resolve,
|
|
334
|
+
reject,
|
|
335
|
+
timer
|
|
336
|
+
});
|
|
337
|
+
sendFrame(frame);
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
const newId = () => `g${++nextId}`;
|
|
341
|
+
dial();
|
|
342
|
+
return {
|
|
343
|
+
state: () => state,
|
|
344
|
+
list: (from) => request({
|
|
345
|
+
t: "peer.list",
|
|
346
|
+
id: newId(),
|
|
347
|
+
from
|
|
348
|
+
}),
|
|
349
|
+
peek: async (from, to, recent) => await request({
|
|
350
|
+
t: "peer.peek",
|
|
351
|
+
id: newId(),
|
|
352
|
+
from,
|
|
353
|
+
to,
|
|
354
|
+
recent
|
|
355
|
+
}) ?? void 0,
|
|
356
|
+
send: (from, to, text, hops) => request({
|
|
357
|
+
t: "peer.send",
|
|
358
|
+
id: newId(),
|
|
359
|
+
from,
|
|
360
|
+
to,
|
|
361
|
+
text,
|
|
362
|
+
hops
|
|
363
|
+
}),
|
|
364
|
+
nudge: () => void tick(),
|
|
365
|
+
setHost: (next) => {
|
|
366
|
+
host = next;
|
|
367
|
+
},
|
|
368
|
+
close: () => {
|
|
369
|
+
state = "stopped";
|
|
370
|
+
clearTimeout(reconnectTimer);
|
|
371
|
+
stopTimers();
|
|
372
|
+
failPending("relay connection closed");
|
|
373
|
+
socket?.close(1e3, "gateway closing");
|
|
374
|
+
socket = void 0;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
export { RELAY_CLOSE, RELAY_FRAME_MAX_BYTES, RELAY_GATEWAY_NAME, RELAY_MAX_HOPS, RELAY_MESSAGE_MAX_CHARS, RELAY_OPS, RELAY_WIRE_VERSION, RelayUnavailableError, canonicalJson, connectRelay, createRegistryPublisher, decodeFrame, digestOf, encodeFrame, isRelayOp, parseRelayPeerId, registryHash, relayPeerId };
|
|
380
|
+
|
|
381
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/frames.ts","../src/registry.ts","../src/connection.ts"],"sourcesContent":["import type { ContextReading, ProfileEngine, SessionStatus } from '@workerdeck/protocol'\n\nexport const RELAY_WIRE_VERSION = 1\n\nexport const RELAY_OPS = ['send', 'peek'] as const\nexport type RelayOp = (typeof RELAY_OPS)[number]\n\nexport const RELAY_CLOSE = {\n badHello: 4400,\n unauthorized: 4401,\n revoked: 4403,\n versionMismatch: 4426,\n replaced: 4409,\n timeout: 4408,\n} as const\n\nexport const RELAY_FRAME_MAX_BYTES = 4 * 1024 * 1024\nexport const RELAY_MESSAGE_MAX_CHARS = 16_000\nexport const RELAY_MAX_HOPS = 12\nexport const RELAY_GATEWAY_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/\n\nexport type RelaySessionEntry = {\n id: string\n engine?: ProfileEngine\n status: SessionStatus\n title?: string\n project?: { name: string; root: string }\n cwd: string\n profile?: string\n model?: string\n permissionMode?: string\n contextUsage?: ContextReading\n checklist?: { done: number; total: number }\n numTurns?: number\n createdAt: number\n lastActivityAt?: number\n pendingPermissionCount: number\n live: boolean\n}\n\nexport type RelayPeerRow = RelaySessionEntry & { gateway: string; allow: RelayOp[] }\n\nexport type RelayOrigin = {\n gateway: string\n sessionId: string\n name?: string\n engine?: ProfileEngine\n hops: string[]\n}\n\nexport type RelayPeek = RelaySessionEntry & {\n checklistItems?: Array<{ text: string; status: string }>\n pendingApprovals: string[]\n recent: string[]\n}\n\nexport type RelaySendResult = { delivered: true; sessionId: string; name?: string; queued: boolean } | { delivered: false; reason: string }\n\nexport type RelayTarget = { gateway: string; id: string }\n\nexport type HelloFrame = { t: 'hello'; gateway: string; key: string; version: number; ceiling: { ops: RelayOp[] } }\nexport type WelcomeFrame = { t: 'welcome'; relayVersion: number }\nexport type SnapshotFrame = { t: 'registry.snapshot'; seq: number; entries: RelaySessionEntry[] }\nexport type DeltaFrame = { t: 'registry.delta'; seq: number; upsert: RelaySessionEntry[]; remove: string[] }\nexport type DigestFrame = { t: 'registry.digest'; seq: number; count: number; hash: string }\nexport type ResyncFrame = { t: 'registry.resync' }\n\nexport type ListRequest = { t: 'peer.list'; id: string; from: string }\nexport type PeekRequest = { t: 'peer.peek'; id: string; from: string; to: RelayTarget; recent?: number }\nexport type SendRequest = { t: 'peer.send'; id: string; from: string; to: RelayTarget; text: string; hops: string[] }\n\nexport type InboundPeek = { t: 'peer.peek'; id: string; origin: RelayOrigin; to: string; recent?: number }\nexport type InboundSend = { t: 'peer.send'; id: string; origin: RelayOrigin; to: string; text: string }\n\nexport type ResponseFrame = { t: 'res'; id: string; ok: true; result: unknown } | { t: 'res'; id: string; ok: false; error: string }\n\nexport type GatewayFrame = HelloFrame | SnapshotFrame | DeltaFrame | DigestFrame | ListRequest | PeekRequest | SendRequest | ResponseFrame\n\nexport type RelayFrame = WelcomeFrame | ResyncFrame | InboundPeek | InboundSend | ResponseFrame\n\nexport function encodeFrame(frame: GatewayFrame | RelayFrame): string {\n return JSON.stringify(frame)\n}\n\n// Only the envelope is checked here; each side narrows the payload of the frames it accepts.\nexport function decodeFrame(raw: unknown): { t: string; [key: string]: unknown } | undefined {\n const text = typeof raw === 'string' ? raw : raw instanceof Buffer ? raw.toString('utf8') : undefined\n if (text === undefined) {\n return undefined\n }\n try {\n const value: unknown = JSON.parse(text)\n if (typeof value !== 'object' || value === null || Array.isArray(value) || typeof (value as { t?: unknown }).t !== 'string') {\n return undefined\n }\n return value as { t: string; [key: string]: unknown }\n } catch {\n return undefined\n }\n}\n\nexport function isRelayOp(value: unknown): value is RelayOp {\n return value === 'send' || value === 'peek'\n}\n\nexport function relayPeerId(gateway: string, sessionId: string): string {\n return `${gateway}:${sessionId}`\n}\n\nexport function parseRelayPeerId(id: string): RelayTarget | undefined {\n const at = id.indexOf(':')\n if (at <= 0 || at === id.length - 1) {\n return undefined\n }\n const gateway = id.slice(0, at)\n return RELAY_GATEWAY_NAME.test(gateway) ? { gateway, id: id.slice(at + 1) } : undefined\n}\n","import { createHash } from 'node:crypto'\nimport type { DeltaFrame, DigestFrame, RelaySessionEntry, SnapshotFrame } from './frames.ts'\n\nexport function canonicalJson(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(canonicalJson).join(',')}]`\n }\n if (value !== null && typeof value === 'object') {\n const keys = Object.keys(value)\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .sort()\n return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(',')}}`\n }\n return JSON.stringify(value)\n}\n\nexport function registryHash(entries: Iterable<[string, string]>): string {\n const hash = createHash('sha256')\n for (const [id, json] of [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {\n hash.update(id).update('\\n').update(json).update('\\n')\n }\n return hash.digest('base64url')\n}\n\nexport function digestOf(entries: readonly RelaySessionEntry[]): { count: number; hash: string } {\n return { count: entries.length, hash: registryHash(entries.map((entry) => [entry.id, canonicalJson(entry)])) }\n}\n\nexport type RegistryPublisher = {\n snapshot(entries: readonly RelaySessionEntry[]): SnapshotFrame\n delta(entries: readonly RelaySessionEntry[]): DeltaFrame | undefined\n digest(): DigestFrame\n}\n\n// Diffs what the host reports against what was last sent, so a session that vanished by any path is\n// removed on the next tick. Nothing listens for session events; the snapshot is the only input.\nexport function createRegistryPublisher(): RegistryPublisher {\n let sent = new Map<string, string>()\n let seq = 0\n\n const index = (entries: readonly RelaySessionEntry[]): Map<string, string> =>\n new Map(entries.map((entry) => [entry.id, canonicalJson(entry)]))\n\n return {\n snapshot(entries) {\n sent = index(entries)\n seq += 1\n return { t: 'registry.snapshot', seq, entries: [...entries] }\n },\n delta(entries) {\n const next = index(entries)\n const upsert = entries.filter((entry) => sent.get(entry.id) !== next.get(entry.id))\n const remove = [...sent.keys()].filter((id) => !next.has(id))\n if (upsert.length === 0 && remove.length === 0) {\n return undefined\n }\n sent = next\n seq += 1\n return { t: 'registry.delta', seq, upsert, remove }\n },\n digest() {\n return { t: 'registry.digest', seq, count: sent.size, hash: registryHash(sent) }\n },\n }\n}\n","import { WebSocket, type RawData } from 'ws'\nimport {\n RELAY_CLOSE,\n RELAY_FRAME_MAX_BYTES,\n RELAY_OPS,\n RELAY_WIRE_VERSION,\n decodeFrame,\n encodeFrame,\n isRelayOp,\n type GatewayFrame,\n type RelayOp,\n type RelayOrigin,\n type RelayPeek,\n type RelayPeerRow,\n type RelaySendResult,\n type RelaySessionEntry,\n type RelayTarget,\n} from './frames.ts'\nimport { createRegistryPublisher } from './registry.ts'\n\nexport type RelayHost = {\n snapshot(): Promise<RelaySessionEntry[]>\n peek(origin: RelayOrigin, sessionId: string, recent?: number): Promise<RelayPeek | undefined>\n send(origin: RelayOrigin, sessionId: string, text: string): Promise<RelaySendResult>\n}\n\nexport type RelayConnectOptions = {\n url: string\n gateway: string\n key: string\n allow?: readonly RelayOp[]\n ca?: string | Buffer\n tickMs?: number\n digestMs?: number\n heartbeatMs?: number\n requestTimeoutMs?: number\n backoffMinMs?: number\n backoffMaxMs?: number\n log?: (message: string) => void\n}\n\nexport type RelayState = 'connecting' | 'online' | 'offline' | 'stopped'\n\nexport type RelayConnection = {\n state(): RelayState\n list(from: string): Promise<RelayPeerRow[]>\n peek(from: string, to: RelayTarget, recent?: number): Promise<RelayPeek | undefined>\n send(from: string, to: RelayTarget, text: string, hops: string[]): Promise<RelaySendResult>\n nudge(): void\n setHost(host: RelayHost): void\n close(): void\n}\n\nexport class RelayUnavailableError extends Error {\n constructor(message = 'remote gateways unavailable') {\n super(message)\n this.name = 'RelayUnavailableError'\n }\n}\n\ntype Pending = { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }\n\nconst TERMINAL_CLOSES: ReadonlyMap<number, string> = new Map([\n [RELAY_CLOSE.unauthorized, 'the relay refused this gateway key'],\n [RELAY_CLOSE.revoked, 'this gateway is not enrolled at the relay'],\n [RELAY_CLOSE.versionMismatch, 'the relay speaks a different wire version'],\n [RELAY_CLOSE.badHello, 'the relay refused the hello frame'],\n])\n\nexport function connectRelay(options: RelayConnectOptions, initialHost: RelayHost): RelayConnection {\n const tickMs = options.tickMs ?? 2_000\n const digestMs = options.digestMs ?? 30_000\n const heartbeatMs = options.heartbeatMs ?? 15_000\n const requestTimeoutMs = options.requestTimeoutMs ?? 10_000\n const backoffMin = options.backoffMinMs ?? 1_000\n const backoffMax = options.backoffMaxMs ?? 60_000\n const allow = new Set<RelayOp>((options.allow ?? RELAY_OPS).filter(isRelayOp))\n const log = options.log ?? (() => {})\n\n let host = initialHost\n let state: RelayState = 'connecting'\n let socket: WebSocket | undefined\n let attempt = 0\n let reconnectTimer: NodeJS.Timeout | undefined\n let tickTimer: NodeJS.Timeout | undefined\n let digestTimer: NodeJS.Timeout | undefined\n let heartbeatTimer: NodeJS.Timeout | undefined\n let lastHeard = 0\n let ticking = false\n let tickAgain = false\n let nextId = 0\n let lastTerminal: number | undefined\n let publisher = createRegistryPublisher()\n const pending = new Map<string, Pending>()\n\n const sendFrame = (frame: GatewayFrame): void => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(encodeFrame(frame))\n }\n }\n\n const failPending = (reason: string): void => {\n for (const [id, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new RelayUnavailableError(reason))\n pending.delete(id)\n }\n }\n\n const stopTimers = (): void => {\n clearInterval(tickTimer)\n clearInterval(digestTimer)\n clearInterval(heartbeatTimer)\n tickTimer = digestTimer = heartbeatTimer = undefined\n }\n\n const publishSnapshot = async (): Promise<void> => {\n const entries = await host.snapshot()\n if (state === 'online') {\n sendFrame(publisher.snapshot(entries))\n }\n }\n\n const tick = async (): Promise<void> => {\n if (state !== 'online') {\n return\n }\n if (ticking) {\n tickAgain = true\n return\n }\n ticking = true\n try {\n const entries = await host.snapshot()\n const delta = state === 'online' ? publisher.delta(entries) : undefined\n if (delta) {\n sendFrame(delta)\n }\n } catch (error) {\n log(`relay: registry tick failed: ${error instanceof Error ? error.message : String(error)}`)\n } finally {\n ticking = false\n if (tickAgain) {\n tickAgain = false\n void tick()\n }\n }\n }\n\n const answer = async (id: string, work: () => Promise<unknown>): Promise<void> => {\n try {\n sendFrame({ t: 'res', id, ok: true, result: (await work()) ?? null })\n } catch (error) {\n sendFrame({ t: 'res', id, ok: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const onInbound = (frame: { t: string; [key: string]: unknown }): void => {\n const id = typeof frame.id === 'string' ? frame.id : undefined\n const origin = frame.origin as RelayOrigin | undefined\n const to = typeof frame.to === 'string' ? frame.to : undefined\n if (!id || !origin || typeof origin.gateway !== 'string' || typeof origin.sessionId !== 'string' || !to) {\n return\n }\n const hops = Array.isArray(origin.hops) ? origin.hops.filter((hop): hop is string => typeof hop === 'string') : []\n const stamped: RelayOrigin = { ...origin, hops }\n if (frame.t === 'peer.peek') {\n if (!allow.has('peek')) {\n void answer(id, async () => null)\n return\n }\n const recent = typeof frame.recent === 'number' ? frame.recent : undefined\n void answer(id, () => host.peek(stamped, to, recent))\n return\n }\n if (frame.t === 'peer.send') {\n if (!allow.has('send')) {\n void answer(id, async () => ({ delivered: false, reason: `no such session: ${to}` }))\n return\n }\n const text = typeof frame.text === 'string' ? frame.text : ''\n void answer(id, () => host.send(stamped, to, text))\n }\n }\n\n const onFrame = (raw: RawData): void => {\n lastHeard = Date.now()\n const frame = decodeFrame(Array.isArray(raw) ? Buffer.concat(raw) : raw instanceof ArrayBuffer ? Buffer.from(raw) : raw)\n if (!frame) {\n return\n }\n switch (frame.t) {\n case 'welcome': {\n if (state !== 'connecting') {\n return\n }\n state = 'online'\n attempt = 0\n lastTerminal = undefined\n log(`relay: connected to ${options.url} as ${options.gateway}`)\n publisher = createRegistryPublisher()\n void publishSnapshot().catch((error) => log(`relay: snapshot failed: ${error instanceof Error ? error.message : String(error)}`))\n tickTimer = setInterval(() => void tick(), tickMs)\n digestTimer = setInterval(() => sendFrame(publisher.digest()), digestMs)\n return\n }\n case 'registry.resync': {\n void publishSnapshot().catch(() => {})\n return\n }\n case 'res': {\n const id = typeof frame.id === 'string' ? frame.id : ''\n const entry = pending.get(id)\n if (!entry) {\n return\n }\n pending.delete(id)\n clearTimeout(entry.timer)\n if (frame.ok === true) {\n entry.resolve(frame.result)\n } else {\n entry.reject(new Error(typeof frame.error === 'string' ? frame.error : 'relay request failed'))\n }\n return\n }\n case 'peer.peek':\n case 'peer.send': {\n if (state === 'online') {\n onInbound(frame)\n }\n return\n }\n }\n }\n\n const scheduleReconnect = (): void => {\n if (state === 'stopped') {\n return\n }\n attempt += 1\n const ceiling = lastTerminal !== undefined ? backoffMax : Math.min(backoffMax, backoffMin * 2 ** (attempt - 1))\n const delay = Math.round(ceiling / 2 + Math.random() * (ceiling / 2))\n reconnectTimer = setTimeout(dial, delay)\n }\n\n const onClose = (code: number, reason: string): void => {\n stopTimers()\n socket = undefined\n failPending('remote gateways unavailable')\n if (state === 'stopped') {\n return\n }\n const wasOnline = state === 'online'\n state = 'offline'\n const terminal = TERMINAL_CLOSES.get(code)\n if (terminal) {\n if (lastTerminal !== code) {\n log(`relay: ${terminal}${reason ? ` (${reason})` : ''}; retrying every ${Math.round(backoffMax / 1000)}s`)\n }\n lastTerminal = code\n } else if (wasOnline) {\n log(`relay: connection lost (${code}${reason ? ` ${reason}` : ''}); reconnecting`)\n }\n scheduleReconnect()\n }\n\n function dial(): void {\n reconnectTimer = undefined\n if (state === 'stopped') {\n return\n }\n state = 'connecting'\n const ws = new WebSocket(options.url, { maxPayload: RELAY_FRAME_MAX_BYTES, handshakeTimeout: requestTimeoutMs, ca: options.ca })\n socket = ws\n lastHeard = Date.now()\n ws.on('open', () => {\n sendFrame({ t: 'hello', gateway: options.gateway, key: options.key, version: RELAY_WIRE_VERSION, ceiling: { ops: [...allow] } })\n heartbeatTimer = setInterval(() => {\n if (Date.now() - lastHeard > heartbeatMs * 2 + 1_000) {\n ws.terminate()\n return\n }\n ws.ping()\n }, heartbeatMs)\n })\n ws.on('message', onFrame)\n ws.on('pong', () => {\n lastHeard = Date.now()\n })\n ws.on('ping', () => {\n lastHeard = Date.now()\n })\n ws.on('error', () => {})\n ws.on('close', (code, reason) => {\n if (socket === ws) {\n onClose(code, reason.toString('utf8'))\n }\n })\n }\n\n const request = <T>(frame: GatewayFrame & { id: string }): Promise<T> => {\n if (state !== 'online') {\n return Promise.reject(new RelayUnavailableError())\n }\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(frame.id)\n reject(new RelayUnavailableError('the relay did not answer in time'))\n }, requestTimeoutMs)\n pending.set(frame.id, { resolve: resolve as (value: unknown) => void, reject, timer })\n sendFrame(frame)\n })\n }\n\n const newId = (): string => `g${++nextId}`\n\n dial()\n\n return {\n state: () => state,\n list: (from) => request<RelayPeerRow[]>({ t: 'peer.list', id: newId(), from }),\n peek: async (from, to, recent) => (await request<RelayPeek | null>({ t: 'peer.peek', id: newId(), from, to, recent })) ?? undefined,\n send: (from, to, text, hops) => request<RelaySendResult>({ t: 'peer.send', id: newId(), from, to, text, hops }),\n nudge: () => void tick(),\n setHost: (next) => {\n host = next\n },\n close: () => {\n state = 'stopped'\n clearTimeout(reconnectTimer)\n stopTimers()\n failPending('relay connection closed')\n socket?.close(1000, 'gateway closing')\n socket = undefined\n },\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workerdeck/relay-client",
|
|
3
|
+
"version": "3.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Gateway side of the WorkerDeck relay: the relay wire frames, the dial-out connection with auth, heartbeat and reconnect, the session registry publisher, and the remote peer directory. Lets agents on separate gateways list, peek at and message each other.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./build/index.mjs",
|
|
8
|
+
"types": "./build/index.d.mts",
|
|
9
|
+
"files": [
|
|
10
|
+
"build"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"@workerdeck/source": "./src/index.ts",
|
|
15
|
+
"types": "./build/index.d.mts",
|
|
16
|
+
"default": "./build/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ws": "^8.21.3",
|
|
21
|
+
"@workerdeck/protocol": "3.3.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^26.6.1",
|
|
25
|
+
"rimraf": "^6.1.3",
|
|
26
|
+
"tsdown": "^0.22.14",
|
|
27
|
+
"vitest": "^4.1.11",
|
|
28
|
+
"@types/ws": "^8.18.1"
|
|
29
|
+
},
|
|
30
|
+
"author": "Tobias Strebitzer",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/workerdeck/workerdeck.git",
|
|
34
|
+
"directory": "packages/relay-client"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://workerdeck.github.io/workerdeck/",
|
|
37
|
+
"bugs": "https://github.com/workerdeck/workerdeck/issues",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"workerdeck",
|
|
40
|
+
"relay",
|
|
41
|
+
"agent",
|
|
42
|
+
"peers",
|
|
43
|
+
"gateway"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "rimraf build",
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"typecheck": "tsgo -p tsconfig.json",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|