@plurnk/plurnk-schemes-http 1.16.4 → 1.17.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/.env.defaults +11 -33
- package/README.md +12 -12
- package/SPEC.md +54 -44
- package/dist/Http.d.ts.map +1 -1
- package/dist/Http.js +27 -44
- package/dist/Http.js.map +1 -1
- package/dist/HttpGet.js +2 -2
- package/dist/HttpGet.js.map +1 -1
- package/dist/HttpRequester.d.ts.map +1 -1
- package/dist/HttpRequester.js +21 -6
- package/dist/HttpRequester.js.map +1 -1
- package/dist/LiveAcquisitions.d.ts +1 -1
- package/dist/LiveAcquisitions.d.ts.map +1 -1
- package/dist/LiveAcquisitions.js +12 -9
- package/dist/LiveAcquisitions.js.map +1 -1
- package/dist/WebFetcher.d.ts +2 -6
- package/dist/WebFetcher.d.ts.map +1 -1
- package/dist/WebFetcher.js +69 -103
- package/dist/WebFetcher.js.map +1 -1
- package/dist/Ws.d.ts.map +1 -1
- package/dist/Ws.js +228 -241
- package/dist/Ws.js.map +1 -1
- package/docs/https.md +33 -28
- package/docs/wss.md +25 -18
- package/package.json +4 -3
package/dist/Ws.js
CHANGED
|
@@ -22,21 +22,10 @@ class Ws {
|
|
|
22
22
|
channels: { [MESSAGES]: "text/plain" },
|
|
23
23
|
defaultChannel: MESSAGES,
|
|
24
24
|
category: "data",
|
|
25
|
-
entryOwner: "worker",
|
|
26
|
-
inherit: "none",
|
|
27
25
|
writableBy: ["model", "client"],
|
|
28
26
|
volatile: true,
|
|
29
27
|
modelVisible: true,
|
|
30
28
|
glyph: "🔌",
|
|
31
|
-
example: [
|
|
32
|
-
"### READ0 (wss://api.example.com/feed)",
|
|
33
|
-
"",
|
|
34
|
-
"### EDIT0 (wss://api.example.com/feed)",
|
|
35
|
-
'{"type":"subscribe","channel":"updates"}',
|
|
36
|
-
"",
|
|
37
|
-
"### SEND0 (wss://api.example.com/feed)",
|
|
38
|
-
'{"type":"message","text":"Hello"}',
|
|
39
|
-
].join("\n"),
|
|
40
29
|
documentation,
|
|
41
30
|
traits: ["web"],
|
|
42
31
|
};
|
|
@@ -66,12 +55,13 @@ class Ws {
|
|
|
66
55
|
return address;
|
|
67
56
|
const { url } = address;
|
|
68
57
|
const pathname = request.pathname;
|
|
69
|
-
const key = _a.#key(ctx.
|
|
58
|
+
const key = _a.#key(ctx.workspaceId, address);
|
|
70
59
|
const existing = this.#sockets.get(key);
|
|
71
60
|
if (existing !== undefined) {
|
|
72
|
-
return { status: 200, connectionState: existing.state };
|
|
61
|
+
return existing.opened ? { status: 200, connectionState: existing.state } : existing.acquisition;
|
|
73
62
|
}
|
|
74
63
|
const done = Promise.withResolvers();
|
|
64
|
+
const shared = Promise.withResolvers();
|
|
75
65
|
const owner = {
|
|
76
66
|
socket: null,
|
|
77
67
|
state: "claimed",
|
|
@@ -80,241 +70,246 @@ class Ws {
|
|
|
80
70
|
shutdownRequested: false,
|
|
81
71
|
async shutdown() { },
|
|
82
72
|
done: done.promise,
|
|
73
|
+
acquisition: shared.promise,
|
|
83
74
|
};
|
|
84
75
|
this.#sockets.set(key, owner);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// {§ws-lifecycle} Seed the declared channel before subscription open.
|
|
89
|
-
const written = await ctx.entries.write(pathname, _a.#seedEntry());
|
|
90
|
-
if (Results.isErrorStatus(written.status))
|
|
91
|
-
return _a.#passthrough(written);
|
|
92
|
-
if (owner.shutdownRequested)
|
|
93
|
-
return _a.#cancelled(url);
|
|
94
|
-
let requestCancel = () => { owner.shutdownRequested = true; };
|
|
95
|
-
const handle = { cancel: () => requestCancel() };
|
|
96
|
-
const subscription = await ctx.subscriptions.open(pathname, handle);
|
|
97
|
-
if (owner.shutdownRequested) {
|
|
98
|
-
const cancelled = _a.#cancelled(url);
|
|
99
|
-
await subscription.close(cancelled, "WebSocket execution was cancelled.");
|
|
100
|
-
return cancelled;
|
|
101
|
-
}
|
|
102
|
-
let socket;
|
|
76
|
+
const acquire = async () => {
|
|
77
|
+
let retained = false;
|
|
78
|
+
let settled = false;
|
|
103
79
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
80
|
+
// {§ws-lifecycle} Seed the declared channel before subscription open.
|
|
81
|
+
const written = await ctx.entries.write(pathname, _a.#seedEntry());
|
|
82
|
+
if (Results.isErrorStatus(written.status))
|
|
83
|
+
return _a.#passthrough(written);
|
|
84
|
+
if (owner.shutdownRequested)
|
|
85
|
+
return _a.#cancelled(url);
|
|
86
|
+
let requestCancel = () => { owner.shutdownRequested = true; };
|
|
87
|
+
const handle = { cancel: () => requestCancel() };
|
|
88
|
+
const subscription = await ctx.subscriptions.open(pathname, handle);
|
|
89
|
+
if (owner.shutdownRequested) {
|
|
90
|
+
const cancelled = _a.#cancelled(url);
|
|
91
|
+
await subscription.close(cancelled, "WebSocket execution was cancelled.");
|
|
92
|
+
return cancelled;
|
|
93
|
+
}
|
|
94
|
+
let socket;
|
|
95
|
+
try {
|
|
96
|
+
socket = this.#connect(url);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error("WebSocket connection failed", { url, err });
|
|
100
|
+
const detail = `The WebSocket connection to ${url} failed.`;
|
|
101
|
+
const result = _a.#bad(502, "connect-failed", detail, {
|
|
102
|
+
target: url,
|
|
103
|
+
stage: "connection",
|
|
104
|
+
retryable: true,
|
|
105
|
+
});
|
|
106
|
+
await subscription.close(result, detail);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
owner.socket = socket;
|
|
110
|
+
owner.state = "connecting";
|
|
111
|
+
const acquisition = Promise.withResolvers();
|
|
112
|
+
let messages = 0;
|
|
113
|
+
let settlement = null;
|
|
114
|
+
let frameFailure = null;
|
|
115
|
+
const pending = new Set();
|
|
116
|
+
const settle = (terminal, summary, transportClose, initial = terminal) => {
|
|
117
|
+
if (settled)
|
|
118
|
+
return settlement ?? Promise.resolve();
|
|
119
|
+
settled = true;
|
|
120
|
+
owner.state = "settling";
|
|
121
|
+
subscription.removeEventListener("abort", onAbort);
|
|
122
|
+
settlement = (async () => {
|
|
123
|
+
const errors = [];
|
|
124
|
+
try {
|
|
125
|
+
await Promise.allSettled([...pending]);
|
|
126
|
+
const failedFrame = frameFailure;
|
|
127
|
+
const effectiveTerminal = !Results.isErrorStatus(terminal.status) && failedFrame !== null
|
|
128
|
+
? failedFrame.result
|
|
129
|
+
: terminal;
|
|
130
|
+
const effectiveSummary = !Results.isErrorStatus(terminal.status) && failedFrame !== null
|
|
131
|
+
? failedFrame.summary
|
|
132
|
+
: typeof summary === "function" ? summary() : summary;
|
|
133
|
+
if (transportClose !== undefined) {
|
|
134
|
+
try {
|
|
135
|
+
socket.close(transportClose.code, transportClose.reason);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
errors.push(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
142
141
|
try {
|
|
143
|
-
|
|
142
|
+
await subscription.close(effectiveTerminal, effectiveSummary);
|
|
143
|
+
if (!owner.opened)
|
|
144
|
+
acquisition.resolve(initial);
|
|
144
145
|
}
|
|
145
146
|
catch (error) {
|
|
147
|
+
if (!owner.opened)
|
|
148
|
+
acquisition.reject(error);
|
|
146
149
|
errors.push(error);
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
finally {
|
|
153
|
+
if (this.#sockets.get(key) === owner)
|
|
154
|
+
this.#sockets.delete(key);
|
|
155
|
+
done.resolve();
|
|
153
156
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
errors
|
|
157
|
+
if (errors.length === 1)
|
|
158
|
+
throw errors[0];
|
|
159
|
+
if (errors.length > 1)
|
|
160
|
+
throw new AggregateError(errors, "WebSocket terminal cleanup failed");
|
|
161
|
+
})();
|
|
162
|
+
return settlement;
|
|
163
|
+
};
|
|
164
|
+
const reportCleanupFailure = (error) => {
|
|
165
|
+
console.error("WebSocket terminal cleanup failed", { url, error });
|
|
166
|
+
};
|
|
167
|
+
const track = (task) => {
|
|
168
|
+
pending.add(task);
|
|
169
|
+
void task.then(() => pending.delete(task), () => pending.delete(task));
|
|
170
|
+
return task;
|
|
171
|
+
};
|
|
172
|
+
const cancelled = () => _a.#cancelled(url);
|
|
173
|
+
requestCancel = () => {
|
|
174
|
+
owner.shutdownRequested = true;
|
|
175
|
+
const result = cancelled();
|
|
176
|
+
void settle(result, "WebSocket execution was cancelled.", {
|
|
177
|
+
code: SOCKET_CLOSE_NORMAL,
|
|
178
|
+
reason: "cancelled",
|
|
179
|
+
}).catch(reportCleanupFailure);
|
|
180
|
+
};
|
|
181
|
+
owner.shutdown = () => {
|
|
182
|
+
const result = cancelled();
|
|
183
|
+
return settle(result, () => `ws closed (${SOCKET_CLOSE_NORMAL}); ${messages} messages`, {
|
|
184
|
+
code: SOCKET_CLOSE_NORMAL,
|
|
185
|
+
reason: "shutdown",
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
const onAbort = () => requestCancel();
|
|
189
|
+
subscription.addEventListener("abort", onAbort, { once: true });
|
|
190
|
+
const setChannelState = ctx.channels.setState.bind(ctx.channels);
|
|
191
|
+
const streamEvent = ctx.notify.streamEvent.bind(ctx.notify);
|
|
192
|
+
let nativeOpened = false;
|
|
193
|
+
let activation = Promise.resolve();
|
|
194
|
+
let persistenceTail = Promise.resolve();
|
|
195
|
+
socket.addEventListener("open", () => {
|
|
196
|
+
if (owner.state !== "connecting")
|
|
197
|
+
return;
|
|
198
|
+
nativeOpened = true;
|
|
199
|
+
activation = track((async () => {
|
|
200
|
+
try {
|
|
201
|
+
const activated = await setChannelState(pathname, MESSAGES, "active");
|
|
202
|
+
if (Results.isErrorStatus(activated.status)) {
|
|
203
|
+
const result = _a.#passthrough(activated);
|
|
204
|
+
void settle(result, result.problem?.detail ?? "WebSocket channel activation failed.", { code: SOCKET_CLOSE_INTERNAL_ERROR, reason: "activation failed" }).catch(reportCleanupFailure);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (owner.state !== "connecting")
|
|
208
|
+
return;
|
|
209
|
+
streamEvent(pathname, MESSAGES, "active", 0);
|
|
210
|
+
owner.opened = true;
|
|
211
|
+
owner.state = "open";
|
|
212
|
+
acquisition.resolve({ shape: "passthrough", status: 102 });
|
|
158
213
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
throw errors[0];
|
|
167
|
-
if (errors.length > 1)
|
|
168
|
-
throw new AggregateError(errors, "WebSocket terminal cleanup failed");
|
|
169
|
-
})();
|
|
170
|
-
return settlement;
|
|
171
|
-
};
|
|
172
|
-
const reportCleanupFailure = (error) => {
|
|
173
|
-
console.error("WebSocket terminal cleanup failed", { url, error });
|
|
174
|
-
};
|
|
175
|
-
const track = (task) => {
|
|
176
|
-
pending.add(task);
|
|
177
|
-
void task.then(() => pending.delete(task), () => pending.delete(task));
|
|
178
|
-
return task;
|
|
179
|
-
};
|
|
180
|
-
const cancelled = () => _a.#cancelled(url);
|
|
181
|
-
requestCancel = () => {
|
|
182
|
-
owner.shutdownRequested = true;
|
|
183
|
-
const result = cancelled();
|
|
184
|
-
void settle(result, "WebSocket execution was cancelled.", {
|
|
185
|
-
code: SOCKET_CLOSE_NORMAL,
|
|
186
|
-
reason: "cancelled",
|
|
187
|
-
}).catch(reportCleanupFailure);
|
|
188
|
-
};
|
|
189
|
-
owner.shutdown = () => {
|
|
190
|
-
const result = cancelled();
|
|
191
|
-
return settle(result, () => `ws closed (${SOCKET_CLOSE_NORMAL}); ${messages} messages`, {
|
|
192
|
-
code: SOCKET_CLOSE_NORMAL,
|
|
193
|
-
reason: "shutdown",
|
|
194
|
-
});
|
|
195
|
-
};
|
|
196
|
-
const onAbort = () => requestCancel();
|
|
197
|
-
subscription.addEventListener("abort", onAbort, { once: true });
|
|
198
|
-
const setChannelState = ctx.channels.setState.bind(ctx.channels);
|
|
199
|
-
const streamEvent = ctx.notify.streamEvent.bind(ctx.notify);
|
|
200
|
-
let nativeOpened = false;
|
|
201
|
-
let activation = Promise.resolve();
|
|
202
|
-
let persistenceTail = Promise.resolve();
|
|
203
|
-
socket.addEventListener("open", () => {
|
|
204
|
-
if (owner.state !== "connecting")
|
|
205
|
-
return;
|
|
206
|
-
nativeOpened = true;
|
|
207
|
-
activation = track((async () => {
|
|
208
|
-
try {
|
|
209
|
-
const activated = await setChannelState(pathname, MESSAGES, "active");
|
|
210
|
-
if (Results.isErrorStatus(activated.status)) {
|
|
211
|
-
const result = _a.#passthrough(activated);
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error("WebSocket channel activation failed", { url, err });
|
|
216
|
+
const result = _a.#bad(500, "channel-activation-failed", "The WebSocket message channel could not enter its active state.", {
|
|
217
|
+
target: url,
|
|
218
|
+
stage: "persistence",
|
|
219
|
+
retryable: false,
|
|
220
|
+
});
|
|
212
221
|
void settle(result, result.problem?.detail ?? "WebSocket channel activation failed.", { code: SOCKET_CLOSE_INTERNAL_ERROR, reason: "activation failed" }).catch(reportCleanupFailure);
|
|
213
|
-
return;
|
|
214
222
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
console.error("WebSocket channel activation failed", { url, err });
|
|
224
|
-
const result = _a.#bad(500, "channel-activation-failed", "The WebSocket message channel could not enter its active state.", {
|
|
225
|
-
target: url,
|
|
226
|
-
stage: "persistence",
|
|
227
|
-
retryable: false,
|
|
228
|
-
});
|
|
229
|
-
void settle(result, result.problem?.detail ?? "WebSocket channel activation failed.", { code: SOCKET_CLOSE_INTERNAL_ERROR, reason: "activation failed" }).catch(reportCleanupFailure);
|
|
230
|
-
}
|
|
231
|
-
})());
|
|
232
|
-
}, { once: true });
|
|
233
|
-
// {§ws-lifecycle} One owner chain makes transport order independent
|
|
234
|
-
// of the subscription implementation's asynchronous scheduler.
|
|
235
|
-
socket.addEventListener("message", (event) => {
|
|
236
|
-
if (!nativeOpened || settled || frameFailure !== null)
|
|
237
|
-
return;
|
|
238
|
-
const data = event.data;
|
|
239
|
-
const persistence = persistenceTail.then(async () => {
|
|
240
|
-
await activation;
|
|
241
|
-
if (!owner.opened || frameFailure !== null)
|
|
223
|
+
})());
|
|
224
|
+
}, { once: true });
|
|
225
|
+
// {§ws-lifecycle} One owner chain makes transport order independent
|
|
226
|
+
// of the subscription implementation's asynchronous scheduler.
|
|
227
|
+
socket.addEventListener("message", (event) => {
|
|
228
|
+
if (!nativeOpened || settled || frameFailure !== null)
|
|
242
229
|
return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
stage: "materialization",
|
|
248
|
-
retryable: false,
|
|
249
|
-
});
|
|
250
|
-
frameFailure = { result, summary: detail };
|
|
251
|
-
void settle(result, detail, { code: SOCKET_CLOSE_UNSUPPORTED_DATA, reason: "binary unsupported" }).catch(reportCleanupFailure);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
try {
|
|
255
|
-
await subscription.notifyChunk(MESSAGES, data, "text/plain");
|
|
256
|
-
messages += 1;
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
if (frameFailure !== null)
|
|
230
|
+
const data = event.data;
|
|
231
|
+
const persistence = persistenceTail.then(async () => {
|
|
232
|
+
await activation;
|
|
233
|
+
if (!owner.opened || frameFailure !== null)
|
|
260
234
|
return;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
235
|
+
if (typeof data !== "string") {
|
|
236
|
+
const detail = "The received WebSocket frame is binary; the messages channel accepts text only.";
|
|
237
|
+
const result = _a.#bad(415, "binary-frame-unsupported", detail, {
|
|
238
|
+
target: url,
|
|
239
|
+
stage: "materialization",
|
|
240
|
+
retryable: false,
|
|
241
|
+
});
|
|
242
|
+
frameFailure = { result, summary: detail };
|
|
243
|
+
void settle(result, detail, { code: SOCKET_CLOSE_UNSUPPORTED_DATA, reason: "binary unsupported" }).catch(reportCleanupFailure);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
await subscription.notifyChunk(MESSAGES, data, "text/plain");
|
|
248
|
+
messages += 1;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (frameFailure !== null)
|
|
252
|
+
return;
|
|
253
|
+
console.error("WebSocket message persistence failed", { url, err });
|
|
254
|
+
const result = _a.#bad(500, "message-persistence-failed", "The received WebSocket message could not be persisted.", {
|
|
255
|
+
target: url,
|
|
256
|
+
stage: "persistence",
|
|
257
|
+
retryable: false,
|
|
258
|
+
});
|
|
259
|
+
const failureSummary = result.problem?.detail ?? "WebSocket message persistence failed.";
|
|
260
|
+
frameFailure = { result, summary: failureSummary };
|
|
261
|
+
void settle(result, failureSummary, { code: SOCKET_CLOSE_INTERNAL_ERROR, reason: "persistence failed" }).catch(reportCleanupFailure);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
persistenceTail = persistence;
|
|
265
|
+
void track(persistence);
|
|
282
266
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}).catch(reportCleanupFailure);
|
|
287
|
-
});
|
|
288
|
-
socket.addEventListener("close", (event) => {
|
|
289
|
-
if (!owner.opened && !owner.killRequested && !subscription.aborted) {
|
|
290
|
-
const detail = `The WebSocket connection to ${url} closed before it opened.`;
|
|
267
|
+
socket.addEventListener("error", (event) => {
|
|
268
|
+
console.error("WebSocket transport failed", { url, message: event.message });
|
|
269
|
+
const detail = `The WebSocket connection to ${url} failed.`;
|
|
291
270
|
const result = _a.#bad(502, "connection-failed", detail, {
|
|
292
271
|
target: url,
|
|
293
272
|
stage: "connection",
|
|
294
273
|
retryable: true,
|
|
295
274
|
});
|
|
296
|
-
void settle(result, detail
|
|
297
|
-
|
|
275
|
+
void settle(result, detail, {
|
|
276
|
+
code: SOCKET_CLOSE_INTERNAL_ERROR,
|
|
277
|
+
reason: "transport failed",
|
|
278
|
+
}).catch(reportCleanupFailure);
|
|
279
|
+
});
|
|
280
|
+
socket.addEventListener("close", (event) => {
|
|
281
|
+
if (!owner.opened && !owner.killRequested && !subscription.aborted) {
|
|
282
|
+
const detail = `The WebSocket connection to ${url} closed before it opened.`;
|
|
283
|
+
const result = _a.#bad(502, "connection-failed", detail, {
|
|
284
|
+
target: url,
|
|
285
|
+
stage: "connection",
|
|
286
|
+
retryable: true,
|
|
287
|
+
});
|
|
288
|
+
void settle(result, detail).catch(reportCleanupFailure);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const summary = () => `ws closed (${event.code ?? 0}); ${messages} messages`;
|
|
292
|
+
const initial = subscription.aborted ? cancelled() : { shape: "passthrough", status: 102 };
|
|
293
|
+
const terminal = subscription.aborted ? initial : { shape: "passthrough", status: 200 };
|
|
294
|
+
void settle(terminal, summary, undefined, initial).catch(reportCleanupFailure);
|
|
295
|
+
});
|
|
296
|
+
if (owner.shutdownRequested || subscription.aborted)
|
|
297
|
+
requestCancel();
|
|
298
|
+
const result = await acquisition.promise;
|
|
299
|
+
if (owner.opened && result.status === 102)
|
|
300
|
+
retained = true;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
if (!retained && !settled) {
|
|
305
|
+
if (this.#sockets.get(key) === owner)
|
|
306
|
+
this.#sockets.delete(key);
|
|
307
|
+
done.resolve();
|
|
298
308
|
}
|
|
299
|
-
const summary = () => `ws closed (${event.code ?? 0}); ${messages} messages`;
|
|
300
|
-
const initial = subscription.aborted ? cancelled() : { shape: "passthrough", status: 102 };
|
|
301
|
-
const terminal = subscription.aborted ? initial : { shape: "passthrough", status: 200 };
|
|
302
|
-
void settle(terminal, summary, undefined, initial).catch(reportCleanupFailure);
|
|
303
|
-
});
|
|
304
|
-
if (owner.shutdownRequested || subscription.aborted)
|
|
305
|
-
requestCancel();
|
|
306
|
-
const result = await acquisition.promise;
|
|
307
|
-
if (owner.opened && result.status === 102)
|
|
308
|
-
retained = true;
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
if (!retained && !settled) {
|
|
313
|
-
if (this.#sockets.get(key) === owner)
|
|
314
|
-
this.#sockets.delete(key);
|
|
315
|
-
done.resolve();
|
|
316
309
|
}
|
|
317
|
-
}
|
|
310
|
+
};
|
|
311
|
+
void acquire().then(shared.resolve, shared.reject);
|
|
312
|
+
return shared.promise;
|
|
318
313
|
}
|
|
319
314
|
async close() {
|
|
320
315
|
const owners = [...this.#sockets.values()];
|
|
@@ -370,7 +365,7 @@ class Ws {
|
|
|
370
365
|
}
|
|
371
366
|
return this.#write(statement.target, statement.body ?? "", ctx, "EDIT");
|
|
372
367
|
}
|
|
373
|
-
//
|
|
368
|
+
// {§turn-disposition} — SEND is messaging; a KILL closes the owner.
|
|
374
369
|
async send(statement, ctx) {
|
|
375
370
|
if (statement.metadata !== null)
|
|
376
371
|
return _a.#metadataUnsupported();
|
|
@@ -381,20 +376,13 @@ class Ws {
|
|
|
381
376
|
retryable: false,
|
|
382
377
|
});
|
|
383
378
|
}
|
|
384
|
-
|
|
385
|
-
if (status === null)
|
|
386
|
-
return this.#write(statement.target, statement.body?.raw ?? "", ctx, "SEND");
|
|
387
|
-
return _a.#bad(501, "send-status-unsupported", `The WebSocket scheme does not interpret SEND status ${status}.`, {
|
|
388
|
-
requestedStatus: status,
|
|
389
|
-
stage: "dispatch",
|
|
390
|
-
retryable: false,
|
|
391
|
-
});
|
|
379
|
+
return this.#write(statement.target, statement.body?.raw ?? "", ctx, "SEND");
|
|
392
380
|
}
|
|
393
381
|
async #write(target, body, ctx, operation) {
|
|
394
382
|
const address = _a.#address(target);
|
|
395
383
|
if (!(address instanceof NetworkAddress))
|
|
396
384
|
return address;
|
|
397
|
-
const owner = this.#sockets.get(_a.#key(ctx.
|
|
385
|
+
const owner = this.#sockets.get(_a.#key(ctx.workspaceId, address));
|
|
398
386
|
if (owner === undefined) {
|
|
399
387
|
return _a.#bad(409, "no-open-socket", `No WebSocket connection is open for ${address.url}.`, {
|
|
400
388
|
target: address.url,
|
|
@@ -448,7 +436,7 @@ class Ws {
|
|
|
448
436
|
const address = _a.#address(statement.target);
|
|
449
437
|
if (!(address instanceof NetworkAddress))
|
|
450
438
|
return address;
|
|
451
|
-
const owner = this.#sockets.get(_a.#key(ctx.
|
|
439
|
+
const owner = this.#sockets.get(_a.#key(ctx.workspaceId, address));
|
|
452
440
|
if (owner === undefined) {
|
|
453
441
|
return _a.#bad(404, "no-open-socket", `No WebSocket connection is open for ${address.url}.`, {
|
|
454
442
|
target: address.url,
|
|
@@ -473,7 +461,7 @@ class Ws {
|
|
|
473
461
|
return { shape: "passthrough", status: 200 };
|
|
474
462
|
}
|
|
475
463
|
catch (err) {
|
|
476
|
-
if (this.#sockets.get(_a.#key(ctx.
|
|
464
|
+
if (this.#sockets.get(_a.#key(ctx.workspaceId, address)) === owner) {
|
|
477
465
|
owner.killRequested = false;
|
|
478
466
|
owner.state = priorState;
|
|
479
467
|
}
|
|
@@ -485,10 +473,9 @@ class Ws {
|
|
|
485
473
|
});
|
|
486
474
|
}
|
|
487
475
|
}
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return `${workerId}:${address.scheme}:${address.pathname}`;
|
|
476
|
+
// {§runtime-resource-binding}: all workspace callers address the same live connection.
|
|
477
|
+
static #key(workspaceId, address) {
|
|
478
|
+
return `${workspaceId}:${address.url}`;
|
|
492
479
|
}
|
|
493
480
|
static #address(target) {
|
|
494
481
|
let address;
|