sandboxedjs 0.2.12 → 0.2.14
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/README.md +5 -0
- package/dist/index.cjs +87 -3
- package/dist/index.js +87 -3
- package/dist/python/python.data +140 -141
- package/dist/python/python.js +1 -1
- package/dist/python/python.wasm +0 -0
- package/dist/python/runtime.json +3 -3
- package/dist/python-worker.js +9 -0
- package/docs/browser-runtime-architecture.md +37 -41
- package/docs/python/release-gates.md +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -256,6 +256,11 @@ from source where a wheel does not exist; browsers have no compiler, so they ask
|
|
|
256
256
|
`npx sandboxedjs-build-wheels 4180`. See [docs/python/build-on-miss.md](docs/python/build-on-miss.md)
|
|
257
257
|
and [compatibility](docs/python/compatibility.md).
|
|
258
258
|
|
|
259
|
+
**Python thread offloads work in the bundled runtime:** `asyncio.to_thread`,
|
|
260
|
+
`run_in_executor`, and synchronous FastAPI routes use real worker threads.
|
|
261
|
+
For browser deployments, update `python-worker.js` and the entire `python/`
|
|
262
|
+
directory together. See [threading architecture and limits](docs/browser-runtime-architecture.md#python-thread-offloads).
|
|
263
|
+
|
|
259
264
|
**What does not work in a browser:** `copyIn()` / `copyOut()` and `expose()` (they need a real
|
|
260
265
|
filesystem and a real socket), and the CLI. They fail only if you call them.
|
|
261
266
|
|
package/dist/index.cjs
CHANGED
|
@@ -20027,7 +20027,7 @@ var CAPABILITIES = ["files", "descriptors", "pipes", "readiness", "time", "ident
|
|
|
20027
20027
|
var OP_NAMES = Object.fromEntries(
|
|
20028
20028
|
Object.entries(Op).map(([name, code]) => [code, name])
|
|
20029
20029
|
);
|
|
20030
|
-
Object.fromEntries(
|
|
20030
|
+
var ERRNO_NAMES = Object.fromEntries(
|
|
20031
20031
|
Object.entries(Errno).map(([name, code]) => [code, name])
|
|
20032
20032
|
);
|
|
20033
20033
|
|
|
@@ -21103,7 +21103,7 @@ var wheels_default = {
|
|
|
21103
21103
|
|
|
21104
21104
|
// package.json
|
|
21105
21105
|
var package_default = {
|
|
21106
|
-
version: "0.2.
|
|
21106
|
+
version: "0.2.14"};
|
|
21107
21107
|
|
|
21108
21108
|
// src/python/extension-abi.ts
|
|
21109
21109
|
var EXTENSION_ABI = {
|
|
@@ -21863,6 +21863,59 @@ var FileService = class {
|
|
|
21863
21863
|
init_signals();
|
|
21864
21864
|
init_binary();
|
|
21865
21865
|
|
|
21866
|
+
// src/python/thread-transport.ts
|
|
21867
|
+
init_binary();
|
|
21868
|
+
async function createThreadTransport(generation, handle) {
|
|
21869
|
+
const crypto2 = globalThis.crypto ?? (await nodeBuiltin("crypto")).webcrypto;
|
|
21870
|
+
const name = `sbx-python-${crypto2.randomUUID()}`;
|
|
21871
|
+
const channel = new BroadcastChannel(name);
|
|
21872
|
+
const pending = /* @__PURE__ */ new Set();
|
|
21873
|
+
let closed = false;
|
|
21874
|
+
let memory;
|
|
21875
|
+
let memoryReady;
|
|
21876
|
+
const ready = new Promise((resolve3) => {
|
|
21877
|
+
memoryReady = resolve3;
|
|
21878
|
+
});
|
|
21879
|
+
channel.onmessage = async ({ data }) => {
|
|
21880
|
+
if (closed || !(data?.request instanceof Uint8Array) || data.request.length < 16 || !Number.isSafeInteger(data.offset) || data.offset < 0 || data.offset % 4 || !Number.isSafeInteger(data.capacity) || data.capacity < 84) return;
|
|
21881
|
+
await ready;
|
|
21882
|
+
if (closed || !memory || data.offset + data.capacity > memory.buffer.byteLength) return;
|
|
21883
|
+
const control = new Int32Array(memory.buffer, data.offset, 1);
|
|
21884
|
+
const output = new Uint8Array(memory.buffer, data.offset, data.capacity);
|
|
21885
|
+
pending.add(control);
|
|
21886
|
+
try {
|
|
21887
|
+
const request = data.request.slice();
|
|
21888
|
+
new DataView(request.buffer).setUint32(8, generation, true);
|
|
21889
|
+
const response = await handle(request);
|
|
21890
|
+
if (closed) return;
|
|
21891
|
+
if (response.length > output.length - 64) throw new Error("thread response exceeds capacity");
|
|
21892
|
+
output.set(response, 64);
|
|
21893
|
+
const status = new DataView(response.buffer, response.byteOffset).getInt32(12, true);
|
|
21894
|
+
if (status < 0) output.set(new TextEncoder().encode(ERRNO_NAMES[-status] ?? "EIO"), 4);
|
|
21895
|
+
Atomics.store(control, 0, response.length);
|
|
21896
|
+
} catch {
|
|
21897
|
+
Atomics.store(control, 0, -1);
|
|
21898
|
+
} finally {
|
|
21899
|
+
pending.delete(control);
|
|
21900
|
+
Atomics.notify(control, 0);
|
|
21901
|
+
}
|
|
21902
|
+
};
|
|
21903
|
+
return { name, setMemory(value) {
|
|
21904
|
+
if (!(value.buffer instanceof SharedArrayBuffer)) throw new Error("Python thread memory must be shared");
|
|
21905
|
+
memory = value;
|
|
21906
|
+
memoryReady();
|
|
21907
|
+
}, close() {
|
|
21908
|
+
closed = true;
|
|
21909
|
+
memoryReady();
|
|
21910
|
+
channel.close();
|
|
21911
|
+
for (const control of pending) {
|
|
21912
|
+
Atomics.store(control, 0, -1);
|
|
21913
|
+
Atomics.notify(control, 0);
|
|
21914
|
+
}
|
|
21915
|
+
pending.clear();
|
|
21916
|
+
} };
|
|
21917
|
+
}
|
|
21918
|
+
|
|
21866
21919
|
// src/python/protocol.ts
|
|
21867
21920
|
var ProtocolError = class extends Error {
|
|
21868
21921
|
code = "ERR_SBX_ABI_PROTOCOL";
|
|
@@ -23109,6 +23162,7 @@ async function startPythonProcess(options) {
|
|
|
23109
23162
|
sockets: options.sockets,
|
|
23110
23163
|
processes: options.processes
|
|
23111
23164
|
});
|
|
23165
|
+
const threads = await createThreadTransport(generation, handle);
|
|
23112
23166
|
const buffers = createHostTransportBuffers();
|
|
23113
23167
|
let fault = null;
|
|
23114
23168
|
const server = new HostCallServer(buffers, handle, (error) => {
|
|
@@ -23127,8 +23181,10 @@ async function startPythonProcess(options) {
|
|
|
23127
23181
|
const cleanup = () => {
|
|
23128
23182
|
if (done) return;
|
|
23129
23183
|
done = true;
|
|
23184
|
+
controller.abort();
|
|
23130
23185
|
options.signal.removeEventListener("abort", abort);
|
|
23131
23186
|
server.close();
|
|
23187
|
+
threads.close();
|
|
23132
23188
|
table.closeAll();
|
|
23133
23189
|
try {
|
|
23134
23190
|
worker?.terminate();
|
|
@@ -23143,6 +23199,10 @@ async function startPythonProcess(options) {
|
|
|
23143
23199
|
}
|
|
23144
23200
|
worker.onMessage((message) => {
|
|
23145
23201
|
const detail = message;
|
|
23202
|
+
if (detail?.type === "thread-memory" && detail.memory) {
|
|
23203
|
+
threads.setMemory(detail.memory);
|
|
23204
|
+
return;
|
|
23205
|
+
}
|
|
23146
23206
|
if (detail?.type === "wake") {
|
|
23147
23207
|
void server.pump();
|
|
23148
23208
|
return;
|
|
@@ -23175,6 +23235,7 @@ async function startPythonProcess(options) {
|
|
|
23175
23235
|
type: "start",
|
|
23176
23236
|
buffers,
|
|
23177
23237
|
generation,
|
|
23238
|
+
threadChannel: threads.name,
|
|
23178
23239
|
moduleUrl: options.manifest.artifacts.moduleUrl,
|
|
23179
23240
|
argv: options.argv,
|
|
23180
23241
|
env: options.env,
|
|
@@ -25301,7 +25362,30 @@ if _egress:
|
|
|
25301
25362
|
sys.meta_path.insert(0, _SbxHttpxHook())
|
|
25302
25363
|
|
|
25303
25364
|
|
|
25304
|
-
if os.environ.get("
|
|
25365
|
+
if os.environ.get("SBX_THREAD_HOST_CALLS") == "1":
|
|
25366
|
+
import selectors as _selectors
|
|
25367
|
+
import threading as _threading
|
|
25368
|
+
import time as _time
|
|
25369
|
+
|
|
25370
|
+
class _SbxThreadSelector(_selectors.SelectSelector):
|
|
25371
|
+
"""Let Emscripten service filesystem calls proxied by Python threads.
|
|
25372
|
+
|
|
25373
|
+
Native socket calls have an independent channel per request. Filesystem
|
|
25374
|
+
calls still use the main runtime thread, so it must periodically leave
|
|
25375
|
+
its host poll and yield whenever other Python threads are alive. Keep
|
|
25376
|
+
the standard asyncio executor, context propagation and cancellation.
|
|
25377
|
+
"""
|
|
25378
|
+
def select(self, timeout=None):
|
|
25379
|
+
threaded = _threading.active_count() > 1
|
|
25380
|
+
if threaded:
|
|
25381
|
+
timeout = 0.01 if timeout is None else min(max(timeout, 0), 0.01)
|
|
25382
|
+
ready = super().select(timeout)
|
|
25383
|
+
if threaded:
|
|
25384
|
+
_time.sleep(0.001)
|
|
25385
|
+
return ready
|
|
25386
|
+
|
|
25387
|
+
_selectors.DefaultSelector = _SbxThreadSelector
|
|
25388
|
+
elif os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
|
|
25305
25389
|
import asyncio
|
|
25306
25390
|
|
|
25307
25391
|
async def _to_thread(func, /, *args, **kwargs):
|
package/dist/index.js
CHANGED
|
@@ -20010,7 +20010,7 @@ var CAPABILITIES = ["files", "descriptors", "pipes", "readiness", "time", "ident
|
|
|
20010
20010
|
var OP_NAMES = Object.fromEntries(
|
|
20011
20011
|
Object.entries(Op).map(([name, code]) => [code, name])
|
|
20012
20012
|
);
|
|
20013
|
-
Object.fromEntries(
|
|
20013
|
+
var ERRNO_NAMES = Object.fromEntries(
|
|
20014
20014
|
Object.entries(Errno).map(([name, code]) => [code, name])
|
|
20015
20015
|
);
|
|
20016
20016
|
|
|
@@ -21086,7 +21086,7 @@ var wheels_default = {
|
|
|
21086
21086
|
|
|
21087
21087
|
// package.json
|
|
21088
21088
|
var package_default = {
|
|
21089
|
-
version: "0.2.
|
|
21089
|
+
version: "0.2.14"};
|
|
21090
21090
|
|
|
21091
21091
|
// src/python/extension-abi.ts
|
|
21092
21092
|
var EXTENSION_ABI = {
|
|
@@ -21846,6 +21846,59 @@ var FileService = class {
|
|
|
21846
21846
|
init_signals();
|
|
21847
21847
|
init_binary();
|
|
21848
21848
|
|
|
21849
|
+
// src/python/thread-transport.ts
|
|
21850
|
+
init_binary();
|
|
21851
|
+
async function createThreadTransport(generation, handle) {
|
|
21852
|
+
const crypto2 = globalThis.crypto ?? (await nodeBuiltin("crypto")).webcrypto;
|
|
21853
|
+
const name = `sbx-python-${crypto2.randomUUID()}`;
|
|
21854
|
+
const channel = new BroadcastChannel(name);
|
|
21855
|
+
const pending = /* @__PURE__ */ new Set();
|
|
21856
|
+
let closed = false;
|
|
21857
|
+
let memory;
|
|
21858
|
+
let memoryReady;
|
|
21859
|
+
const ready = new Promise((resolve3) => {
|
|
21860
|
+
memoryReady = resolve3;
|
|
21861
|
+
});
|
|
21862
|
+
channel.onmessage = async ({ data }) => {
|
|
21863
|
+
if (closed || !(data?.request instanceof Uint8Array) || data.request.length < 16 || !Number.isSafeInteger(data.offset) || data.offset < 0 || data.offset % 4 || !Number.isSafeInteger(data.capacity) || data.capacity < 84) return;
|
|
21864
|
+
await ready;
|
|
21865
|
+
if (closed || !memory || data.offset + data.capacity > memory.buffer.byteLength) return;
|
|
21866
|
+
const control = new Int32Array(memory.buffer, data.offset, 1);
|
|
21867
|
+
const output = new Uint8Array(memory.buffer, data.offset, data.capacity);
|
|
21868
|
+
pending.add(control);
|
|
21869
|
+
try {
|
|
21870
|
+
const request = data.request.slice();
|
|
21871
|
+
new DataView(request.buffer).setUint32(8, generation, true);
|
|
21872
|
+
const response = await handle(request);
|
|
21873
|
+
if (closed) return;
|
|
21874
|
+
if (response.length > output.length - 64) throw new Error("thread response exceeds capacity");
|
|
21875
|
+
output.set(response, 64);
|
|
21876
|
+
const status = new DataView(response.buffer, response.byteOffset).getInt32(12, true);
|
|
21877
|
+
if (status < 0) output.set(new TextEncoder().encode(ERRNO_NAMES[-status] ?? "EIO"), 4);
|
|
21878
|
+
Atomics.store(control, 0, response.length);
|
|
21879
|
+
} catch {
|
|
21880
|
+
Atomics.store(control, 0, -1);
|
|
21881
|
+
} finally {
|
|
21882
|
+
pending.delete(control);
|
|
21883
|
+
Atomics.notify(control, 0);
|
|
21884
|
+
}
|
|
21885
|
+
};
|
|
21886
|
+
return { name, setMemory(value) {
|
|
21887
|
+
if (!(value.buffer instanceof SharedArrayBuffer)) throw new Error("Python thread memory must be shared");
|
|
21888
|
+
memory = value;
|
|
21889
|
+
memoryReady();
|
|
21890
|
+
}, close() {
|
|
21891
|
+
closed = true;
|
|
21892
|
+
memoryReady();
|
|
21893
|
+
channel.close();
|
|
21894
|
+
for (const control of pending) {
|
|
21895
|
+
Atomics.store(control, 0, -1);
|
|
21896
|
+
Atomics.notify(control, 0);
|
|
21897
|
+
}
|
|
21898
|
+
pending.clear();
|
|
21899
|
+
} };
|
|
21900
|
+
}
|
|
21901
|
+
|
|
21849
21902
|
// src/python/protocol.ts
|
|
21850
21903
|
var ProtocolError = class extends Error {
|
|
21851
21904
|
code = "ERR_SBX_ABI_PROTOCOL";
|
|
@@ -23092,6 +23145,7 @@ async function startPythonProcess(options) {
|
|
|
23092
23145
|
sockets: options.sockets,
|
|
23093
23146
|
processes: options.processes
|
|
23094
23147
|
});
|
|
23148
|
+
const threads = await createThreadTransport(generation, handle);
|
|
23095
23149
|
const buffers = createHostTransportBuffers();
|
|
23096
23150
|
let fault = null;
|
|
23097
23151
|
const server = new HostCallServer(buffers, handle, (error) => {
|
|
@@ -23110,8 +23164,10 @@ async function startPythonProcess(options) {
|
|
|
23110
23164
|
const cleanup = () => {
|
|
23111
23165
|
if (done) return;
|
|
23112
23166
|
done = true;
|
|
23167
|
+
controller.abort();
|
|
23113
23168
|
options.signal.removeEventListener("abort", abort);
|
|
23114
23169
|
server.close();
|
|
23170
|
+
threads.close();
|
|
23115
23171
|
table.closeAll();
|
|
23116
23172
|
try {
|
|
23117
23173
|
worker?.terminate();
|
|
@@ -23126,6 +23182,10 @@ async function startPythonProcess(options) {
|
|
|
23126
23182
|
}
|
|
23127
23183
|
worker.onMessage((message) => {
|
|
23128
23184
|
const detail = message;
|
|
23185
|
+
if (detail?.type === "thread-memory" && detail.memory) {
|
|
23186
|
+
threads.setMemory(detail.memory);
|
|
23187
|
+
return;
|
|
23188
|
+
}
|
|
23129
23189
|
if (detail?.type === "wake") {
|
|
23130
23190
|
void server.pump();
|
|
23131
23191
|
return;
|
|
@@ -23158,6 +23218,7 @@ async function startPythonProcess(options) {
|
|
|
23158
23218
|
type: "start",
|
|
23159
23219
|
buffers,
|
|
23160
23220
|
generation,
|
|
23221
|
+
threadChannel: threads.name,
|
|
23161
23222
|
moduleUrl: options.manifest.artifacts.moduleUrl,
|
|
23162
23223
|
argv: options.argv,
|
|
23163
23224
|
env: options.env,
|
|
@@ -25284,7 +25345,30 @@ if _egress:
|
|
|
25284
25345
|
sys.meta_path.insert(0, _SbxHttpxHook())
|
|
25285
25346
|
|
|
25286
25347
|
|
|
25287
|
-
if os.environ.get("
|
|
25348
|
+
if os.environ.get("SBX_THREAD_HOST_CALLS") == "1":
|
|
25349
|
+
import selectors as _selectors
|
|
25350
|
+
import threading as _threading
|
|
25351
|
+
import time as _time
|
|
25352
|
+
|
|
25353
|
+
class _SbxThreadSelector(_selectors.SelectSelector):
|
|
25354
|
+
"""Let Emscripten service filesystem calls proxied by Python threads.
|
|
25355
|
+
|
|
25356
|
+
Native socket calls have an independent channel per request. Filesystem
|
|
25357
|
+
calls still use the main runtime thread, so it must periodically leave
|
|
25358
|
+
its host poll and yield whenever other Python threads are alive. Keep
|
|
25359
|
+
the standard asyncio executor, context propagation and cancellation.
|
|
25360
|
+
"""
|
|
25361
|
+
def select(self, timeout=None):
|
|
25362
|
+
threaded = _threading.active_count() > 1
|
|
25363
|
+
if threaded:
|
|
25364
|
+
timeout = 0.01 if timeout is None else min(max(timeout, 0), 0.01)
|
|
25365
|
+
ready = super().select(timeout)
|
|
25366
|
+
if threaded:
|
|
25367
|
+
_time.sleep(0.001)
|
|
25368
|
+
return ready
|
|
25369
|
+
|
|
25370
|
+
_selectors.DefaultSelector = _SbxThreadSelector
|
|
25371
|
+
elif os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
|
|
25288
25372
|
import asyncio
|
|
25289
25373
|
|
|
25290
25374
|
async def _to_thread(func, /, *args, **kwargs):
|