@gjsify/node-gi 0.25.1 → 0.26.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/gi.js +127 -13
- package/index.d.ts +23 -0
- package/index.js +29 -0
- package/package.json +1 -1
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/src/addon.cc +2 -0
- package/src/calls.cc +25 -8
- package/src/common.h +10 -2
- package/src/loop.cc +85 -10
package/gi.js
CHANGED
|
@@ -19,7 +19,18 @@ import * as native from './index.js';
|
|
|
19
19
|
// setImmediate is a Node global but NOT a Deno global (Deno requires the explicit
|
|
20
20
|
// node:timers import; Bun/Node re-export it there too), so import it for the
|
|
21
21
|
// macrotask-deferred runAsync to work on all three runtimes.
|
|
22
|
-
|
|
22
|
+
//
|
|
23
|
+
// setInterval/clearInterval come from here for a second, load-bearing reason:
|
|
24
|
+
// the RUNTIME's timers, never `globalThis.setInterval`. A GJS source running
|
|
25
|
+
// through the bridge routinely replaces the global timers with GLib-backed ones
|
|
26
|
+
// (`@gjsify/node-globals/register` swaps them for `GLib.timeout_add` wrappers) —
|
|
27
|
+
// driving the GLib pump off a GLib timeout is a deadlock: the timer that is
|
|
28
|
+
// supposed to dispatch GLib sources is itself a GLib source nobody dispatches.
|
|
29
|
+
import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
30
|
+
// Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
|
|
31
|
+
// polyfill, whose EventEmitter never emits the runtime's 'beforeExit'. The
|
|
32
|
+
// loop-liveness hooks below must attach to the real one or they silently never fire.
|
|
33
|
+
import runtimeProcess from 'node:process';
|
|
23
34
|
// The GJS Gio DBus surface (Gio.DBus.session/system, own_name/watch_name,
|
|
24
35
|
// Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject) — ported to the L1 model.
|
|
25
36
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
@@ -977,11 +988,94 @@ function applicationRunAsync(handle) {
|
|
|
977
988
|
// concurrent runAsync calls share one timer.
|
|
978
989
|
let pumpTimer = null;
|
|
979
990
|
let pumpRefCount = 0;
|
|
991
|
+
// How many of those refs belong to the AUTO-armed pump (requireGi's permanent
|
|
992
|
+
// hold on Bun/Deno). `pumpRefCount > pumpAutoRefs` therefore means an EXPLICIT
|
|
993
|
+
// `startMainContextPump()` caller currently holds the pump.
|
|
994
|
+
let pumpAutoRefs = 0;
|
|
995
|
+
// Whether the pump timer currently HOLDS the runtime's event loop open (ref'd).
|
|
996
|
+
let pumpHoldsRuntime = false;
|
|
997
|
+
|
|
998
|
+
/** Dispatch every currently-ready GLib source, then return. */
|
|
999
|
+
function drainReadySources() {
|
|
1000
|
+
let guard = 0;
|
|
1001
|
+
while (native.iterateMainContext(false) && guard++ < 100000) {
|
|
1002
|
+
/* drain ready sources */
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Zero-N-API view over the JS-armed-work counter — `pendingView[0] > 0` is
|
|
1007
|
+
// `mainContextHasPending()` as a plain V8 memory read. The IDLE beat must not
|
|
1008
|
+
// enter the addon at all: on Deno, a single napi call from the pump tick during
|
|
1009
|
+
// the between-test-files GC window reproduces the #47 boxed-handle teardown
|
|
1010
|
+
// SIGSEGV (measured on the gtk-smoke leg: a query-only tick crashed 3/3, a tick
|
|
1011
|
+
// that never touches the addon exited 0, the full dispatching tick crashed
|
|
1012
|
+
// 10/10). So every read on the beat's idle path goes through this view.
|
|
1013
|
+
// Created LAZILY on the first pump arm (Bun/Deno only): the @gjsify/napi shim
|
|
1014
|
+
// loud-stubs external arraybuffers, and the gjs host never arms the pump.
|
|
1015
|
+
let pendingView = null;
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* One pump beat: dispatch if the program owns GLib work, then re-evaluate the
|
|
1019
|
+
* keep-alive hold. Both the gate and the hold read the SAME counter, so the
|
|
1020
|
+
* auto-armed pump dispatches exactly when it also holds the loop open.
|
|
1021
|
+
*
|
|
1022
|
+
* DISPATCH is gated like the hold — on the program's OWN GLib work. An EXPLICIT
|
|
1023
|
+
* `startMainContextPump()` holder gets unconditional dispatch (its documented
|
|
1024
|
+
* contract: co-pump the context, whatever is in it). The AUTO-armed pump
|
|
1025
|
+
* dispatches only while a JS-armed GI callback is outstanding (scope=async
|
|
1026
|
+
* completions + scope=notified sources), so a FINISHED program stops touching
|
|
1027
|
+
* the addon entirely instead of grinding through GDK/GIO's C-armed housekeeping
|
|
1028
|
+
* sources forever.
|
|
1029
|
+
*
|
|
1030
|
+
* That silence is load-bearing on Deno: the documented #47 N-API teardown race
|
|
1031
|
+
* is armed by the pump ENTERING the addon between test files (see pendingView
|
|
1032
|
+
* above), while stopping/unref'ing the pump at teardown does NOT avoid it.
|
|
1033
|
+
* Nothing is lost for a RUNNING program: any JS-observable progress path is
|
|
1034
|
+
* counted (a Gio completion is scope=async, a `GLib.timeout_add` is
|
|
1035
|
+
* scope=notified), and while one is outstanding the beat drains ALL ready
|
|
1036
|
+
* sources, C-armed ones included.
|
|
1037
|
+
*/
|
|
1038
|
+
function pumpBeat() {
|
|
1039
|
+
if (pumpRefCount > pumpAutoRefs || pendingView[0] > 0) {
|
|
1040
|
+
drainReadySources();
|
|
1041
|
+
}
|
|
1042
|
+
syncPumpKeepAlive();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Keep-alive accounting — the portable twin of the uv auto-pump's ref/unref
|
|
1047
|
+
* policy (`src/loop.cc`): scheduled GLib work (an armed timeout/idle) or an
|
|
1048
|
+
* in-flight scope=async GI callback holds the runtime's event loop open;
|
|
1049
|
+
* anything else lets it drain.
|
|
1050
|
+
*
|
|
1051
|
+
* Both halves are load-bearing. Permanently UNREF'd (the pre-fix behaviour) a
|
|
1052
|
+
* program whose only pending work lives in GLib dies mid-flight — a bare
|
|
1053
|
+
* `await` on a GLib timeout never settles and the runtime exits 0 with the
|
|
1054
|
+
* program half-run. Permanently REF'd, a sync-only program would never exit.
|
|
1055
|
+
*
|
|
1056
|
+
* Consequence, identical to Node's auto-pump: a REPEATING GLib timeout has
|
|
1057
|
+
* `setInterval` semantics and keeps the process alive, where `gjs -m` exits once
|
|
1058
|
+
* the module settles. That is the one deliberate lifetime divergence from gjs,
|
|
1059
|
+
* and it is now the SAME on node, bun and deno rather than node-only.
|
|
1060
|
+
*/
|
|
1061
|
+
function syncPumpKeepAlive() {
|
|
1062
|
+
if (pumpTimer === null) return;
|
|
1063
|
+
const hold = pendingView[0] > 0; // zero-N-API — see pendingView above
|
|
1064
|
+
if (hold === pumpHoldsRuntime) return;
|
|
1065
|
+
if (hold) pumpTimer.ref?.();
|
|
1066
|
+
else pumpTimer.unref?.();
|
|
1067
|
+
pumpHoldsRuntime = hold;
|
|
1068
|
+
}
|
|
980
1069
|
|
|
981
1070
|
/**
|
|
982
1071
|
* Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
|
|
983
1072
|
* No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
|
|
984
1073
|
* returns a disposer that decrements the count (clearing the timer at zero).
|
|
1074
|
+
*
|
|
1075
|
+
* `requireGi` arms this automatically on Bun/Deno (the Node twin of
|
|
1076
|
+
* `native.startMainLoop()`), so a plain `bun bundle.mjs` / `deno run bundle.mjs`
|
|
1077
|
+
* dispatches GLib sources with no explicit loop — calling it by hand is only
|
|
1078
|
+
* needed to hold the pump across a scope that must not depend on that.
|
|
985
1079
|
* @returns {() => void}
|
|
986
1080
|
*/
|
|
987
1081
|
export function startMainContextPump() {
|
|
@@ -991,18 +1085,17 @@ export function startMainContextPump() {
|
|
|
991
1085
|
if (native.isNodeRuntime || native.RUNTIME === 'gjs') return () => {};
|
|
992
1086
|
pumpRefCount++;
|
|
993
1087
|
if (pumpTimer === null) {
|
|
994
|
-
|
|
1088
|
+
if (pendingView === null) pendingView = native.makePumpPendingCount();
|
|
1089
|
+
// ~4 ms cadence: low latency without busy-spinning. Each beat drains every
|
|
995
1090
|
// currently-ready source (iterateMainContext(false) returns false when none
|
|
996
|
-
// remain, bounding the inner loop)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
// (Node/Bun expose Timeout.unref; Deno's numeric handle has none).
|
|
1005
|
-
if (typeof pumpTimer.unref === 'function') pumpTimer.unref();
|
|
1091
|
+
// remain, bounding the inner loop) — gated on the program's own GLib work
|
|
1092
|
+
// for the auto-armed pump, see pumpBeat — then re-evaluates the hold.
|
|
1093
|
+
pumpTimer = setInterval(pumpBeat, 4);
|
|
1094
|
+
// Start UNREF'd: the pump alone must never keep a finished program alive.
|
|
1095
|
+
// syncPumpKeepAlive() refs it for exactly as long as GLib has work.
|
|
1096
|
+
pumpTimer.unref?.();
|
|
1097
|
+
pumpHoldsRuntime = false;
|
|
1098
|
+
syncPumpKeepAlive();
|
|
1006
1099
|
}
|
|
1007
1100
|
let disposed = false;
|
|
1008
1101
|
return () => {
|
|
@@ -1019,6 +1112,7 @@ export function stopMainContextPump() {
|
|
|
1019
1112
|
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
1020
1113
|
clearInterval(pumpTimer);
|
|
1021
1114
|
pumpTimer = null;
|
|
1115
|
+
pumpHoldsRuntime = false;
|
|
1022
1116
|
}
|
|
1023
1117
|
}
|
|
1024
1118
|
|
|
@@ -2309,7 +2403,27 @@ export function requireGi(namespace, version) {
|
|
|
2309
2403
|
// 'beforeExit' fires exactly when the loop runs empty: drain what is
|
|
2310
2404
|
// ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
|
|
2311
2405
|
// uv timer revives the loop; otherwise the process exits normally.
|
|
2312
|
-
|
|
2406
|
+
runtimeProcess.on('beforeExit', () => native.pumpKick());
|
|
2407
|
+
} else if (native.RUNTIME !== 'gjs') {
|
|
2408
|
+
// Bun/Deno: no libuv to hook, so the portable timer pump IS the auto-pump.
|
|
2409
|
+
// Armed HERE, not left to the caller, so that `bun bundle.mjs` /
|
|
2410
|
+
// `deno run bundle.mjs` behaves like `node bundle.mjs` and `gjs -m`:
|
|
2411
|
+
// GLib sources dispatch with no explicit loop anywhere, and the pump's
|
|
2412
|
+
// keep-alive accounting decides when the process may exit. Leaving it
|
|
2413
|
+
// opt-in is what made the same bundle exit 0 MID-suite on bun/deno while
|
|
2414
|
+
// node ran it to completion.
|
|
2415
|
+
startMainContextPump(); // permanent +1 — the auto-pump is never disposed
|
|
2416
|
+
// Tag that hold as the AUTO pump's: pumpBeat dispatches for it only while
|
|
2417
|
+
// a JS-armed GI callback is outstanding, so a finished program produces
|
|
2418
|
+
// no teardown churn (the deno #47 window) — explicit callers keep
|
|
2419
|
+
// unconditional dispatch.
|
|
2420
|
+
pumpAutoRefs++;
|
|
2421
|
+
// The portable analogue of the Node pump's beforeExit kick. The runtime's
|
|
2422
|
+
// loop just ran empty, which is exactly the moment an unref'd pump would
|
|
2423
|
+
// let the process die with GLib work still outstanding: run one beat —
|
|
2424
|
+
// if the program still owns GLib work it drains + re-refs (reviving the
|
|
2425
|
+
// loop); if not, the beat stays silent and the process exits.
|
|
2426
|
+
runtimeProcess.on('beforeExit', pumpBeat);
|
|
2313
2427
|
}
|
|
2314
2428
|
loopAttached = true;
|
|
2315
2429
|
}
|
package/index.d.ts
CHANGED
|
@@ -418,6 +418,27 @@ export function startMainLoop(): void;
|
|
|
418
418
|
*/
|
|
419
419
|
export function iterateMainContext(mayBlock?: boolean): boolean;
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Whether the default GLib main context holds work a *pumping* runtime must stay
|
|
423
|
+
* alive for: a scheduled source (an armed timeout/idle) or an in-flight
|
|
424
|
+
* scope=async GI callback. Pure GLib — the query half of the keep-alive contract
|
|
425
|
+
* the L1 `startMainContextPump` uses on Bun/Deno to decide whether its timer
|
|
426
|
+
* holds the runtime's event loop open. Node reads the same facts inline in the
|
|
427
|
+
* uv auto-pump.
|
|
428
|
+
*/
|
|
429
|
+
export function mainContextHasPending(): boolean;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create an `Int32Array(1)` view over the same JS-armed-work counter
|
|
433
|
+
* `mainContextHasPending()` reads (`view[0] > 0` is equivalent). Reading the
|
|
434
|
+
* view is a plain memory access — no call into the addon — which the Bun/Deno
|
|
435
|
+
* pump's idle beat depends on: on Deno, entering the addon from a timer tick
|
|
436
|
+
* during the between-test-files GC window reproduces the #47 boxed-handle
|
|
437
|
+
* teardown SIGSEGV. Lazy factory: the @gjsify/napi shim loud-stubs external
|
|
438
|
+
* arraybuffers and never arms the portable pump, so it must not run at Init.
|
|
439
|
+
*/
|
|
440
|
+
export function makePumpPendingCount(): Int32Array;
|
|
441
|
+
|
|
421
442
|
/**
|
|
422
443
|
* Drain ready GLib sources + re-arm the auto-pump's libuv wake-ups now (no-op
|
|
423
444
|
* unless {@link startMainLoop} armed the pump on this env). The L1 layer wires
|
|
@@ -548,6 +569,8 @@ declare const native: {
|
|
|
548
569
|
isVariantHandle: typeof isVariantHandle;
|
|
549
570
|
startMainLoop: typeof startMainLoop;
|
|
550
571
|
iterateMainContext: typeof iterateMainContext;
|
|
572
|
+
mainContextHasPending: typeof mainContextHasPending;
|
|
573
|
+
makePumpPendingCount: typeof makePumpPendingCount;
|
|
551
574
|
pumpKick: typeof pumpKick;
|
|
552
575
|
setMicrotaskDrain: typeof setMicrotaskDrain;
|
|
553
576
|
connectSignal: typeof connectSignal;
|
package/index.js
CHANGED
|
@@ -661,6 +661,35 @@ export const startMainLoop = native.startMainLoop;
|
|
|
661
661
|
*/
|
|
662
662
|
export const iterateMainContext = native.iterateMainContext;
|
|
663
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Whether the default GLib main context holds work a *pumping* runtime must stay
|
|
666
|
+
* alive for: a scheduled source (an armed timeout/idle — the prepare/query
|
|
667
|
+
* timeout hint is >= 0) or an in-flight scope=async GI callback. Pure GLib —
|
|
668
|
+
* the companion of {@link iterateMainContext} and the query half of the
|
|
669
|
+
* keep-alive contract the L1 `startMainContextPump` applies on Bun/Deno (Node
|
|
670
|
+
* reads the same facts inline in the uv auto-pump). A `false` answer is what
|
|
671
|
+
* lets a sync-only program exit immediately.
|
|
672
|
+
* @returns {boolean}
|
|
673
|
+
*/
|
|
674
|
+
export const mainContextHasPending = native.mainContextHasPending;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Create an `Int32Array(1)` view over the SAME JS-armed-work counter
|
|
678
|
+
* {@link mainContextHasPending} reads — `view[0] > 0` ⇔
|
|
679
|
+
* `mainContextHasPending()`. The zero-N-API spelling: reading the view is a
|
|
680
|
+
* plain V8 memory access, no call into the addon. Load-bearing for the Bun/Deno
|
|
681
|
+
* pump's IDLE beat — on Deno, merely ENTERING the addon from a timer tick
|
|
682
|
+
* during the between-test-files GC window reproduces the #47 boxed-handle
|
|
683
|
+
* teardown SIGSEGV (measured: a query-only tick crashed 3/3 where a tick that
|
|
684
|
+
* never touches the addon exited 0), so the beat must be able to answer "is
|
|
685
|
+
* there work?" without a single native call. A lazy FACTORY rather than an
|
|
686
|
+
* Init-time export: the @gjsify/napi shim (the gjs-napi conformance oracle)
|
|
687
|
+
* loud-stubs `napi_create_external_arraybuffer`, and the gjs host never arms
|
|
688
|
+
* the portable pump — called only on the first Bun/Deno pump arm.
|
|
689
|
+
* @returns {Int32Array}
|
|
690
|
+
*/
|
|
691
|
+
export const makePumpPendingCount = native.makePumpPendingCount;
|
|
692
|
+
|
|
664
693
|
/**
|
|
665
694
|
* Drain ready GLib sources + re-arm the auto-pump's libuv wake-ups NOW (no-op
|
|
666
695
|
* unless {@link startMainLoop} armed the pump, i.e. Node's main env). The L1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -113,6 +113,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
113
113
|
exports.Set("isVariantHandle", Napi::Function::New(env, IsVariantHandle));
|
|
114
114
|
exports.Set("startMainLoop", Napi::Function::New(env, StartMainLoop));
|
|
115
115
|
exports.Set("iterateMainContext", Napi::Function::New(env, IterateMainContext));
|
|
116
|
+
exports.Set("mainContextHasPending", Napi::Function::New(env, MainContextHasPending));
|
|
117
|
+
exports.Set("makePumpPendingCount", Napi::Function::New(env, MakePumpPendingCount));
|
|
116
118
|
exports.Set("pumpKick", Napi::Function::New(env, PumpKick));
|
|
117
119
|
exports.Set("setMicrotaskDrain", Napi::Function::New(env, SetMicrotaskDrain));
|
|
118
120
|
exports.Set("connectSignal", Napi::Function::New(env, ConnectSignal));
|
package/src/calls.cc
CHANGED
|
@@ -28,10 +28,23 @@ struct NodeGiCallback {
|
|
|
28
28
|
ffi_closure* closure; // from gi_callable_info_create_closure
|
|
29
29
|
gpointer native; // executable trampoline address
|
|
30
30
|
GIScopeType scope;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
31
|
+
// Counted in the auto-pump's in-flight keep-alive — the process stays alive
|
|
32
|
+
// while JS-armed GLib work is outstanding, like in-flight Node I/O.
|
|
33
|
+
// • scope=async — a one-shot completion (GAsyncReadyCallback), released
|
|
34
|
+
// at the trampoline's single invocation.
|
|
35
|
+
// • scope=notified — a callback GLib owns until its GDestroyNotify fires:
|
|
36
|
+
// `GLib.timeout_add` / `GLib.idle_add` and friends. Released by
|
|
37
|
+
// NodeGiCallbackFree, i.e. exactly when GLib drops the source (the
|
|
38
|
+
// callback returned SOURCE_REMOVE, or `GLib.Source.remove` killed it).
|
|
39
|
+
// Either way released exactly once, also at free if it never fired.
|
|
40
|
+
//
|
|
41
|
+
// Counting the JS-ARMED sources is the point: a source armed from JS is
|
|
42
|
+
// program work the process must survive for, while a source armed inside C —
|
|
43
|
+
// GDK's frame clock, GIO's portal/a11y retries — is library plumbing that
|
|
44
|
+
// `gjs -m` would never stay alive for either. Keying the keep-alive on "the
|
|
45
|
+
// context has ANY scheduled source" instead made a finished GTK program
|
|
46
|
+
// immortal: GDK leaves a ~1 s repeating timeout armed, so the context reports
|
|
47
|
+
// work forever while nothing is ever ready to dispatch.
|
|
35
48
|
bool pumpCounted = false;
|
|
36
49
|
};
|
|
37
50
|
|
|
@@ -301,10 +314,14 @@ static NodeGiCallback* CreateCallback(Napi::Env env, Napi::Function fn, GICallab
|
|
|
301
314
|
cb->env = env;
|
|
302
315
|
cb->info = reinterpret_cast<GICallableInfo*>(gi_base_info_ref(callbackInfo));
|
|
303
316
|
cb->scope = scope;
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
317
|
+
// Count the JS-armed GLib work in the auto-pump keep-alive so the process
|
|
318
|
+
// stays alive until it completes: a one-shot in-flight operation
|
|
319
|
+
// (GAsyncReadyCallback et al, scope=async) or a source GLib owns until its
|
|
320
|
+
// destroy-notify (`GLib.timeout_add`/`idle_add`, scope=notified). See the
|
|
321
|
+
// `pumpCounted` note above for why JS-armed is the right unit.
|
|
322
|
+
if (scope == GI_SCOPE_TYPE_ASYNC || scope == GI_SCOPE_TYPE_NOTIFIED) {
|
|
323
|
+
cb->pumpCounted = NodeGiPumpAsyncBegin(env);
|
|
324
|
+
}
|
|
308
325
|
napi_create_reference(env, fn, 1, &cb->jsFn);
|
|
309
326
|
cb->closure = gi_callable_info_create_closure(callbackInfo, &cb->cif, NodeGiCallbackTrampoline, cb);
|
|
310
327
|
cb->native = gi_callable_info_get_closure_native_address(callbackInfo, cb->closure);
|
package/src/common.h
CHANGED
|
@@ -635,6 +635,12 @@ Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info);
|
|
|
635
635
|
// loop.cc
|
|
636
636
|
Napi::Value StartMainLoop(const Napi::CallbackInfo& info);
|
|
637
637
|
Napi::Value IterateMainContext(const Napi::CallbackInfo& info);
|
|
638
|
+
Napi::Value MainContextHasPending(const Napi::CallbackInfo& info);
|
|
639
|
+
// Factory for an Int32Array(1) view over the JS-armed-work counter (see
|
|
640
|
+
// loop.cc) — lets the L1 pump answer "does the program own GLib work?" WITHOUT
|
|
641
|
+
// a napi call. Lazy (a factory, not an Init-time export): the @gjsify/napi shim
|
|
642
|
+
// loud-stubs napi_create_external_arraybuffer and never arms the pump.
|
|
643
|
+
Napi::Value MakePumpPendingCount(const Napi::CallbackInfo& info);
|
|
638
644
|
Napi::Value PumpKick(const Napi::CallbackInfo& info);
|
|
639
645
|
Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info);
|
|
640
646
|
|
|
@@ -643,8 +649,10 @@ Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info);
|
|
|
643
649
|
// In-flight scope=async keep-alive: while a GI scope=async callback (e.g. a
|
|
644
650
|
// GAsyncReadyCallback) is pending, the pump holds a libuv ref so a plain Node
|
|
645
651
|
// script survives until the completion dispatches (the top-level-await case).
|
|
646
|
-
//
|
|
647
|
-
//
|
|
652
|
+
// The counter itself is runtime-independent (Bun/Deno's portable pump reads it
|
|
653
|
+
// via MainContextHasPending); only the libuv ref is Node-only. Begin returns
|
|
654
|
+
// whether the callback was counted (false only for a foreign env, i.e. a worker
|
|
655
|
+
// thread once Node's main env owns the pump); the caller must call End exactly
|
|
648
656
|
// once for each counted callback. Main-thread only.
|
|
649
657
|
bool NodeGiPumpAsyncBegin(napi_env env);
|
|
650
658
|
void NodeGiPumpAsyncEnd();
|
package/src/loop.cc
CHANGED
|
@@ -550,14 +550,31 @@ static void PumpArmWakeups() {
|
|
|
550
550
|
if (g_pump_async_pending > 0 && (timeout < 0 || timeout > 32)) timeout = 32;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
// WAKE-UP and HOLD are separate decisions.
|
|
554
|
+
//
|
|
555
|
+
// Wake-up: whenever GLib has a deadline, arm the mirrored uv timer for it, so
|
|
556
|
+
// libuv's poll sleep ends exactly when GLib next has work.
|
|
557
|
+
//
|
|
558
|
+
// Hold (uv_ref): ONLY while JS-armed GLib work is outstanding — the
|
|
559
|
+
// scope=async/notified counter. Ref'ing whenever a deadline exists (what this
|
|
560
|
+
// used to do) makes any C-armed toolkit timer immortalize the process: GDK
|
|
561
|
+
// keeps a ~1 s repeating timeout armed for the process's whole life, so
|
|
562
|
+
// `node --test test/gtk-smoke.test.mjs` never exited once the GTK stack had
|
|
563
|
+
// been initialized. `gjs -m` exits when the module settles regardless of what
|
|
564
|
+
// GLib still has scheduled; keying the hold on the program's OWN outstanding
|
|
565
|
+
// GLib work is as close to that as a libuv-driven process can get, and it is
|
|
566
|
+
// what keeps a top-level `await` on a GLib timeout alive (the timeout's
|
|
567
|
+
// GSourceFunc is a scope=notified callback, counted until GLib drops it).
|
|
553
568
|
if (timeout >= 0) {
|
|
554
569
|
uv_timer_start(&g_pump_timer, PumpTimerCb, static_cast<uint64_t>(timeout), 0);
|
|
555
|
-
|
|
556
|
-
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
557
|
-
g_pump_timer_reffed = TRUE;
|
|
558
|
-
}
|
|
559
|
-
} else if (g_pump_timer_reffed) {
|
|
570
|
+
} else {
|
|
560
571
|
uv_timer_stop(&g_pump_timer);
|
|
572
|
+
}
|
|
573
|
+
const gboolean hold = g_pump_async_pending > 0 && timeout >= 0;
|
|
574
|
+
if (hold && !g_pump_timer_reffed) {
|
|
575
|
+
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
576
|
+
g_pump_timer_reffed = TRUE;
|
|
577
|
+
} else if (!hold && g_pump_timer_reffed) {
|
|
561
578
|
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
562
579
|
g_pump_timer_reffed = FALSE;
|
|
563
580
|
}
|
|
@@ -632,15 +649,72 @@ static void PumpInit(uv_loop_t* loop) {
|
|
|
632
649
|
g_pump_inited = TRUE;
|
|
633
650
|
}
|
|
634
651
|
|
|
652
|
+
// mainContextHasPending() -> boolean
|
|
653
|
+
//
|
|
654
|
+
// Is there JS-armed GLib work a *pumping* runtime must stay alive for? True
|
|
655
|
+
// while a GI callback the program handed to GLib is outstanding — a scope=async
|
|
656
|
+
// completion (a GAsyncReadyCallback) or a scope=notified source
|
|
657
|
+
// (`GLib.timeout_add`/`idle_add`), counted in calls.cc and released when GLib
|
|
658
|
+
// drops it. The query half of the keep-alive contract for the Bun/Deno pump
|
|
659
|
+
// (L1 `startMainContextPump`); Node reads the same counter inline in
|
|
660
|
+
// PumpArmWakeups and never calls this.
|
|
661
|
+
//
|
|
662
|
+
// It deliberately does NOT ask whether the context has any scheduled source.
|
|
663
|
+
// That probe (`g_main_context_prepare` + `_query`, timeout >= 0) reports the
|
|
664
|
+
// timers a TOOLKIT arms in C as well, and GDK keeps a ~1 s repeating timeout
|
|
665
|
+
// armed for the process's whole life: a finished GTK program then answers
|
|
666
|
+
// "pending" forever while nothing is ever ready to dispatch, and never exits.
|
|
667
|
+
// `gjs -m` would not stay alive for those either — it exits once the module
|
|
668
|
+
// settles. A `false` answer is also what lets a sync-only program exit
|
|
669
|
+
// immediately: the pump alone must never keep a finished program alive.
|
|
670
|
+
Napi::Value MainContextHasPending(const Napi::CallbackInfo& info) {
|
|
671
|
+
Napi::Env env = info.Env();
|
|
672
|
+
return Napi::Boolean::New(env, g_pump_async_pending > 0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// makePumpPendingCount() -> Int32Array(1) OVER g_pump_async_pending (external,
|
|
676
|
+
// zero copy). The JS pump's beat reads this view instead of calling
|
|
677
|
+
// MainContextHasPending(): on Deno, merely ENTERING the addon from the pump's
|
|
678
|
+
// timer tick during the between-test-files GC window reproduces the #47
|
|
679
|
+
// boxed-handle teardown SIGSEGV — measured on the gtk-smoke leg, a query-only
|
|
680
|
+
// tick (one napi call, no dispatch) crashed 3/3 while a tick that never touches
|
|
681
|
+
// the addon exited 0. A typed-array read is a plain JS memory access, so the
|
|
682
|
+
// beat's idle path stays native-silent. No atomics: the counter is written only
|
|
683
|
+
// from the JS thread (Begin/End are main-thread only) and read from JS.
|
|
684
|
+
//
|
|
685
|
+
// A FACTORY, deliberately not created at addon Init: the @gjsify/napi shim
|
|
686
|
+
// (the gjs-napi conformance oracle runs node-gi under it) loud-stubs
|
|
687
|
+
// napi_create_external_arraybuffer, and the gjs host never arms the portable
|
|
688
|
+
// pump — L1 calls this lazily on the first Bun/Deno pump arm, so the stub is
|
|
689
|
+
// never reached where the view is never needed.
|
|
690
|
+
Napi::Value MakePumpPendingCount(const Napi::CallbackInfo& info) {
|
|
691
|
+
Napi::Env env = info.Env();
|
|
692
|
+
static_assert(sizeof(g_pump_async_pending) == sizeof(int32_t),
|
|
693
|
+
"the JS Int32Array view requires a 32-bit counter");
|
|
694
|
+
// External buffer over static storage — no finalizer needed, the counter
|
|
695
|
+
// outlives every env.
|
|
696
|
+
Napi::ArrayBuffer buf =
|
|
697
|
+
Napi::ArrayBuffer::New(env, &g_pump_async_pending, sizeof(g_pump_async_pending));
|
|
698
|
+
return Napi::TypedArrayOf<int32_t>::New(env, 1, buf, 0, napi_int32_array);
|
|
699
|
+
}
|
|
700
|
+
|
|
635
701
|
// In-flight scope=async keep-alive: while any GI scope=async callback (a
|
|
636
702
|
// GAsyncReadyCallback) is pending, REF the (always-active) prepare handle so the
|
|
637
703
|
// libuv loop stays alive until the completion arrives — the exact analogue of
|
|
638
704
|
// Node's own in-flight I/O keeping the process alive, and the reason a plain
|
|
639
705
|
// top-level `await` on a Gio async op settles instead of exiting with an
|
|
640
|
-
// unsettled-TLA error.
|
|
706
|
+
// unsettled-TLA error.
|
|
707
|
+
//
|
|
708
|
+
// The COUNTER is maintained on every runtime, not just Node: it is the
|
|
709
|
+
// "in-flight async work" half of the keep-alive contract, and Bun/Deno's
|
|
710
|
+
// portable pump reads it through MainContextHasPending() to decide whether its
|
|
711
|
+
// timer holds the runtime's event loop open. Only the uv ref/unref half is
|
|
712
|
+
// Node-only (it needs the uv handles the uv pump owns). Main-thread only —
|
|
713
|
+
// where a pump env is known (Node), a foreign env is not counted.
|
|
641
714
|
bool NodeGiPumpAsyncBegin(napi_env env) {
|
|
642
|
-
if (
|
|
643
|
-
|
|
715
|
+
if (g_loop_env != nullptr && env != g_loop_env) return false;
|
|
716
|
+
++g_pump_async_pending;
|
|
717
|
+
if (g_pump_inited && g_pump_async_pending == 1 && !g_pump_prepare_reffed) {
|
|
644
718
|
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
645
719
|
g_pump_prepare_reffed = TRUE;
|
|
646
720
|
}
|
|
@@ -648,8 +722,9 @@ bool NodeGiPumpAsyncBegin(napi_env env) {
|
|
|
648
722
|
}
|
|
649
723
|
|
|
650
724
|
void NodeGiPumpAsyncEnd() {
|
|
651
|
-
if (
|
|
652
|
-
|
|
725
|
+
if (g_pump_async_pending == 0) return;
|
|
726
|
+
--g_pump_async_pending;
|
|
727
|
+
if (g_pump_inited && g_pump_async_pending == 0 && g_pump_prepare_reffed) {
|
|
653
728
|
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
654
729
|
g_pump_prepare_reffed = FALSE;
|
|
655
730
|
}
|