@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc → 0.0.0-alpha.1380.bdd09a9f
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/cli.js +24 -9
- package/dist/test.js +24 -9
- package/package.json +1 -1
- package/src/compile.ts +1 -0
- package/src/sandbox.ts +57 -22
package/dist/cli.js
CHANGED
|
@@ -1543,14 +1543,20 @@ var gateResultSchema = z2.discriminatedUnion("kind", [
|
|
|
1543
1543
|
})
|
|
1544
1544
|
]);
|
|
1545
1545
|
var asyncModulePromise = null;
|
|
1546
|
+
var asyncModule = null;
|
|
1546
1547
|
function loadAsyncModule() {
|
|
1547
1548
|
if (!asyncModulePromise) {
|
|
1548
1549
|
const pending = newQuickJSAsyncWASMModuleFromVariant(
|
|
1549
1550
|
import("@jitl/quickjs-wasmfile-release-asyncify")
|
|
1550
1551
|
);
|
|
1551
|
-
pending.
|
|
1552
|
-
|
|
1553
|
-
|
|
1552
|
+
pending.then(
|
|
1553
|
+
(mod) => {
|
|
1554
|
+
asyncModule = mod;
|
|
1555
|
+
},
|
|
1556
|
+
() => {
|
|
1557
|
+
if (asyncModulePromise === pending) asyncModulePromise = null;
|
|
1558
|
+
}
|
|
1559
|
+
);
|
|
1554
1560
|
asyncModulePromise = pending;
|
|
1555
1561
|
}
|
|
1556
1562
|
return asyncModulePromise;
|
|
@@ -1558,9 +1564,20 @@ function loadAsyncModule() {
|
|
|
1558
1564
|
async function createVmInstance(opts) {
|
|
1559
1565
|
const QuickJS = await loadAsyncModule();
|
|
1560
1566
|
const suspends = Object.keys(opts.hostFns ?? {}).length > 0;
|
|
1561
|
-
const runtime =
|
|
1562
|
-
const vm = runtime
|
|
1567
|
+
const runtime = QuickJS.newRuntime();
|
|
1568
|
+
const vm = runtime.newContext();
|
|
1563
1569
|
vm.runtime.setMemoryLimit(32 * 1024 * 1024);
|
|
1570
|
+
const hostFnNames = Object.keys(opts.hostFns ?? {});
|
|
1571
|
+
function freeVm() {
|
|
1572
|
+
if (hostFnNames.length > 0) {
|
|
1573
|
+
const names = [...hostFnNames, "__app", "__realm_module__"];
|
|
1574
|
+
const release = vm.evalCode(names.map((n) => `globalThis.${n} = undefined;`).join("\n"));
|
|
1575
|
+
if (release.error) release.error.dispose();
|
|
1576
|
+
else release.value.dispose();
|
|
1577
|
+
}
|
|
1578
|
+
vm.dispose();
|
|
1579
|
+
runtime.dispose();
|
|
1580
|
+
}
|
|
1564
1581
|
for (const [name, fn] of Object.entries(opts.hostFns ?? {})) {
|
|
1565
1582
|
const hostFn = vm.newAsyncifiedFunction(
|
|
1566
1583
|
name,
|
|
@@ -1577,8 +1594,7 @@ async function createVmInstance(opts) {
|
|
|
1577
1594
|
if (init.error) {
|
|
1578
1595
|
const err = vm.dump(init.error);
|
|
1579
1596
|
init.error.dispose();
|
|
1580
|
-
|
|
1581
|
-
runtime?.dispose();
|
|
1597
|
+
freeVm();
|
|
1582
1598
|
throw new RealmCompileError({
|
|
1583
1599
|
kind: "eval",
|
|
1584
1600
|
message: typeof err === "string" ? err : JSON.stringify(err)
|
|
@@ -1625,8 +1641,7 @@ async function createVmInstance(opts) {
|
|
|
1625
1641
|
disposing = true;
|
|
1626
1642
|
await queue.catch(() => {
|
|
1627
1643
|
});
|
|
1628
|
-
|
|
1629
|
-
runtime?.dispose();
|
|
1644
|
+
freeVm();
|
|
1630
1645
|
}
|
|
1631
1646
|
};
|
|
1632
1647
|
}
|
package/dist/test.js
CHANGED
|
@@ -1257,14 +1257,20 @@ var gateResultSchema = z.discriminatedUnion("kind", [
|
|
|
1257
1257
|
})
|
|
1258
1258
|
]);
|
|
1259
1259
|
var asyncModulePromise = null;
|
|
1260
|
+
var asyncModule = null;
|
|
1260
1261
|
function loadAsyncModule() {
|
|
1261
1262
|
if (!asyncModulePromise) {
|
|
1262
1263
|
const pending = newQuickJSAsyncWASMModuleFromVariant(
|
|
1263
1264
|
import("@jitl/quickjs-wasmfile-release-asyncify")
|
|
1264
1265
|
);
|
|
1265
|
-
pending.
|
|
1266
|
-
|
|
1267
|
-
|
|
1266
|
+
pending.then(
|
|
1267
|
+
(mod) => {
|
|
1268
|
+
asyncModule = mod;
|
|
1269
|
+
},
|
|
1270
|
+
() => {
|
|
1271
|
+
if (asyncModulePromise === pending) asyncModulePromise = null;
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1268
1274
|
asyncModulePromise = pending;
|
|
1269
1275
|
}
|
|
1270
1276
|
return asyncModulePromise;
|
|
@@ -1272,9 +1278,20 @@ function loadAsyncModule() {
|
|
|
1272
1278
|
async function createVmInstance(opts) {
|
|
1273
1279
|
const QuickJS = await loadAsyncModule();
|
|
1274
1280
|
const suspends = Object.keys(opts.hostFns ?? {}).length > 0;
|
|
1275
|
-
const runtime =
|
|
1276
|
-
const vm = runtime
|
|
1281
|
+
const runtime = QuickJS.newRuntime();
|
|
1282
|
+
const vm = runtime.newContext();
|
|
1277
1283
|
vm.runtime.setMemoryLimit(32 * 1024 * 1024);
|
|
1284
|
+
const hostFnNames = Object.keys(opts.hostFns ?? {});
|
|
1285
|
+
function freeVm() {
|
|
1286
|
+
if (hostFnNames.length > 0) {
|
|
1287
|
+
const names = [...hostFnNames, "__app", "__realm_module__"];
|
|
1288
|
+
const release = vm.evalCode(names.map((n) => `globalThis.${n} = undefined;`).join("\n"));
|
|
1289
|
+
if (release.error) release.error.dispose();
|
|
1290
|
+
else release.value.dispose();
|
|
1291
|
+
}
|
|
1292
|
+
vm.dispose();
|
|
1293
|
+
runtime.dispose();
|
|
1294
|
+
}
|
|
1278
1295
|
for (const [name, fn] of Object.entries(opts.hostFns ?? {})) {
|
|
1279
1296
|
const hostFn = vm.newAsyncifiedFunction(
|
|
1280
1297
|
name,
|
|
@@ -1291,8 +1308,7 @@ async function createVmInstance(opts) {
|
|
|
1291
1308
|
if (init.error) {
|
|
1292
1309
|
const err = vm.dump(init.error);
|
|
1293
1310
|
init.error.dispose();
|
|
1294
|
-
|
|
1295
|
-
runtime?.dispose();
|
|
1311
|
+
freeVm();
|
|
1296
1312
|
throw new RealmCompileError({
|
|
1297
1313
|
kind: "eval",
|
|
1298
1314
|
message: typeof err === "string" ? err : JSON.stringify(err)
|
|
@@ -1339,8 +1355,7 @@ async function createVmInstance(opts) {
|
|
|
1339
1355
|
disposing = true;
|
|
1340
1356
|
await queue.catch(() => {
|
|
1341
1357
|
});
|
|
1342
|
-
|
|
1343
|
-
runtime?.dispose();
|
|
1358
|
+
freeVm();
|
|
1344
1359
|
}
|
|
1345
1360
|
};
|
|
1346
1361
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yourrealm/sdk",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.1380.bdd09a9f",
|
|
4
4
|
"description": "The Realm SDK: createApp, the r.* permission factories, the ui surface, the realm-sdk CLI and test harness.",
|
|
5
5
|
"license": "AGPL-3.0-only WITH Classpath-exception-2.0",
|
|
6
6
|
"repository": {
|
package/src/compile.ts
CHANGED
package/src/sandbox.ts
CHANGED
|
@@ -759,6 +759,19 @@ interface VmInstance {
|
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
let asyncModulePromise: ReturnType<typeof newQuickJSAsyncWASMModuleFromVariant> | null = null;
|
|
762
|
+
let asyncModule: Awaited<ReturnType<typeof newQuickJSAsyncWASMModuleFromVariant>> | null = null;
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Bytes of WebAssembly linear memory the QuickJS module holds, or `null` before
|
|
766
|
+
* the first eval loads it. This is the sandbox's true footprint: every runtime
|
|
767
|
+
* mallocs out of this one arena, and the arena only ever grows - Emscripten
|
|
768
|
+
* cannot hand pages back - so the number is the peak concurrent demand plus
|
|
769
|
+
* anything a dispose failed to free. V8 does not count it under `external`, so
|
|
770
|
+
* it is otherwise invisible in `Deno.memoryUsage()`.
|
|
771
|
+
*/
|
|
772
|
+
export function sandboxArenaBytes(): number | null {
|
|
773
|
+
return asyncModule ? asyncModule.getWasmMemory().buffer.byteLength : null;
|
|
774
|
+
}
|
|
762
775
|
|
|
763
776
|
// Memoize the async WASM module load (shared by runInSandbox + the gate), but
|
|
764
777
|
// never cache a rejection: a transient load failure must not permanently wedge
|
|
@@ -773,9 +786,14 @@ function loadAsyncModule(): ReturnType<typeof newQuickJSAsyncWASMModuleFromVaria
|
|
|
773
786
|
const pending = newQuickJSAsyncWASMModuleFromVariant(
|
|
774
787
|
import("@jitl/quickjs-wasmfile-release-asyncify"),
|
|
775
788
|
);
|
|
776
|
-
pending.
|
|
777
|
-
|
|
778
|
-
|
|
789
|
+
pending.then(
|
|
790
|
+
(mod) => {
|
|
791
|
+
asyncModule = mod;
|
|
792
|
+
},
|
|
793
|
+
() => {
|
|
794
|
+
if (asyncModulePromise === pending) asyncModulePromise = null;
|
|
795
|
+
},
|
|
796
|
+
);
|
|
779
797
|
asyncModulePromise = pending;
|
|
780
798
|
}
|
|
781
799
|
return asyncModulePromise;
|
|
@@ -802,22 +820,41 @@ async function createVmInstance(opts: {
|
|
|
802
820
|
const QuickJS = await loadAsyncModule();
|
|
803
821
|
const suspends = Object.keys(opts.hostFns ?? {}).length > 0;
|
|
804
822
|
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// runtime-scoped HostRef in the right order; disposing an explicit runtime
|
|
816
|
-
// there double-frees the HostRef and throws. These are warm-pooled and
|
|
817
|
-
// disposed rarely, so the (unreclaimed) heap doesn't accumulate.
|
|
818
|
-
const runtime = suspends ? null : QuickJS.newRuntime();
|
|
819
|
-
const vm = runtime ? runtime.newContext() : QuickJS.newContext();
|
|
823
|
+
// Every VM owns its runtime explicitly, whichever eval mode it runs in. The
|
|
824
|
+
// shorthand `QuickJS.newContext()` leaves its runtime alive after
|
|
825
|
+
// `context.dispose()` in quickjs-emscripten 0.32, and the WASM arena those
|
|
826
|
+
// runtimes sit in never shrinks: each create/dispose then pins ~5MB for the
|
|
827
|
+
// life of the process. Home's gate VMs churned through that path once per
|
|
828
|
+
// idle-out and grew the arena by ~700MB in a day and a half (on a 2GB
|
|
829
|
+
// ceiling, past which every eval OOMs). See `freeVm` for the dispose order
|
|
830
|
+
// the explicit runtime needs when host fns are bound.
|
|
831
|
+
const runtime = QuickJS.newRuntime();
|
|
832
|
+
const vm = runtime.newContext();
|
|
820
833
|
vm.runtime.setMemoryLimit(32 * 1024 * 1024); // 32 MB
|
|
834
|
+
const hostFnNames = Object.keys(opts.hostFns ?? {});
|
|
835
|
+
|
|
836
|
+
// Free the context and its runtime, in the order the host fns need. A bound
|
|
837
|
+
// host fn is a QuickJS function object whose finalizer calls back into the
|
|
838
|
+
// module to free its HostRef, looked up by runtime. When that object is still
|
|
839
|
+
// reachable while `vm.dispose()` tears the context down, the lookup fails
|
|
840
|
+
// ("QuickJSRuntime not found when trying to free HostRef"), the dispose throws
|
|
841
|
+
// half-way and the runtime is never freed. Dropping every global that can
|
|
842
|
+
// reach a host fn first lets the refcount hit zero and the finalizer run while
|
|
843
|
+
// the runtime is still registered; then both disposes complete. If
|
|
844
|
+
// `vm.dispose()` still throws, the runtime is deliberately left alone - a
|
|
845
|
+
// `runtime.dispose()` over a half-freed context aborts the whole Emscripten
|
|
846
|
+
// module - so that path leaks one VM rather than wedging every later eval.
|
|
847
|
+
function freeVm(): void {
|
|
848
|
+
if (hostFnNames.length > 0) {
|
|
849
|
+
const names = [...hostFnNames, "__app", "__realm_module__"];
|
|
850
|
+
const release = vm.evalCode(names.map((n) => `globalThis.${n} = undefined;`).join("\n"));
|
|
851
|
+
if (release.error) release.error.dispose();
|
|
852
|
+
else release.value.dispose();
|
|
853
|
+
}
|
|
854
|
+
vm.dispose();
|
|
855
|
+
runtime.dispose();
|
|
856
|
+
}
|
|
857
|
+
|
|
821
858
|
for (const [name, fn] of Object.entries(opts.hostFns ?? {})) {
|
|
822
859
|
const hostFn = vm.newAsyncifiedFunction(name, async (argHandle) =>
|
|
823
860
|
vm.newString(await fn(vm.getString(argHandle))),
|
|
@@ -836,8 +873,7 @@ async function createVmInstance(opts: {
|
|
|
836
873
|
if (init.error) {
|
|
837
874
|
const err = vm.dump(init.error);
|
|
838
875
|
init.error.dispose();
|
|
839
|
-
|
|
840
|
-
runtime?.dispose();
|
|
876
|
+
freeVm();
|
|
841
877
|
throw new RealmCompileError({
|
|
842
878
|
kind: "eval",
|
|
843
879
|
message: typeof err === "string" ? err : JSON.stringify(err),
|
|
@@ -891,8 +927,7 @@ async function createVmInstance(opts: {
|
|
|
891
927
|
// dispose mid-call (an eviction during a suspended host fn) would otherwise
|
|
892
928
|
// resume the asyncified eval into a freed context and hang.
|
|
893
929
|
await queue.catch(() => {});
|
|
894
|
-
|
|
895
|
-
runtime?.dispose();
|
|
930
|
+
freeVm();
|
|
896
931
|
},
|
|
897
932
|
};
|
|
898
933
|
}
|