@volter/tabnode 0.5.11 → 0.5.13
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/dist/assets/{runtime-worker-C5pP4aT7.js → runtime-worker-CQv8H3sj.js} +4 -11
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +139 -5
- package/dist/native-stream-owner.d.ts +11 -0
- package/dist/node-lib/http-bridge.d.ts +11 -1
- package/dist/server-bridge.d.ts +12 -2
- package/package.json +1 -1
- package/src/index.ts +5 -2
- package/src/native-stream-owner.ts +22 -0
- package/src/node-lib/http-bridge.ts +17 -0
- package/src/server-bridge.ts +85 -2
|
@@ -17621,17 +17621,10 @@ var implementation$1 = function bind2(that) {
|
|
|
17621
17621
|
var implementation = implementation$1;
|
|
17622
17622
|
var functionBind = Function.prototype.bind || implementation;
|
|
17623
17623
|
var functionCall = Function.prototype.call;
|
|
17624
|
-
var functionApply;
|
|
17625
|
-
var hasRequiredFunctionApply;
|
|
17626
|
-
function requireFunctionApply() {
|
|
17627
|
-
if (hasRequiredFunctionApply) return functionApply;
|
|
17628
|
-
hasRequiredFunctionApply = 1;
|
|
17629
|
-
functionApply = Function.prototype.apply;
|
|
17630
|
-
return functionApply;
|
|
17631
|
-
}
|
|
17624
|
+
var functionApply = Function.prototype.apply;
|
|
17632
17625
|
var reflectApply = typeof Reflect !== "undefined" && Reflect && Reflect.apply;
|
|
17633
17626
|
var bind$2 = functionBind;
|
|
17634
|
-
var $apply$1 =
|
|
17627
|
+
var $apply$1 = functionApply;
|
|
17635
17628
|
var $call$2 = functionCall;
|
|
17636
17629
|
var $reflectApply = reflectApply;
|
|
17637
17630
|
var actualApply = $reflectApply || bind$2.call($call$2, $apply$1);
|
|
@@ -17751,7 +17744,7 @@ var hasSymbols = requireHasSymbols()();
|
|
|
17751
17744
|
var getProto = requireGetProto();
|
|
17752
17745
|
var $ObjectGPO = requireObject_getPrototypeOf();
|
|
17753
17746
|
var $ReflectGPO = requireReflect_getPrototypeOf();
|
|
17754
|
-
var $apply =
|
|
17747
|
+
var $apply = functionApply;
|
|
17755
17748
|
var $call = functionCall;
|
|
17756
17749
|
var needsEval = {};
|
|
17757
17750
|
var TypedArray = typeof Uint8Array === "undefined" || !getProto ? undefined$1 : getProto(Uint8Array);
|
|
@@ -18392,7 +18385,7 @@ function requireApplyBind() {
|
|
|
18392
18385
|
if (hasRequiredApplyBind) return applyBind;
|
|
18393
18386
|
hasRequiredApplyBind = 1;
|
|
18394
18387
|
var bind3 = functionBind;
|
|
18395
|
-
var $apply2 =
|
|
18388
|
+
var $apply2 = functionApply;
|
|
18396
18389
|
var actualApply$1 = actualApply;
|
|
18397
18390
|
applyBind = function applyBind2() {
|
|
18398
18391
|
return actualApply$1(bind3, $apply2, arguments);
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { Runtime, RuntimeOptions } from './runtime';
|
|
|
44
44
|
import { PackageManager } from './npm';
|
|
45
45
|
import { ServerBridge } from './server-bridge';
|
|
46
46
|
import { type Socket as NetSocket } from './node-lib/net-module';
|
|
47
|
+
import { type NativeStreamHandleView } from './native-stream-owner';
|
|
47
48
|
import { runPid, processByPid } from './process-tokens';
|
|
48
49
|
export { runPid, processByPid };
|
|
49
50
|
export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope, ownerProcessTable } from './process-tokens';
|
|
@@ -52,7 +53,7 @@ export type { NodeProcessLaunch, NodeProcessHost } from './node-process-host';
|
|
|
52
53
|
export type { ProcessIdentity, ProcessRegistry, ProcessRegistryScope, InitialProcessRegistration } from './process-registry';
|
|
53
54
|
export { NativeStreamScope } from './native-stream-owner';
|
|
54
55
|
export { installNativeStreamTransport, nativeStreamDescriptor } from './native-stream-binding';
|
|
55
|
-
export type { NativeStreamDescriptor, NativeStreamLimits, NativeStreamEvent, NativeStreamOperation, NativeStreamReply, NativeStreamTransport } from './native-stream-owner';
|
|
56
|
+
export type { NativeStreamDescriptor, NativeStreamLimits, NativeStreamEvent, NativeStreamOperation, NativeStreamReply, NativeStreamTransport, NativeStreamHandleView } from './native-stream-owner';
|
|
56
57
|
export interface RunResult {
|
|
57
58
|
stdout: string;
|
|
58
59
|
stderr: string;
|
|
@@ -116,6 +117,8 @@ export declare function createContainer(options?: ContainerOptions): {
|
|
|
116
117
|
processPorts: (token: string) => number[];
|
|
117
118
|
/** The pid of the process listening on a port of this engine, where a guest process is: `/proc`'s socket owner. */
|
|
118
119
|
portPid: (port: number) => number | undefined;
|
|
120
|
+
/** The stream handles the processes of other workers hold through this engine: `/proc/<pid>/fd`. */
|
|
121
|
+
streamHandles: () => NativeStreamHandleView[];
|
|
119
122
|
/** Deliver a signal to the named run as another process's `kill(pid)` does; false when the run is gone. */
|
|
120
123
|
signalProcess: (token: string, signal: string) => boolean;
|
|
121
124
|
stopProcess: (token: string) => boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -35728,7 +35728,7 @@ class WorkerRuntime {
|
|
|
35728
35728
|
this.worker = new Worker(
|
|
35729
35729
|
new URL(
|
|
35730
35730
|
/* @vite-ignore */
|
|
35731
|
-
"/assets/runtime-worker-
|
|
35731
|
+
"/assets/runtime-worker-CQv8H3sj.js",
|
|
35732
35732
|
import.meta.url
|
|
35733
35733
|
),
|
|
35734
35734
|
{ type: "module" }
|
|
@@ -37278,13 +37278,14 @@ function __requestOverLoopback(port, method, url2, headers, body) {
|
|
|
37278
37278
|
if (body && body.length > 0) socket.write(body);
|
|
37279
37279
|
});
|
|
37280
37280
|
}
|
|
37281
|
-
function __streamOverLoopback(port, method, url2, headers, body, onStart, onChunk, onEnd) {
|
|
37281
|
+
function __streamOverLoopback(port, method, url2, headers, body, onStart, onChunk, onEnd, flow) {
|
|
37282
37282
|
if (!__listening(port)) {
|
|
37283
37283
|
onStart(503, "Service Unavailable", { "Content-Type": "text/plain" });
|
|
37284
37284
|
onEnd();
|
|
37285
37285
|
return Promise.resolve();
|
|
37286
37286
|
}
|
|
37287
37287
|
return new Promise((resolve2) => {
|
|
37288
|
+
var _a2;
|
|
37288
37289
|
const collected = emptyCollected();
|
|
37289
37290
|
let started2 = false;
|
|
37290
37291
|
let ended = false;
|
|
@@ -37304,6 +37305,18 @@ function __streamOverLoopback(port, method, url2, headers, body, onStart, onChun
|
|
|
37304
37305
|
(chunk) => onChunk(chunk)
|
|
37305
37306
|
);
|
|
37306
37307
|
const socket = netModule.connect({ port, host: "127.0.0.1" });
|
|
37308
|
+
(_a2 = flow == null ? void 0 : flow.control) == null ? void 0 : _a2.call(flow, () => socket.pause(), () => socket.resume());
|
|
37309
|
+
if (flow == null ? void 0 : flow.signal) {
|
|
37310
|
+
if (flow.signal.aborted) {
|
|
37311
|
+
socket.destroy();
|
|
37312
|
+
finish();
|
|
37313
|
+
return;
|
|
37314
|
+
}
|
|
37315
|
+
flow.signal.addEventListener("abort", () => {
|
|
37316
|
+
socket.destroy();
|
|
37317
|
+
finish();
|
|
37318
|
+
}, { once: true });
|
|
37319
|
+
}
|
|
37307
37320
|
socket.on("data", (chunk) => {
|
|
37308
37321
|
if (ended) return;
|
|
37309
37322
|
const read = parser.execute(chunk);
|
|
@@ -37380,6 +37393,9 @@ function __upgradeOverLoopback(port, method, url2, headers) {
|
|
|
37380
37393
|
});
|
|
37381
37394
|
}
|
|
37382
37395
|
const _encoder = new TextEncoder();
|
|
37396
|
+
const FLOW_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
37397
|
+
const FLOW_MAX_CHUNK_BYTES = 65536;
|
|
37398
|
+
const FLOW_QUEUED_CHUNKS = 4;
|
|
37383
37399
|
function ownedBytes(view) {
|
|
37384
37400
|
const copy2 = new Uint8Array(view.byteLength);
|
|
37385
37401
|
copy2.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
@@ -37649,6 +37665,8 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37649
37665
|
/** The server the service worker answers at the origin's root, the preview's; null for none. */
|
|
37650
37666
|
__publicField(this, "primaryPort", null);
|
|
37651
37667
|
__publicField(this, "ownDocuments");
|
|
37668
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
37669
|
+
__publicField(this, "controlledStreams", /* @__PURE__ */ new Map());
|
|
37652
37670
|
this.options = options;
|
|
37653
37671
|
if (typeof location !== "undefined") {
|
|
37654
37672
|
this.baseUrl = options.baseUrl || `${location.protocol}//${location.host}`;
|
|
@@ -37819,6 +37837,7 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37819
37837
|
await controllerReady;
|
|
37820
37838
|
const reinit = () => {
|
|
37821
37839
|
if (navigator.serviceWorker.controller) {
|
|
37840
|
+
for (const stream of [...this.controlledStreams.values()]) stream.cancel();
|
|
37822
37841
|
this.messageChannel = new MessageChannel();
|
|
37823
37842
|
this.messageChannel.port1.onmessage = this.handleServiceWorkerMessage.bind(this);
|
|
37824
37843
|
navigator.serviceWorker.controller.postMessage(
|
|
@@ -37850,6 +37869,17 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37850
37869
|
var _a2, _b2, _c2;
|
|
37851
37870
|
const { type: type2, id, data: data2 } = event.data;
|
|
37852
37871
|
_ServerBridge.DEBUG && console.log("[ServerBridge] SW message:", type2, id, data2 == null ? void 0 : data2.url);
|
|
37872
|
+
if (type2 === "stream-pull" || type2 === "stream-cancel") {
|
|
37873
|
+
const stream = this.controlledStreams.get(id);
|
|
37874
|
+
if (type2 === "stream-pull") stream == null ? void 0 : stream.pull();
|
|
37875
|
+
else stream == null ? void 0 : stream.cancel();
|
|
37876
|
+
return;
|
|
37877
|
+
}
|
|
37878
|
+
if (type2 === "request" && (data2 == null ? void 0 : data2.flowControl) === 1) {
|
|
37879
|
+
const { port, method, url: url2, headers, body } = data2;
|
|
37880
|
+
await this.controlledStream(id, port, method, url2, headers, body);
|
|
37881
|
+
return;
|
|
37882
|
+
}
|
|
37853
37883
|
if (type2 === "request") {
|
|
37854
37884
|
const { port, method, url: url2, headers, body, streaming } = data2;
|
|
37855
37885
|
_ServerBridge.DEBUG && console.log("[ServerBridge] Handling request:", port, method, url2, "streaming:", streaming);
|
|
@@ -37898,7 +37928,7 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37898
37928
|
* it; a server the host registered answers through its own streaming
|
|
37899
37929
|
* method where it has one, else its buffered answer is delivered whole.
|
|
37900
37930
|
*/
|
|
37901
|
-
async handleStreamingRequest(port, method, url2, headers, body, callbacks) {
|
|
37931
|
+
async handleStreamingRequest(port, method, url2, headers, body, callbacks, flow) {
|
|
37902
37932
|
const virtualServer = this.servers.get(port);
|
|
37903
37933
|
if (!virtualServer) return false;
|
|
37904
37934
|
const bodyBuffer = body ? Buffer$9.from(new Uint8Array(body)) : void 0;
|
|
@@ -37913,7 +37943,8 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37913
37943
|
bodyBuffer,
|
|
37914
37944
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
37915
37945
|
(chunk) => callbacks.chunk(ownedBytes(chunk)),
|
|
37916
|
-
() => callbacks.end()
|
|
37946
|
+
() => callbacks.end(),
|
|
37947
|
+
flow
|
|
37917
37948
|
);
|
|
37918
37949
|
return true;
|
|
37919
37950
|
}
|
|
@@ -38043,7 +38074,91 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
38043
38074
|
*/
|
|
38044
38075
|
notifyServiceWorker(type2, data2) {
|
|
38045
38076
|
if (this.serviceWorkerReady && this.messageChannel) {
|
|
38046
|
-
|
|
38077
|
+
const flow = type2 === "server-registered" ? { flowControl: 1, maxRequestBytes: FLOW_MAX_REQUEST_BYTES, maxChunkBytes: FLOW_MAX_CHUNK_BYTES } : {};
|
|
38078
|
+
this.messageChannel.port1.postMessage({ type: type2, data: { ...data2, ...flow } });
|
|
38079
|
+
}
|
|
38080
|
+
}
|
|
38081
|
+
/**
|
|
38082
|
+
* A request the worker sent under flow control: the response starts with
|
|
38083
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
38084
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
38085
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
38086
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
38087
|
+
*/
|
|
38088
|
+
async controlledStream(id, port, method, url2, headers, body) {
|
|
38089
|
+
const channel = this.messageChannel;
|
|
38090
|
+
const post = (type2, data2) => {
|
|
38091
|
+
channel == null ? void 0 : channel.port1.postMessage({ type: type2, id, ...data2 === void 0 ? {} : { data: data2 } });
|
|
38092
|
+
};
|
|
38093
|
+
const queue = [];
|
|
38094
|
+
const abort = new AbortController();
|
|
38095
|
+
let credits = 0;
|
|
38096
|
+
let upstreamEnded = false;
|
|
38097
|
+
let finished = false;
|
|
38098
|
+
let paused = false;
|
|
38099
|
+
let pause = () => void 0;
|
|
38100
|
+
let resume = () => void 0;
|
|
38101
|
+
const finish = () => {
|
|
38102
|
+
finished = true;
|
|
38103
|
+
this.controlledStreams.delete(id);
|
|
38104
|
+
};
|
|
38105
|
+
const flush = () => {
|
|
38106
|
+
if (finished) return;
|
|
38107
|
+
while (credits > 0 && queue.length > 0) {
|
|
38108
|
+
credits -= 1;
|
|
38109
|
+
post("stream-chunk", { chunkBase64: uint8ToBase64(queue.shift()) });
|
|
38110
|
+
}
|
|
38111
|
+
if (upstreamEnded && queue.length === 0) {
|
|
38112
|
+
post("stream-end");
|
|
38113
|
+
finish();
|
|
38114
|
+
return;
|
|
38115
|
+
}
|
|
38116
|
+
if (queue.length >= FLOW_QUEUED_CHUNKS && !paused) {
|
|
38117
|
+
paused = true;
|
|
38118
|
+
pause();
|
|
38119
|
+
} else if (queue.length < FLOW_QUEUED_CHUNKS && paused) {
|
|
38120
|
+
paused = false;
|
|
38121
|
+
resume();
|
|
38122
|
+
}
|
|
38123
|
+
};
|
|
38124
|
+
this.controlledStreams.set(id, {
|
|
38125
|
+
pull: () => {
|
|
38126
|
+
credits += 1;
|
|
38127
|
+
flush();
|
|
38128
|
+
},
|
|
38129
|
+
cancel: () => {
|
|
38130
|
+
if (finished) return;
|
|
38131
|
+
finish();
|
|
38132
|
+
queue.length = 0;
|
|
38133
|
+
abort.abort();
|
|
38134
|
+
}
|
|
38135
|
+
});
|
|
38136
|
+
try {
|
|
38137
|
+
const answered = await this.handleStreamingRequest(port, method, url2, headers, body, {
|
|
38138
|
+
start: (statusCode, statusMessage, respHeaders) => post("stream-start", { statusCode, statusMessage, headers: respHeaders }),
|
|
38139
|
+
chunk: (chunk) => {
|
|
38140
|
+
if (finished) return;
|
|
38141
|
+
for (let at2 = 0; at2 < chunk.byteLength; at2 += FLOW_MAX_CHUNK_BYTES) queue.push(chunk.slice(at2, at2 + FLOW_MAX_CHUNK_BYTES));
|
|
38142
|
+
flush();
|
|
38143
|
+
},
|
|
38144
|
+
end: () => {
|
|
38145
|
+
upstreamEnded = true;
|
|
38146
|
+
flush();
|
|
38147
|
+
}
|
|
38148
|
+
}, { signal: abort.signal, control: (p, r) => {
|
|
38149
|
+
pause = p;
|
|
38150
|
+
resume = r;
|
|
38151
|
+
} });
|
|
38152
|
+
if (!answered) {
|
|
38153
|
+
post("stream-start", { statusCode: 503, statusMessage: "Service Unavailable", headers: {} });
|
|
38154
|
+
post("stream-end");
|
|
38155
|
+
finish();
|
|
38156
|
+
}
|
|
38157
|
+
} catch (error) {
|
|
38158
|
+
if (!finished) {
|
|
38159
|
+
post("stream-error", { message: error instanceof Error ? error.message : String(error) });
|
|
38160
|
+
finish();
|
|
38161
|
+
}
|
|
38047
38162
|
}
|
|
38048
38163
|
}
|
|
38049
38164
|
/**
|
|
@@ -38098,6 +38213,7 @@ function resetServerBridge() {
|
|
|
38098
38213
|
globalBridge = null;
|
|
38099
38214
|
}
|
|
38100
38215
|
const scopes = /* @__PURE__ */ new WeakMap();
|
|
38216
|
+
const liveScopes = /* @__PURE__ */ new Set();
|
|
38101
38217
|
class NativeStreamScope {
|
|
38102
38218
|
/**
|
|
38103
38219
|
* `ownerPid` names the process whose worker this scope serves: a listener
|
|
@@ -38116,6 +38232,19 @@ class NativeStreamScope {
|
|
|
38116
38232
|
this.ownerPid = ownerPid;
|
|
38117
38233
|
if ([limits.maxHandles, limits.maxReadChunkBytes, limits.maxQueuedBytes, limits.maxQueuedHandles, limits.maxPendingWriteBytes].some((value) => !Number.isSafeInteger(value) || value < 1) || limits.maxReadChunkBytes > 2147483647 || limits.maxReadChunkBytes > limits.maxQueuedBytes || limits.maxReadChunkBytes > limits.maxPendingWriteBytes) throw new Error("Invalid native stream resource limits.");
|
|
38118
38234
|
this.limits = Object.freeze({ ...limits });
|
|
38235
|
+
liveScopes.add(this);
|
|
38236
|
+
}
|
|
38237
|
+
/** This scope's open handles, with the process it serves. */
|
|
38238
|
+
handles() {
|
|
38239
|
+
var _a2;
|
|
38240
|
+
if (this.disposed) return [];
|
|
38241
|
+
const pid = (_a2 = this.ownerPid) == null ? void 0 : _a2.call(this);
|
|
38242
|
+
return [...this.entries.values()].map(({ handle, descriptor }) => ({
|
|
38243
|
+
...pid !== void 0 ? { pid } : {},
|
|
38244
|
+
id: descriptor.id,
|
|
38245
|
+
kind: descriptor.kind,
|
|
38246
|
+
listening: !!handle.listening
|
|
38247
|
+
}));
|
|
38119
38248
|
}
|
|
38120
38249
|
entry(id) {
|
|
38121
38250
|
return this.disposed ? void 0 : this.entries.get(id);
|
|
@@ -38387,6 +38516,7 @@ class NativeStreamScope {
|
|
|
38387
38516
|
dispose() {
|
|
38388
38517
|
if (this.disposed) return;
|
|
38389
38518
|
this.disposed = true;
|
|
38519
|
+
liveScopes.delete(this);
|
|
38390
38520
|
const failures = [];
|
|
38391
38521
|
for (const id of [...this.entries.keys()]) {
|
|
38392
38522
|
try {
|
|
@@ -38402,6 +38532,9 @@ function nativeStreamOwnerPid(handle) {
|
|
|
38402
38532
|
var _a2, _b2;
|
|
38403
38533
|
return (_b2 = (_a2 = scopes.get(handle)) == null ? void 0 : _a2.ownerPid) == null ? void 0 : _b2.call(_a2);
|
|
38404
38534
|
}
|
|
38535
|
+
function nativeStreamHandles() {
|
|
38536
|
+
return [...liveScopes].flatMap((scope) => scope.handles());
|
|
38537
|
+
}
|
|
38405
38538
|
let __nextRunName = 1;
|
|
38406
38539
|
function createContainer(options) {
|
|
38407
38540
|
const vfs2 = options && options.vfs || new VirtualFS();
|
|
@@ -38464,6 +38597,7 @@ function createContainer(options) {
|
|
|
38464
38597
|
if (!listener) return void 0;
|
|
38465
38598
|
return ((_a2 = runPid(ownerOf(listener))) == null ? void 0 : _a2.pid) ?? nativeStreamOwnerPid(listener);
|
|
38466
38599
|
},
|
|
38600
|
+
streamHandles: () => nativeStreamHandles(),
|
|
38467
38601
|
/**
|
|
38468
38602
|
* Deliver a signal to the named run as another process's `kill(pid)` does:
|
|
38469
38603
|
* the guest's listeners for it run, else its default action ends the run.
|
|
@@ -88,6 +88,13 @@ export interface NativeStreamTransport {
|
|
|
88
88
|
write(id: number, bytes: Uint8Array, handle?: number): Promise<number>;
|
|
89
89
|
onEvent(listener: (event: NativeStreamEvent) => void): () => void;
|
|
90
90
|
}
|
|
91
|
+
/** One open stream handle, as a process's descriptor table shows it. */
|
|
92
|
+
export interface NativeStreamHandleView {
|
|
93
|
+
pid?: number;
|
|
94
|
+
id: number;
|
|
95
|
+
kind: 'tcp' | 'pipe';
|
|
96
|
+
listening: boolean;
|
|
97
|
+
}
|
|
91
98
|
/**
|
|
92
99
|
* One capability namespace per worker. Only the trusted host can inherit a
|
|
93
100
|
* descriptor from another scope; wire operations can name this scope's IDs only.
|
|
@@ -108,6 +115,8 @@ export declare class NativeStreamScope {
|
|
|
108
115
|
* it opens is that process's socket, as the container's /proc shows it.
|
|
109
116
|
*/
|
|
110
117
|
constructor(limits: NativeStreamLimits, emit: (event: NativeStreamEvent) => void, ownerPid?: (() => number | undefined) | undefined);
|
|
118
|
+
/** This scope's open handles, with the process it serves. */
|
|
119
|
+
handles(): NativeStreamHandleView[];
|
|
111
120
|
private entry;
|
|
112
121
|
private publish;
|
|
113
122
|
private claim;
|
|
@@ -126,3 +135,5 @@ export declare class NativeStreamScope {
|
|
|
126
135
|
}
|
|
127
136
|
/** The process a handle opened for another worker's process belongs to, where a scope holds it. */
|
|
128
137
|
export declare function nativeStreamOwnerPid(handle: object): number | undefined;
|
|
138
|
+
/** Every open native stream handle in this worker, by the process each scope serves. */
|
|
139
|
+
export declare function nativeStreamHandles(): NativeStreamHandleView[];
|
|
@@ -22,7 +22,17 @@ export declare function __requestOverLoopback(port: number, method: string, url:
|
|
|
22
22
|
* the server writes, then the end. This is what the service worker streams
|
|
23
23
|
* to the page.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* How a caller paces a streamed answer: `signal` ends the connection (the
|
|
27
|
+
* reader went away), and `control` is handed the connection's pause and
|
|
28
|
+
* resume, so a reader that holds no credit holds the server back instead of
|
|
29
|
+
* a queue growing without bound.
|
|
30
|
+
*/
|
|
31
|
+
export interface LoopbackStreamFlow {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
control?(pause: () => void, resume: () => void): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function __streamOverLoopback(port: number, method: string, url: string, headers: Record<string, string>, body: Buffer | undefined, onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void, onChunk: (chunk: Uint8Array) => void, onEnd: () => void, flow?: LoopbackStreamFlow): Promise<void>;
|
|
26
36
|
/** What an upgrade answered with: the 101 and the socket the page now owns. */
|
|
27
37
|
export interface UpgradeResult {
|
|
28
38
|
statusCode: number;
|
package/dist/server-bridge.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Server Bridge
|
|
3
3
|
* Connects Service Worker requests to virtual HTTP servers
|
|
4
4
|
*/
|
|
5
|
-
import { type ResponseData } from './node-lib/http-bridge';
|
|
5
|
+
import { type ResponseData, type LoopbackStreamFlow } from './node-lib/http-bridge';
|
|
6
6
|
import { EventEmitter } from './node-lib/events-module';
|
|
7
7
|
/**
|
|
8
8
|
* Interface for virtual servers that can be registered with the bridge
|
|
@@ -125,12 +125,22 @@ export declare class ServerBridge extends EventEmitter {
|
|
|
125
125
|
start(statusCode: number, statusMessage: string, headers: Record<string, string>): void;
|
|
126
126
|
chunk(chunk: Uint8Array): void;
|
|
127
127
|
end(): void;
|
|
128
|
-
}): Promise<boolean>;
|
|
128
|
+
}, flow?: LoopbackStreamFlow): Promise<boolean>;
|
|
129
129
|
private streamToServiceWorker;
|
|
130
130
|
/**
|
|
131
131
|
* Send message to Service Worker
|
|
132
132
|
*/
|
|
133
133
|
private notifyServiceWorker;
|
|
134
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
135
|
+
private readonly controlledStreams;
|
|
136
|
+
/**
|
|
137
|
+
* A request the worker sent under flow control: the response starts with
|
|
138
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
139
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
140
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
141
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
142
|
+
*/
|
|
143
|
+
private controlledStream;
|
|
134
144
|
/**
|
|
135
145
|
* Create a mock request handler for testing without Service Worker
|
|
136
146
|
*/
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ import { runCommand, registerRunStreams, releaseRunStreams, sendStdin } from './
|
|
|
56
56
|
import { Server as NetServer, __releaseOwnedHandles, type Socket as NetSocket } from './node-lib/net-module';
|
|
57
57
|
import { __adoptHandle, ownerOf, type OwnedHandle } from './node-lib/binding/handles';
|
|
58
58
|
import { listenerOnPort } from './node-lib/binding/tcp_wrap';
|
|
59
|
-
import { nativeStreamOwnerPid } from './native-stream-owner';
|
|
59
|
+
import { nativeStreamHandles, nativeStreamOwnerPid, type NativeStreamHandleView } from './native-stream-owner';
|
|
60
60
|
|
|
61
61
|
import { __currentProcessToken, __runFor, __signalOwnedProcess, __takeTermination, __stopOwnedProcess, runPid, processByPid } from './process-tokens';
|
|
62
62
|
export { runPid, processByPid };
|
|
@@ -66,7 +66,7 @@ export type { NodeProcessLaunch, NodeProcessHost } from './node-process-host';
|
|
|
66
66
|
export type { ProcessIdentity, ProcessRegistry, ProcessRegistryScope, InitialProcessRegistration } from './process-registry';
|
|
67
67
|
export { NativeStreamScope } from './native-stream-owner';
|
|
68
68
|
export { installNativeStreamTransport, nativeStreamDescriptor } from './native-stream-binding';
|
|
69
|
-
export type { NativeStreamDescriptor, NativeStreamLimits, NativeStreamEvent, NativeStreamOperation, NativeStreamReply, NativeStreamTransport } from './native-stream-owner';
|
|
69
|
+
export type { NativeStreamDescriptor, NativeStreamLimits, NativeStreamEvent, NativeStreamOperation, NativeStreamReply, NativeStreamTransport, NativeStreamHandleView } from './native-stream-owner';
|
|
70
70
|
|
|
71
71
|
export interface RunResult {
|
|
72
72
|
stdout: string;
|
|
@@ -129,6 +129,8 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
129
129
|
processPorts: (token: string) => number[];
|
|
130
130
|
/** The pid of the process listening on a port of this engine, where a guest process is: `/proc`'s socket owner. */
|
|
131
131
|
portPid: (port: number) => number | undefined;
|
|
132
|
+
/** The stream handles the processes of other workers hold through this engine: `/proc/<pid>/fd`. */
|
|
133
|
+
streamHandles: () => NativeStreamHandleView[];
|
|
132
134
|
/** Deliver a signal to the named run as another process's `kill(pid)` does; false when the run is gone. */
|
|
133
135
|
signalProcess: (token: string, signal: string) => boolean;
|
|
134
136
|
stopProcess: (token: string) => boolean;
|
|
@@ -224,6 +226,7 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
224
226
|
// a guest's own listener, or one this engine holds for another worker's process
|
|
225
227
|
return runPid(ownerOf(listener as unknown as OwnedHandle))?.pid ?? nativeStreamOwnerPid(listener);
|
|
226
228
|
},
|
|
229
|
+
streamHandles: (): NativeStreamHandleView[] => nativeStreamHandles(),
|
|
227
230
|
/**
|
|
228
231
|
* Deliver a signal to the named run as another process's `kill(pid)` does:
|
|
229
232
|
* the guest's listeners for it run, else its default action ends the run.
|
|
@@ -41,6 +41,12 @@ export interface NativeStreamTransport {
|
|
|
41
41
|
type NativeHandle = TCP | Pipe;
|
|
42
42
|
interface Entry { handle: NativeHandle; descriptor: NativeStreamDescriptor; buffer?: Uint8Array }
|
|
43
43
|
const scopes = new WeakMap<LibuvStreamWrap, NativeStreamScope>();
|
|
44
|
+
// Every scope not yet disposed: what the container's /proc lists as each
|
|
45
|
+
// process's descriptors.
|
|
46
|
+
const liveScopes = new Set<NativeStreamScope>();
|
|
47
|
+
|
|
48
|
+
/** One open stream handle, as a process's descriptor table shows it. */
|
|
49
|
+
export interface NativeStreamHandleView { pid?: number; id: number; kind: 'tcp' | 'pipe'; listening: boolean }
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
52
|
* One capability namespace per worker. Only the trusted host can inherit a
|
|
@@ -66,6 +72,16 @@ export class NativeStreamScope {
|
|
|
66
72
|
|| limits.maxReadChunkBytes > limits.maxQueuedBytes
|
|
67
73
|
|| limits.maxReadChunkBytes > limits.maxPendingWriteBytes) throw new Error('Invalid native stream resource limits.');
|
|
68
74
|
this.limits = Object.freeze({ ...limits });
|
|
75
|
+
liveScopes.add(this);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This scope's open handles, with the process it serves. */
|
|
79
|
+
handles(): NativeStreamHandleView[] {
|
|
80
|
+
if (this.disposed) return [];
|
|
81
|
+
const pid = this.ownerPid?.();
|
|
82
|
+
return [...this.entries.values()].map(({ handle, descriptor }) => ({
|
|
83
|
+
...(pid !== undefined ? { pid } : {}), id: descriptor.id, kind: descriptor.kind, listening: !!handle.listening,
|
|
84
|
+
}));
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
private entry(id: number): Entry | undefined {
|
|
@@ -318,6 +334,7 @@ export class NativeStreamScope {
|
|
|
318
334
|
dispose(): void {
|
|
319
335
|
if (this.disposed) return;
|
|
320
336
|
this.disposed = true;
|
|
337
|
+
liveScopes.delete(this);
|
|
321
338
|
const failures: unknown[] = [];
|
|
322
339
|
for (const id of [...this.entries.keys()]) {
|
|
323
340
|
try { this.close(id); } catch (cause) { failures.push(cause); }
|
|
@@ -330,3 +347,8 @@ export class NativeStreamScope {
|
|
|
330
347
|
export function nativeStreamOwnerPid(handle: object): number | undefined {
|
|
331
348
|
return scopes.get(handle as NativeHandle)?.ownerPid?.();
|
|
332
349
|
}
|
|
350
|
+
|
|
351
|
+
/** Every open native stream handle in this worker, by the process each scope serves. */
|
|
352
|
+
export function nativeStreamHandles(): NativeStreamHandleView[] {
|
|
353
|
+
return [...liveScopes].flatMap(scope => scope.handles());
|
|
354
|
+
}
|
|
@@ -195,11 +195,23 @@ export function __requestOverLoopback(
|
|
|
195
195
|
* the server writes, then the end. This is what the service worker streams
|
|
196
196
|
* to the page.
|
|
197
197
|
*/
|
|
198
|
+
/**
|
|
199
|
+
* How a caller paces a streamed answer: `signal` ends the connection (the
|
|
200
|
+
* reader went away), and `control` is handed the connection's pause and
|
|
201
|
+
* resume, so a reader that holds no credit holds the server back instead of
|
|
202
|
+
* a queue growing without bound.
|
|
203
|
+
*/
|
|
204
|
+
export interface LoopbackStreamFlow {
|
|
205
|
+
signal?: AbortSignal;
|
|
206
|
+
control?(pause: () => void, resume: () => void): void;
|
|
207
|
+
}
|
|
208
|
+
|
|
198
209
|
export function __streamOverLoopback(
|
|
199
210
|
port: number, method: string, url: string, headers: Record<string, string>, body: Buffer | undefined,
|
|
200
211
|
onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void,
|
|
201
212
|
onChunk: (chunk: Uint8Array) => void,
|
|
202
213
|
onEnd: () => void,
|
|
214
|
+
flow?: LoopbackStreamFlow,
|
|
203
215
|
): Promise<void> {
|
|
204
216
|
if (!__listening(port)) {
|
|
205
217
|
onStart(503, 'Service Unavailable', { 'Content-Type': 'text/plain' });
|
|
@@ -217,6 +229,11 @@ export function __streamOverLoopback(
|
|
|
217
229
|
(chunk) => onChunk(chunk),
|
|
218
230
|
);
|
|
219
231
|
const socket = netModule.connect({ port, host: '127.0.0.1' }) as unknown as Socket;
|
|
232
|
+
flow?.control?.(() => socket.pause(), () => socket.resume());
|
|
233
|
+
if (flow?.signal) {
|
|
234
|
+
if (flow.signal.aborted) { socket.destroy(); finish(); return; }
|
|
235
|
+
flow.signal.addEventListener('abort', () => { socket.destroy(); finish(); }, { once: true });
|
|
236
|
+
}
|
|
220
237
|
socket.on('data', (chunk: Uint8Array) => {
|
|
221
238
|
if (ended) return;
|
|
222
239
|
const read = parser.execute(chunk);
|
package/src/server-bridge.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
__requestOverLoopback, __streamOverLoopback, __upgradeOverLoopback, __listening,
|
|
8
|
-
type ResponseData,
|
|
8
|
+
type ResponseData, type LoopbackStreamFlow,
|
|
9
9
|
} from './node-lib/http-bridge';
|
|
10
10
|
import { setPortWatchers } from './node-lib/net-module';
|
|
11
11
|
import { EventEmitter } from './node-lib/events-module';
|
|
@@ -15,6 +15,12 @@ import { Buffer } from './node-lib/buffer-module';
|
|
|
15
15
|
import { uint8ToBase64 } from './utils/binary-encoding';
|
|
16
16
|
|
|
17
17
|
const _encoder = new TextEncoder();
|
|
18
|
+
// The worker's flow-controlled stream: a request body up to this size, and a
|
|
19
|
+
// response in chunks of exactly the worker's own maximum, one per pull.
|
|
20
|
+
const FLOW_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const FLOW_MAX_CHUNK_BYTES = 65536;
|
|
22
|
+
// Chunks that may wait for the reader's credit before the server is paused.
|
|
23
|
+
const FLOW_QUEUED_CHUNKS = 4;
|
|
18
24
|
|
|
19
25
|
/** The bytes this view names, in a buffer of their own. A pooled Buffer is a window on 8 KB. */
|
|
20
26
|
function ownedBytes(view: Uint8Array): Uint8Array {
|
|
@@ -649,6 +655,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
649
655
|
// to all clients when a request arrives but mainPort is null.
|
|
650
656
|
const reinit = () => {
|
|
651
657
|
if (navigator.serviceWorker.controller) {
|
|
658
|
+
for (const stream of [...this.controlledStreams.values()]) stream.cancel();
|
|
652
659
|
this.messageChannel = new MessageChannel();
|
|
653
660
|
this.messageChannel.port1.onmessage = this.handleServiceWorkerMessage.bind(this);
|
|
654
661
|
navigator.serviceWorker.controller.postMessage(
|
|
@@ -684,6 +691,16 @@ export class ServerBridge extends EventEmitter {
|
|
|
684
691
|
|
|
685
692
|
ServerBridge.DEBUG && console.log('[ServerBridge] SW message:', type, id, data?.url);
|
|
686
693
|
|
|
694
|
+
if (type === 'stream-pull' || type === 'stream-cancel') {
|
|
695
|
+
const stream = this.controlledStreams.get(id);
|
|
696
|
+
if (type === 'stream-pull') stream?.pull(); else stream?.cancel();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (type === 'request' && data?.flowControl === 1) {
|
|
700
|
+
const { port, method, url, headers, body } = data;
|
|
701
|
+
await this.controlledStream(id, port, method, url, headers, body);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
687
704
|
if (type === 'request') {
|
|
688
705
|
const { port, method, url, headers, body, streaming } = data;
|
|
689
706
|
|
|
@@ -752,6 +769,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
752
769
|
chunk(chunk: Uint8Array): void;
|
|
753
770
|
end(): void;
|
|
754
771
|
},
|
|
772
|
+
flow?: LoopbackStreamFlow,
|
|
755
773
|
): Promise<boolean> {
|
|
756
774
|
const virtualServer = this.servers.get(port);
|
|
757
775
|
if (!virtualServer) return false;
|
|
@@ -764,6 +782,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
764
782
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
765
783
|
(chunk) => callbacks.chunk(ownedBytes(chunk)),
|
|
766
784
|
() => callbacks.end(),
|
|
785
|
+
flow,
|
|
767
786
|
);
|
|
768
787
|
return true;
|
|
769
788
|
}
|
|
@@ -909,7 +928,71 @@ export class ServerBridge extends EventEmitter {
|
|
|
909
928
|
*/
|
|
910
929
|
private notifyServiceWorker(type: string, data: unknown): void {
|
|
911
930
|
if (this.serviceWorkerReady && this.messageChannel) {
|
|
912
|
-
this
|
|
931
|
+
// Every port this bridge answers speaks the worker's flow-controlled
|
|
932
|
+
// stream: a response is paced by the reader's pulls, one chunk a pull,
|
|
933
|
+
// and a reader that goes away ends the request (virtual HTTP flow
|
|
934
|
+
// control, the worker's side of which is in public/__sw__.js).
|
|
935
|
+
const flow = type === 'server-registered'
|
|
936
|
+
? { flowControl: 1, maxRequestBytes: FLOW_MAX_REQUEST_BYTES, maxChunkBytes: FLOW_MAX_CHUNK_BYTES } : {};
|
|
937
|
+
this.messageChannel.port1.postMessage({ type, data: { ...(data as object), ...flow } });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
942
|
+
private readonly controlledStreams = new Map<number, { pull(): void; cancel(): void }>();
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* A request the worker sent under flow control: the response starts with
|
|
946
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
947
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
948
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
949
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
950
|
+
*/
|
|
951
|
+
private async controlledStream(id: number, port: number, method: string, url: string, headers: Record<string, string>, body?: ArrayBuffer): Promise<void> {
|
|
952
|
+
// the stream belongs to the channel it arrived on; a new channel's worker failed it already
|
|
953
|
+
const channel = this.messageChannel;
|
|
954
|
+
const post = (type: string, data?: unknown): void => { channel?.port1.postMessage({ type, id, ...(data === undefined ? {} : { data }) }); };
|
|
955
|
+
const queue: Uint8Array[] = [];
|
|
956
|
+
const abort = new AbortController();
|
|
957
|
+
let credits = 0;
|
|
958
|
+
let upstreamEnded = false;
|
|
959
|
+
let finished = false;
|
|
960
|
+
let paused = false;
|
|
961
|
+
let pause = (): void => undefined;
|
|
962
|
+
let resume = (): void => undefined;
|
|
963
|
+
const finish = (): void => { finished = true; this.controlledStreams.delete(id); };
|
|
964
|
+
const flush = (): void => {
|
|
965
|
+
if (finished) return;
|
|
966
|
+
while (credits > 0 && queue.length > 0) {
|
|
967
|
+
credits -= 1;
|
|
968
|
+
post('stream-chunk', { chunkBase64: uint8ToBase64(queue.shift()!) });
|
|
969
|
+
}
|
|
970
|
+
if (upstreamEnded && queue.length === 0) { post('stream-end'); finish(); return; }
|
|
971
|
+
// a few chunks may wait for credit; more than that, and the server waits
|
|
972
|
+
if (queue.length >= FLOW_QUEUED_CHUNKS && !paused) { paused = true; pause(); }
|
|
973
|
+
else if (queue.length < FLOW_QUEUED_CHUNKS && paused) { paused = false; resume(); }
|
|
974
|
+
};
|
|
975
|
+
this.controlledStreams.set(id, {
|
|
976
|
+
pull: () => { credits += 1; flush(); },
|
|
977
|
+
cancel: () => { if (finished) return; finish(); queue.length = 0; abort.abort(); },
|
|
978
|
+
});
|
|
979
|
+
try {
|
|
980
|
+
const answered = await this.handleStreamingRequest(port, method, url, headers, body, {
|
|
981
|
+
start: (statusCode, statusMessage, respHeaders) => post('stream-start', { statusCode, statusMessage, headers: respHeaders }),
|
|
982
|
+
chunk: (chunk) => {
|
|
983
|
+
if (finished) return;
|
|
984
|
+
for (let at = 0; at < chunk.byteLength; at += FLOW_MAX_CHUNK_BYTES) queue.push(chunk.slice(at, at + FLOW_MAX_CHUNK_BYTES));
|
|
985
|
+
flush();
|
|
986
|
+
},
|
|
987
|
+
end: () => { upstreamEnded = true; flush(); },
|
|
988
|
+
}, { signal: abort.signal, control: (p, r) => { pause = p; resume = r; } });
|
|
989
|
+
if (!answered) {
|
|
990
|
+
post('stream-start', { statusCode: 503, statusMessage: 'Service Unavailable', headers: {} });
|
|
991
|
+
post('stream-end');
|
|
992
|
+
finish();
|
|
993
|
+
}
|
|
994
|
+
} catch (error) {
|
|
995
|
+
if (!finished) { post('stream-error', { message: error instanceof Error ? error.message : String(error) }); finish(); }
|
|
913
996
|
}
|
|
914
997
|
}
|
|
915
998
|
|