@deepseek-ai/dsh-experimental-liveshare 0.0.0-stage → 0.2.2-alpha.2
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.i18n.yaml +52 -0
- package/README.md +176 -2
- package/README.zh.md +177 -0
- package/cordis.patch.yml +17 -0
- package/icon.svg +12 -0
- package/lib/client.js +7130 -0
- package/lib/index.js +7775 -0
- package/lib/owner-gateway/cli.js +370 -0
- package/lib/retry-backoff-ccJYtzOj.js +121 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +767 -0
- package/lib/typert.remote-client.d.ts +58 -0
- package/lib/typert.remote-client.js +551 -0
- package/lib/types/client/ManageSharesDialog.d.ts +21 -0
- package/lib/types/client/ShareAction.d.ts +19 -0
- package/lib/types/client/ShareCapabilitiesPicker.d.ts +77 -0
- package/lib/types/client/ShareCreateForm.d.ts +104 -0
- package/lib/types/client/ShareDialog.d.ts +28 -0
- package/lib/types/client/ShareEditPanel.d.ts +98 -0
- package/lib/types/client/SharePermissionFields.d.ts +46 -0
- package/lib/types/client/endpoint-classification.d.ts +201 -0
- package/lib/types/client/index.d.ts +33 -0
- package/lib/types/client/locales.d.ts +282 -0
- package/lib/types/client/owner-display.d.ts +41 -0
- package/lib/types/client/owner-identity-login.d.ts +72 -0
- package/lib/types/client/owner-identity.d.ts +81 -0
- package/lib/types/client/owner-login.d.ts +146 -0
- package/lib/types/client/owner-routes.d.ts +20 -0
- package/lib/types/client/owner-shares.d.ts +83 -0
- package/lib/types/client/share-navigation.d.ts +25 -0
- package/lib/types/client/share-options.d.ts +19 -0
- package/lib/types/client/share-relay-address.d.ts +117 -0
- package/lib/types/client/share-types.d.ts +76 -0
- package/lib/types/client/user-origin.d.ts +46 -0
- package/lib/types/client/viewer-brand.d.ts +48 -0
- package/lib/types/client/viewer-capabilities.d.ts +74 -0
- package/lib/types/client/viewer-context.d.ts +45 -0
- package/lib/types/client/viewer-model.d.ts +77 -0
- package/lib/types/client/viewer-permission.d.ts +44 -0
- package/lib/types/client/viewer-seat.d.ts +55 -0
- package/lib/types/client/viewer-session-plugin.d.ts +47 -0
- package/lib/types/client/viewer-session-row.d.ts +49 -0
- package/lib/types/client/viewer-session.d.ts +20 -0
- package/lib/types/client/viewer-share-path.d.ts +32 -0
- package/lib/types/client/viewer-status-banner.d.ts +33 -0
- package/lib/types/client/viewer-status.d.ts +69 -0
- package/lib/types/endpoint-classification.d.ts +233 -0
- package/lib/types/fork-lineage.d.ts +47 -0
- package/lib/types/host/base64-url.d.ts +17 -0
- package/lib/types/host/fork-ceiling.d.ts +41 -0
- package/lib/types/host/fork-enforcement.d.ts +271 -0
- package/lib/types/host/fork-policy.d.ts +147 -0
- package/lib/types/host/fork-registry.d.ts +231 -0
- package/lib/types/host/fork-tool-guard.d.ts +148 -0
- package/lib/types/host/owner-authorization.d.ts +128 -0
- package/lib/types/host/owner-credential-file.d.ts +64 -0
- package/lib/types/host/owner-http-routes.d.ts +46 -0
- package/lib/types/host/owner-persistence.d.ts +91 -0
- package/lib/types/host/owner-share-operations.d.ts +70 -0
- package/lib/types/host/owner-types.d.ts +64 -0
- package/lib/types/host/relay-authority.d.ts +130 -0
- package/lib/types/host/relay-url.d.ts +16 -0
- package/lib/types/host/share-host.d.ts +61 -0
- package/lib/types/host/share-operations.d.ts +87 -0
- package/lib/types/host/share-remote.d.ts +96 -0
- package/lib/types/host/shared-tools.d.ts +109 -0
- package/lib/types/host/source-access.d.ts +171 -0
- package/lib/types/host/tunnel-supervision.d.ts +218 -0
- package/lib/types/host/tunnel-worker.d.ts +80 -0
- package/lib/types/host/viewer-access-policy.d.ts +100 -0
- package/lib/types/host/viewer-assets.d.ts +23 -0
- package/lib/types/host/viewer-capability-selection.d.ts +59 -0
- package/lib/types/host/viewer-declarations.d.ts +162 -0
- package/lib/types/host/viewer-dispatch.d.ts +88 -0
- package/lib/types/host/viewer-fork.d.ts +84 -0
- package/lib/types/host/viewer-lifecycle.d.ts +226 -0
- package/lib/types/host/viewer-principal.d.ts +59 -0
- package/lib/types/host/viewer-read.d.ts +201 -0
- package/lib/types/host/wire-json.d.ts +24 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/owner-gateway/cli.d.ts +3 -0
- package/lib/types/owner-gateway/pending-rpc.d.ts +47 -0
- package/lib/types/owner-gateway/registration-url.d.ts +20 -0
- package/lib/types/owner-gateway/relay-refusal.d.ts +28 -0
- package/lib/types/owner-gateway/retry-backoff.d.ts +37 -0
- package/lib/types/owner-gateway/worker.d.ts +7 -0
- package/lib/types/owner-identity.d.ts +23 -0
- package/lib/types/read-budgets.d.ts +45 -0
- package/lib/types/share-api-types.d.ts +284 -0
- package/lib/types/viewer-channel.d.ts +107 -0
- package/lib/types/viewer-declarations.d.ts +22 -0
- package/lib/viewer-declarations-krUNq6VU.js +287 -0
- package/lib/viewer-declarations.js +26 -0
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/pt.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +168 -3
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { a as relayRefusal, c as relayEndpoint, i as retryBackoffSettled, r as retryBackoffNext, t as INITIAL_RETRY_BACKOFF } from "../retry-backoff-ccJYtzOj.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { LIVESHARE_CAPABILITIES, LIVESHARE_PROTOCOL_VERSION, parseRelayFrame, parseTunnelWorkerCommand, rawText, sendFrame, unsupportedFrameRefusal, viewerRequestFields } from "@deepseek-ai/dsh-experimental-liveshare-protocol";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
//#region lib/types/owner-gateway/registration-url.js
|
|
6
|
+
/**
|
|
7
|
+
* The registration handshake one share is published with.
|
|
8
|
+
*
|
|
9
|
+
* This URL is the one surface an older owner meets a newer Relay on, so what it states about the
|
|
10
|
+
* owner — the protocol version it reads frames with, and the capabilities it offers beyond them — is
|
|
11
|
+
* the only thing a Relay can compare instead of assuming. Keeping the whole handshake in one function
|
|
12
|
+
* is what lets a check pin it: every parameter here is read by a peer this repository does not build.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Build the WebSocket URL one share registers with.
|
|
16
|
+
*
|
|
17
|
+
* Absent optionals stay absent rather than being sent empty: the Relay reads a missing `sessionMode`
|
|
18
|
+
* as view-only and a missing `allowsFork` as no forking, so an older owner publishes the narrower
|
|
19
|
+
* share instead of a share whose fields a peer must interpret.
|
|
20
|
+
* @param spec - Share the owner host asked this worker to carry.
|
|
21
|
+
* @returns The registration URL, with this build's protocol version and capabilities stated.
|
|
22
|
+
*/
|
|
23
|
+
function registrationUrl(spec) {
|
|
24
|
+
const relay = relayEndpoint(spec.relay);
|
|
25
|
+
const url = new URL(`${relay.basePath}/__tunnel_gateway__`, relay.origin);
|
|
26
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
27
|
+
url.searchParams.set("shareId", spec.shareId);
|
|
28
|
+
url.searchParams.set("sessionId", spec.sessionId);
|
|
29
|
+
url.searchParams.set("sessionTitle", spec.sessionTitle);
|
|
30
|
+
url.searchParams.set("accessMode", spec.accessMode);
|
|
31
|
+
url.searchParams.set("ownerDisplayName", spec.ownerDisplayName);
|
|
32
|
+
if (spec.dshVersion !== void 0 && spec.dshVersion !== "") url.searchParams.set("dshVersion", spec.dshVersion);
|
|
33
|
+
if (spec.ownerInstanceId !== void 0 && spec.ownerInstanceId !== "") url.searchParams.set("ownerInstanceId", spec.ownerInstanceId);
|
|
34
|
+
url.searchParams.set("protocolVersion", String(LIVESHARE_PROTOCOL_VERSION));
|
|
35
|
+
url.searchParams.set("capabilities", LIVESHARE_CAPABILITIES.join(","));
|
|
36
|
+
if (spec.allowsFork === true) url.searchParams.set("allowsFork", "1");
|
|
37
|
+
if (spec.sessionMode !== void 0) url.searchParams.set("sessionMode", spec.sessionMode);
|
|
38
|
+
if (spec.ownerProviderId !== void 0 && spec.ownerProviderId !== "") url.searchParams.set("ownerProviderId", spec.ownerProviderId);
|
|
39
|
+
if (spec.ownerTenantId !== void 0 && spec.ownerTenantId !== "") url.searchParams.set("ownerTenantId", spec.ownerTenantId);
|
|
40
|
+
for (const subject of spec.allowedSubjectIds) url.searchParams.append("allowedSubjectId", subject);
|
|
41
|
+
if (spec.expiresAt !== void 0 && spec.expiresAt !== "") url.searchParams.set("expiresAt", spec.expiresAt);
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region lib/types/owner-gateway/pending-rpc.js
|
|
46
|
+
/** Correlate IPC requests by worker-issued tokens and Relay ids by their exact socket. */
|
|
47
|
+
/** Request identities remain distinct when a Relay reconnects and reuses its numeric ids. */
|
|
48
|
+
var PendingRpc = class {
|
|
49
|
+
tokens = /* @__PURE__ */ new Map();
|
|
50
|
+
owners = /* @__PURE__ */ new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Register a request with a new IPC token.
|
|
53
|
+
* @param shareId Share carrying the request.
|
|
54
|
+
* @param id Relay id on this socket.
|
|
55
|
+
* @param owner Exact socket that received the request.
|
|
56
|
+
* @returns Registered request; an existing id must be cancelled before reuse on the same socket.
|
|
57
|
+
*/
|
|
58
|
+
add(shareId, id, owner) {
|
|
59
|
+
const request = {
|
|
60
|
+
token: randomUUID(),
|
|
61
|
+
shareId,
|
|
62
|
+
id,
|
|
63
|
+
owner,
|
|
64
|
+
since: Date.now()
|
|
65
|
+
};
|
|
66
|
+
let requests = this.owners.get(owner);
|
|
67
|
+
if (requests === void 0) {
|
|
68
|
+
requests = /* @__PURE__ */ new Map();
|
|
69
|
+
this.owners.set(owner, requests);
|
|
70
|
+
}
|
|
71
|
+
if (requests.has(id)) throw new Error("Relay request id is already pending on this socket");
|
|
72
|
+
requests.set(id, request);
|
|
73
|
+
this.tokens.set(request.token, request);
|
|
74
|
+
return request;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Claim an answer only when all IPC correlation fields match.
|
|
78
|
+
* @param answer Token, share and Relay id echoed by the Host.
|
|
79
|
+
* @returns The request removed from pending state, or undefined for a stale or mismatched answer.
|
|
80
|
+
*/
|
|
81
|
+
take(answer) {
|
|
82
|
+
const request = this.tokens.get(answer.token);
|
|
83
|
+
if (request === void 0 || request.shareId !== answer.shareId || request.id !== answer.id) return void 0;
|
|
84
|
+
return this.cancel(request.owner, request.id);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove one socket-local request.
|
|
88
|
+
* @param owner Socket receiving the cancellation.
|
|
89
|
+
* @param id Relay id on that socket.
|
|
90
|
+
* @returns Removed request for cancellation notification, or undefined if it already settled.
|
|
91
|
+
*/
|
|
92
|
+
cancel(owner, id) {
|
|
93
|
+
const requests = this.owners.get(owner);
|
|
94
|
+
const request = requests?.get(id);
|
|
95
|
+
if (request === void 0) return void 0;
|
|
96
|
+
this.tokens.delete(request.token);
|
|
97
|
+
requests?.delete(id);
|
|
98
|
+
if (requests?.size === 0) this.owners.delete(owner);
|
|
99
|
+
return request;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Remove all requests on a closed socket.
|
|
103
|
+
* @param owner Socket being disposed.
|
|
104
|
+
* @returns Removed requests for Host cancellation notifications.
|
|
105
|
+
*/
|
|
106
|
+
clear(owner) {
|
|
107
|
+
const requests = [...this.owners.get(owner)?.values() ?? []];
|
|
108
|
+
for (const request of requests) this.tokens.delete(request.token);
|
|
109
|
+
this.owners.delete(owner);
|
|
110
|
+
return requests;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region lib/types/owner-gateway/worker.js
|
|
115
|
+
/** Owner-side gateway worker: one process multiplexes every share over IPC to the owner host. */
|
|
116
|
+
const HEARTBEAT_INTERVAL_MS = 25e3;
|
|
117
|
+
/**
|
|
118
|
+
* Consecutive unanswered heartbeats tolerated before this worker drops its own Relay socket.
|
|
119
|
+
*
|
|
120
|
+
* One miss means a busy event loop, not a dead connection: terminating there aborts every in-flight
|
|
121
|
+
* viewer stream on the share and forces a re-registration, which is the periodic reconnect an owner
|
|
122
|
+
* would otherwise never see the cause of.
|
|
123
|
+
*/
|
|
124
|
+
const GATEWAY_MAX_MISSED_PONGS = 2;
|
|
125
|
+
/** IPC tokens identify requests; socket-local Relay ids never select a Host response. */
|
|
126
|
+
const pendingRpc = new PendingRpc();
|
|
127
|
+
/**
|
|
128
|
+
* Run one owner worker that multiplexes every share over IPC. The worker owns
|
|
129
|
+
* one outbound Relay socket per share, but one process and one owner DSH.
|
|
130
|
+
*/
|
|
131
|
+
function runGatewayWorker() {
|
|
132
|
+
const shares = /* @__PURE__ */ new Map();
|
|
133
|
+
notifyWorker({ type: "ready" });
|
|
134
|
+
process.on("message", (value) => {
|
|
135
|
+
const command = parseTunnelWorkerCommand(value);
|
|
136
|
+
if (command === void 0) {
|
|
137
|
+
console.warn("[liveshare] worker refused one owner command");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (command.type === "share-rpc-response") {
|
|
141
|
+
if ((shares.get(command.shareId)?.gateway)?.registrationToken !== command.registrationToken) return;
|
|
142
|
+
const request = pendingRpc.take(command);
|
|
143
|
+
if (request === void 0) {
|
|
144
|
+
console.warn(`[liveshare] worker dropped late read response relayId=${String(command.id)}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const gateway = request.owner.socket;
|
|
148
|
+
if (gateway.readyState === WebSocket.OPEN) {
|
|
149
|
+
const observed = viewerRequestFields(command.body);
|
|
150
|
+
console.log(`[liveshare] worker read answered share=${request.shareId} relayId=${String(command.id)} op=${observed.op} streamId=${observed.streamId} status=${String(command.status)} elapsedMs=${String(Date.now() - request.since)}`);
|
|
151
|
+
sendFrame(gateway, {
|
|
152
|
+
type: "share-rpc-res",
|
|
153
|
+
id: command.id,
|
|
154
|
+
status: command.status,
|
|
155
|
+
...command.body === void 0 ? {} : { body: command.body }
|
|
156
|
+
});
|
|
157
|
+
} else console.warn(`[liveshare] worker cannot deliver read response share=${request.shareId} relayId=${String(command.id)} — gateway is not connected`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (command.type === "add-share") addShare(shares, command.share, command.registrationToken);
|
|
161
|
+
else if (command.type === "remove-share") {
|
|
162
|
+
console.warn(`[liveshare] worker dropping one share by owner command share=${command.shareId}`);
|
|
163
|
+
removeShare(shares, command.shareId);
|
|
164
|
+
} else {
|
|
165
|
+
console.warn(`[liveshare] worker shutting down command=${String(command.type)} shares=${String(shares.size)}`);
|
|
166
|
+
for (const shareId of [...shares.keys()]) removeShare(shares, shareId);
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
process.on("disconnect", () => {
|
|
171
|
+
console.warn(`[liveshare] worker lost its host connection shares=${String(shares.size)}`);
|
|
172
|
+
for (const shareId of [...shares.keys()]) removeShare(shares, shareId);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function addShare(shares, spec, registrationToken) {
|
|
177
|
+
if (shares.get(spec.shareId)?.registrationToken === registrationToken) return;
|
|
178
|
+
removeShare(shares, spec.shareId);
|
|
179
|
+
const state = {
|
|
180
|
+
spec,
|
|
181
|
+
registrationToken,
|
|
182
|
+
accessVersion: void 0,
|
|
183
|
+
gateway: void 0,
|
|
184
|
+
heartbeat: void 0,
|
|
185
|
+
retryTimer: void 0,
|
|
186
|
+
retry: INITIAL_RETRY_BACKOFF,
|
|
187
|
+
openedAt: void 0,
|
|
188
|
+
stopped: false
|
|
189
|
+
};
|
|
190
|
+
shares.set(spec.shareId, state);
|
|
191
|
+
connectShare(shares, state);
|
|
192
|
+
}
|
|
193
|
+
function removeShare(shares, shareId) {
|
|
194
|
+
const state = shares.get(shareId);
|
|
195
|
+
if (state === void 0) return;
|
|
196
|
+
state.stopped = true;
|
|
197
|
+
if (state.retryTimer !== void 0) clearTimeout(state.retryTimer);
|
|
198
|
+
clearInterval(state.heartbeat);
|
|
199
|
+
if (state.gateway !== void 0) clearPendingRpc(state.gateway);
|
|
200
|
+
state.gateway?.socket.close(1e3, "share removed");
|
|
201
|
+
shares.delete(shareId);
|
|
202
|
+
}
|
|
203
|
+
function connectShare(shares, state) {
|
|
204
|
+
const gateway = new WebSocket(registrationUrl(state.spec), { headers: {
|
|
205
|
+
authorization: `Bearer ${state.spec.gatewayToken}`,
|
|
206
|
+
...state.spec.ownerCredential === void 0 || state.spec.ownerCredential === "" ? {} : { "x-liveshare-owner-credential": state.spec.ownerCredential }
|
|
207
|
+
} });
|
|
208
|
+
const connection = {
|
|
209
|
+
socket: gateway,
|
|
210
|
+
registrationToken: state.registrationToken
|
|
211
|
+
};
|
|
212
|
+
state.gateway = connection;
|
|
213
|
+
let awaitingPong = false;
|
|
214
|
+
let missedPongs = 0;
|
|
215
|
+
let closeReason = "unknown";
|
|
216
|
+
let handshakeStatus;
|
|
217
|
+
state.heartbeat = setInterval(() => {
|
|
218
|
+
if (gateway.readyState !== WebSocket.OPEN) return;
|
|
219
|
+
if (awaitingPong) {
|
|
220
|
+
missedPongs += 1;
|
|
221
|
+
if (missedPongs >= GATEWAY_MAX_MISSED_PONGS) {
|
|
222
|
+
closeReason = `heartbeat-timeout missedPongs=${String(missedPongs)}`;
|
|
223
|
+
gateway.terminate();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.warn(`[liveshare] worker heartbeat missed share=${state.spec.shareId} missedPongs=${String(missedPongs)}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
awaitingPong = true;
|
|
230
|
+
gateway.ping();
|
|
231
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
232
|
+
gateway.on("pong", () => {
|
|
233
|
+
awaitingPong = false;
|
|
234
|
+
missedPongs = 0;
|
|
235
|
+
});
|
|
236
|
+
gateway.on("open", () => {
|
|
237
|
+
if (state.stopped || state.gateway !== connection) return;
|
|
238
|
+
state.openedAt = Date.now();
|
|
239
|
+
notifyWorker({
|
|
240
|
+
type: "share-connected",
|
|
241
|
+
registrationToken: state.registrationToken,
|
|
242
|
+
shareId: state.spec.shareId
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
gateway.on("message", (data) => {
|
|
246
|
+
awaitingPong = false;
|
|
247
|
+
missedPongs = 0;
|
|
248
|
+
const text = rawText(data);
|
|
249
|
+
const frame = parseRelayFrame(text);
|
|
250
|
+
if (frame !== void 0) {
|
|
251
|
+
if (!state.stopped && state.gateway === connection) handleGatewayFrame(state, connection, frame);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const refusal = unsupportedFrameRefusal(text);
|
|
255
|
+
console.warn(`[liveshare] worker refused one relay frame share=${state.spec.shareId} frame=${refusal === void 0 ? "unanswerable" : `status=${String(refusal.status)} relayId=${String(refusal.id)}`}`);
|
|
256
|
+
if (refusal !== void 0 && !state.stopped && state.gateway === connection) sendFrame(gateway, refusal);
|
|
257
|
+
});
|
|
258
|
+
gateway.on("close", (code, socketReason) => {
|
|
259
|
+
clearPendingRpc(connection);
|
|
260
|
+
if (state.gateway !== connection) return;
|
|
261
|
+
clearInterval(state.heartbeat);
|
|
262
|
+
state.gateway = void 0;
|
|
263
|
+
state.accessVersion = void 0;
|
|
264
|
+
if (state.stopped) return;
|
|
265
|
+
console.warn(`[liveshare] worker gateway closed share=${state.spec.shareId} code=${String(code)} reason=${closeReason} socketReason=${socketReason.toString()} retryMs=${String(state.retry.delayMs)}`);
|
|
266
|
+
const refusal = relayRefusal(handshakeStatus);
|
|
267
|
+
if (refusal !== void 0) {
|
|
268
|
+
state.stopped = true;
|
|
269
|
+
shares.delete(state.spec.shareId);
|
|
270
|
+
console.warn(`[liveshare] worker stopped share=${state.spec.shareId} ${refusal}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (state.openedAt !== void 0 && Date.now() - state.openedAt >= 3e4) state.retry = retryBackoffSettled();
|
|
274
|
+
state.openedAt = void 0;
|
|
275
|
+
const delayMs = state.retry.delayMs;
|
|
276
|
+
state.retry = retryBackoffNext(state.retry);
|
|
277
|
+
state.retryTimer = setTimeout(() => {
|
|
278
|
+
connectShare(shares, state);
|
|
279
|
+
}, delayMs);
|
|
280
|
+
notifyWorker({
|
|
281
|
+
type: "share-closed",
|
|
282
|
+
registrationToken: state.registrationToken,
|
|
283
|
+
shareId: state.spec.shareId
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
gateway.on("error", (error) => {
|
|
287
|
+
if (state.stopped || state.gateway !== connection) return;
|
|
288
|
+
const detail = (handshakeStatus === void 0 ? void 0 : relayRefusal(handshakeStatus) ?? `relay answered registration status=${String(handshakeStatus)}`) ?? (error instanceof Error ? error.message : String(error));
|
|
289
|
+
closeReason = `gateway-error ${detail}`;
|
|
290
|
+
notifyWorker({
|
|
291
|
+
type: "share-error",
|
|
292
|
+
registrationToken: state.registrationToken,
|
|
293
|
+
shareId: state.spec.shareId,
|
|
294
|
+
message: detail
|
|
295
|
+
});
|
|
296
|
+
gateway.terminate();
|
|
297
|
+
});
|
|
298
|
+
gateway.on("unexpected-response", (_request, response) => {
|
|
299
|
+
handshakeStatus = response.statusCode;
|
|
300
|
+
gateway.terminate();
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function handleGatewayFrame(state, connection, frame) {
|
|
304
|
+
const shareId = state.spec.shareId;
|
|
305
|
+
switch (frame.type) {
|
|
306
|
+
case "share-access-state": {
|
|
307
|
+
if (state.accessVersion !== void 0 && state.accessVersion >= frame.version) return;
|
|
308
|
+
const { expiresAt: _expiresAt, ...retained } = state.spec;
|
|
309
|
+
state.spec = {
|
|
310
|
+
...retained,
|
|
311
|
+
accessMode: frame.accessMode,
|
|
312
|
+
allowedSubjectIds: frame.allowedSubjectIds,
|
|
313
|
+
sessionMode: frame.sessionMode,
|
|
314
|
+
allowsFork: frame.allowsFork === true,
|
|
315
|
+
...frame.expiresAt === void 0 ? {} : { expiresAt: frame.expiresAt }
|
|
316
|
+
};
|
|
317
|
+
state.accessVersion = frame.version;
|
|
318
|
+
notifyWorker({
|
|
319
|
+
...frame,
|
|
320
|
+
registrationToken: state.registrationToken,
|
|
321
|
+
shareId
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
case "share-rpc": {
|
|
326
|
+
const previous = pendingRpc.cancel(connection, frame.id);
|
|
327
|
+
if (previous !== void 0) notifyCancellation(previous);
|
|
328
|
+
const request = pendingRpc.add(shareId, frame.id, connection);
|
|
329
|
+
const observed = viewerRequestFields(frame.body);
|
|
330
|
+
console.log(`[liveshare] worker read received share=${shareId} relayId=${String(frame.id)} op=${observed.op} streamId=${observed.streamId}`);
|
|
331
|
+
notifyWorker({
|
|
332
|
+
type: "share-rpc-request",
|
|
333
|
+
registrationToken: connection.registrationToken,
|
|
334
|
+
token: request.token,
|
|
335
|
+
id: frame.id,
|
|
336
|
+
shareId,
|
|
337
|
+
body: frame.body,
|
|
338
|
+
actor: frame.actor
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
case "share-rpc-cancel": {
|
|
343
|
+
const request = pendingRpc.cancel(connection, frame.id);
|
|
344
|
+
if (request !== void 0) notifyCancellation(request);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function notifyWorker(event) {
|
|
349
|
+
if (process.send !== void 0) process.send(event);
|
|
350
|
+
}
|
|
351
|
+
/** Cancel Host work before forgetting requests on a closed Relay socket. */
|
|
352
|
+
function clearPendingRpc(connection) {
|
|
353
|
+
for (const request of pendingRpc.clear(connection)) notifyCancellation(request);
|
|
354
|
+
}
|
|
355
|
+
function notifyCancellation(request) {
|
|
356
|
+
notifyWorker({
|
|
357
|
+
type: "share-rpc-cancel",
|
|
358
|
+
registrationToken: request.owner.registrationToken,
|
|
359
|
+
token: request.token,
|
|
360
|
+
shareId: request.shareId,
|
|
361
|
+
id: request.id
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region lib/types/owner-gateway/cli.js
|
|
366
|
+
/** Entry point for the owner gateway worker, launched by the host over an IPC channel. */
|
|
367
|
+
if (process.send === void 0) throw new Error("Liveshare gateway requires the owner worker IPC channel");
|
|
368
|
+
runGatewayWorker();
|
|
369
|
+
//#endregion
|
|
370
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//#region lib/types/host/relay-url.js
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a public Relay base URL by removing query, hash, and trailing slashes.
|
|
4
|
+
* @param value Public Relay URL or base URL.
|
|
5
|
+
* @returns Canonical base URL without credentials, query, hash, or trailing slash.
|
|
6
|
+
*/
|
|
7
|
+
function normalizeRelayUrl(value) {
|
|
8
|
+
if (value === "") throw new Error("liveshare relayUrl must not be empty");
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("liveshare relayUrl must be an absolute URL");
|
|
14
|
+
}
|
|
15
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("liveshare relayUrl must use http or https");
|
|
16
|
+
if (url.username !== "" || url.password !== "") throw new Error("liveshare relayUrl must not include credentials");
|
|
17
|
+
url.search = "";
|
|
18
|
+
url.hash = "";
|
|
19
|
+
url.pathname = url.pathname.replace(/\/+$/u, "");
|
|
20
|
+
return url.href.replace(/\/$/u, "");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Split one Relay base URL into its origin and base path.
|
|
24
|
+
* @param relay Public Relay URL or base URL.
|
|
25
|
+
* @returns Validated origin and trailing-slash-free base path.
|
|
26
|
+
*/
|
|
27
|
+
function relayEndpoint(relay) {
|
|
28
|
+
const url = new URL(normalizeRelayUrl(relay));
|
|
29
|
+
return {
|
|
30
|
+
origin: url.origin,
|
|
31
|
+
basePath: url.pathname.replace(/\/+$/u, "")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region lib/types/owner-gateway/relay-refusal.js
|
|
36
|
+
/**
|
|
37
|
+
* When a Relay handshake response ends one registration for good.
|
|
38
|
+
*
|
|
39
|
+
* The Relay answers a gateway handshake it will not establish with an HTTP status instead of
|
|
40
|
+
* upgrading. A credential the Relay does not accept (401), one it forbids for this share (403), or a
|
|
41
|
+
* share it no longer serves (410), or an unsupported protocol (426) ends this registration.
|
|
42
|
+
* Reconnecting with the same token and protocol reaches the same answer, so the share stays offline and the status becomes its diagnostic
|
|
43
|
+
* instead of a silent retry. Every other failure — a Relay that is down (5xx), a rate limit, a
|
|
44
|
+
* timeout, a refused socket — is transient and keeps the ordinary reconnect schedule.
|
|
45
|
+
*/
|
|
46
|
+
/** The Relay handshake statuses that end a registration for good. */
|
|
47
|
+
const RELAY_REFUSAL_STATUSES = [
|
|
48
|
+
401,
|
|
49
|
+
403,
|
|
50
|
+
410,
|
|
51
|
+
426
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Read one handshake status as a decision about the registration it carried.
|
|
55
|
+
* @param status HTTP status the Relay answered the gateway handshake with, if it answered.
|
|
56
|
+
* @returns The refusal to report, or undefined when the failure is worth retrying.
|
|
57
|
+
*/
|
|
58
|
+
function relayRefusal(status) {
|
|
59
|
+
if (status !== void 0 && RELAY_REFUSAL_STATUSES.includes(status)) return `relay refused registration status=${String(status)}`;
|
|
60
|
+
}
|
|
61
|
+
/** The one form every permanent refusal reaches a Host as; nothing else composes it. */
|
|
62
|
+
const RELAY_REFUSAL_MESSAGE = /^relay refused registration status=(\d+)$/u;
|
|
63
|
+
/**
|
|
64
|
+
* Read one reported registration failure as the Relay status that decided it.
|
|
65
|
+
*
|
|
66
|
+
* The gateway reports a permanent refusal exactly as `relayRefusal` composes it, and every other
|
|
67
|
+
* failure it reports — a status it did not refuse with, a dropped socket, a timeout — is worth
|
|
68
|
+
* another attempt. A Host reading the message can therefore tell the owner to act on this address
|
|
69
|
+
* instead of promising a retry that reaches the same answer, and can say which status decided it.
|
|
70
|
+
* @param message - Failure the gateway reported for one registration.
|
|
71
|
+
* @returns The refusing status, or undefined when the failure is worth retrying.
|
|
72
|
+
*/
|
|
73
|
+
function relayRefusalStatus(message) {
|
|
74
|
+
const match = RELAY_REFUSAL_MESSAGE.exec(message);
|
|
75
|
+
if (match === null) return void 0;
|
|
76
|
+
const status = Number(match[1]);
|
|
77
|
+
return RELAY_REFUSAL_STATUSES.includes(status) ? status : void 0;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region lib/types/owner-gateway/retry-backoff.js
|
|
81
|
+
/**
|
|
82
|
+
* Delay schedule for one connection that keeps failing.
|
|
83
|
+
*
|
|
84
|
+
* The owner host's worker supervisor and the gateway's per-share Relay socket both reconnect rather
|
|
85
|
+
* than give up, and both treat a connection that stayed up long enough as healthy, so the failure
|
|
86
|
+
* after one starts the delays over instead of continuing the previous run's growth. Extracted from
|
|
87
|
+
* the socket lifecycle so the schedule is deterministic and testable apart from spawning processes.
|
|
88
|
+
*/
|
|
89
|
+
/** Schedule of a connection that has not (yet) proven itself stable. */
|
|
90
|
+
const INITIAL_RETRY_BACKOFF = {
|
|
91
|
+
delayMs: 500,
|
|
92
|
+
attempt: 0
|
|
93
|
+
};
|
|
94
|
+
/** Ceiling for the doubling delay: a failing peer is retried, never hammered. */
|
|
95
|
+
const RETRY_BACKOFF_MAX_MS = 1e4;
|
|
96
|
+
/**
|
|
97
|
+
* A connection that stays up this long is healthy, so the next failure starts the delays over.
|
|
98
|
+
* A peer that replaces the connection right after the handshake keeps the previous backoff instead
|
|
99
|
+
* of retrying every 500 ms.
|
|
100
|
+
*/
|
|
101
|
+
const RETRY_STABLE_MS = 3e4;
|
|
102
|
+
/**
|
|
103
|
+
* Advance the schedule after one failed attempt.
|
|
104
|
+
* @param state Backoff of the attempt that just failed.
|
|
105
|
+
* @returns Schedule the retry starts from: the delay to wait and the state after that attempt.
|
|
106
|
+
*/
|
|
107
|
+
function retryBackoffNext(state) {
|
|
108
|
+
return {
|
|
109
|
+
delayMs: Math.min(state.delayMs * 2, RETRY_BACKOFF_MAX_MS),
|
|
110
|
+
attempt: state.attempt + 1
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Schedule for a connection that has proven itself stable.
|
|
115
|
+
* @returns The schedule's first entry, so the next failure is retried promptly.
|
|
116
|
+
*/
|
|
117
|
+
function retryBackoffSettled() {
|
|
118
|
+
return INITIAL_RETRY_BACKOFF;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { relayRefusal as a, relayEndpoint as c, retryBackoffSettled as i, RETRY_STABLE_MS as n, relayRefusalStatus as o, retryBackoffNext as r, normalizeRelayUrl as s, INITIAL_RETRY_BACKOFF as t };
|