@gjsify/node-gi 0.13.0 → 0.20.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 CHANGED
@@ -16,11 +16,24 @@
16
16
  // flags, constants, structs/boxed, interface static methods and camelCase
17
17
  // aliases land in subsequent drops.
18
18
  import * as native from './index.js';
19
+ // setImmediate is a Node global but NOT a Deno global (Deno requires the explicit
20
+ // node:timers import; Bun/Node re-export it there too), so import it for the
21
+ // macrotask-deferred runAsync to work on all three runtimes.
22
+ import { setImmediate } from 'node:timers';
23
+ // The GJS Gio DBus surface (Gio.DBus.session/system, own_name/watch_name,
24
+ // Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject) — ported to the L1 model.
25
+ import { createGioDBus } from './overrides/gio-dbus.js';
19
26
 
20
27
  // Symbol carrying the raw native GObject handle on a wrapped instance, so it can
21
28
  // be unwrapped again when passed back into the engine as a GI argument.
22
29
  const HANDLE = Symbol('nodeGiHandle');
23
30
 
31
+ // Symbol carrying the user-class prototype (a registerClass subclass) on a wrapped
32
+ // instance. Stored on the target rather than captured in the proxy closure so a
33
+ // later wrap with a userProto can UPGRADE an already-cached generic wrapper in
34
+ // place (preserving identity + expandos) — see wrapInstance.
35
+ const USER_PROTO = Symbol('nodeGiUserProto');
36
+
24
37
  // JS-builtin / interop names that must NEVER be treated as a GI method or
25
38
  // property — otherwise awaiting, printing or inspecting a wrapper would call
26
39
  // into GObject (e.g. a stray `then` would make a wrapper look thenable).
@@ -37,10 +50,34 @@ function toKebab(name) {
37
50
  return name.replace(/([A-Z])/g, '-$1').replace(/_/g, '-').toLowerCase();
38
51
  }
39
52
 
53
+ // Per-user-function shim cache so repeated passes of the same callback marshal to
54
+ // the SAME native-facing function (stable identity, one ffi/GClosure wrapper per
55
+ // user fn per call site pattern).
56
+ const callbackShims = new WeakMap();
57
+
58
+ // A JS function crossing INTO the engine (a GI callback arg, or a JS fn
59
+ // marshalled as a GClosure IN-arg) is wrapped so that when C invokes it:
60
+ // • each native arg is marshalled through wrapReturn — a GObject handle becomes
61
+ // the cached chainable instance, a GVariant a GLib.Variant wrapper, a GParamSpec
62
+ // a ParamSpec wrapper — matching what GJS hands a callback;
63
+ // • the JS return is unwrapped back to its native handle (a GLib.Variant
64
+ // wrapper returned from e.g. a DBus get-property closure must reach the
65
+ // engine as the raw variant handle).
66
+ // Primitives and plain objects pass through both directions untouched.
67
+ function wrapCallbackFn(fn) {
68
+ let shim = callbackShims.get(fn);
69
+ if (shim === undefined) {
70
+ shim = (...args) => unwrapArg(fn(...args.map(wrapReturn)));
71
+ callbackShims.set(fn, shim);
72
+ }
73
+ return shim;
74
+ }
75
+
40
76
  function unwrapArg(value) {
41
77
  if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
42
78
  return value[HANDLE];
43
79
  }
80
+ if (typeof value === 'function') return wrapCallbackFn(value);
44
81
  return value;
45
82
  }
46
83
 
@@ -48,66 +85,1125 @@ function unwrapArgs(args) {
48
85
  return args.map(unwrapArg);
49
86
  }
50
87
 
88
+ // Normalize a construct-property dict: each KEY to the GObject canonical (kebab)
89
+ // property name and each VALUE unwrapped to its native handle. GJS accepts a
90
+ // construct-prop key in camelCase, snake_case OR already-dashed form and maps it to
91
+ // the GObject property; the native newObject/constructType layer below looks each
92
+ // key up against the GParamSpec by its canonical dashed name, so `{maximumSize:400}`
93
+ // would otherwise miss `maximum-size`. Reuse `toKebab` — the SAME normalization the
94
+ // property getter/setter accessor path already applies (lines below: `x.maximumSize`
95
+ // reads/writes `maximum-size`) — keeping a single source of truth, so construction
96
+ // and accessor agree. Idempotent: an already-dashed (`maximum-size`) or snake_case
97
+ // (`maximum_size`) key passes through unchanged (GObject canonicalizes `_`↔`-`).
98
+ // This is the ONLY caller-facing construct path — newObject for an introspected
99
+ // `new Ns.Class({...})`, and constructType for a registerClass'd subclass plus its
100
+ // `super({...})` chain-up — so all three accept camelCase identically to GJS.
51
101
  function unwrapProps(props) {
52
102
  const out = {};
53
- for (const key of Object.keys(props)) out[key] = unwrapArg(props[key]);
103
+ for (const key of Object.keys(props)) out[toKebab(key)] = unwrapArg(props[key]);
54
104
  return out;
55
105
  }
56
106
 
107
+ // Symbol carrying an enum object's GError-domain descriptor ({ name, quark }) so
108
+ // GLib.Error.prototype.matches can resolve an error-enum (e.g. Gio.IOErrorEnum)
109
+ // to the domain it represents. Attached by makeEnum when the enum is registered
110
+ // as a GError domain; read by errorDomainOf.
111
+ const ERROR_DOMAIN = Symbol('nodeGiErrorDomain');
112
+
113
+ // ---- GLib.Error (the GError surface, L1) ----
114
+ //
115
+ // A real Error subclass mirroring GJS's GLib.Error: `.domain`, `.code`,
116
+ // `.message` plus `.matches(domain, code)` (the g_error_matches semantics — true
117
+ // when the error's domain AND code both match). The engine throws an instance of
118
+ // this on a failed sync GI invoke (via the builder registered below), and
119
+ // `requireGi('GLib').Error` is this constructor, so a caught error is
120
+ // `instanceof GLib.Error` exactly as under GJS.
121
+ //
122
+ // Divergence from GJS (documented): GJS's `.domain` is the numeric GQuark; here
123
+ // `.domain` is the quark NAME string (more useful in a Node context, and the task
124
+ // permits either). The numeric quark is kept on `.domainQuark`. `matches()`
125
+ // accepts the domain as an error-enum object (Gio.IOErrorEnum), a name string, or
126
+ // a numeric quark, so all the idiomatic call shapes work.
127
+
128
+ // Resolve a `matches` domain argument to a { name?, quark? } descriptor.
129
+ function errorDomainOf(domain) {
130
+ if (domain === null || domain === undefined) return null;
131
+ if (typeof domain === 'object' && domain[ERROR_DOMAIN] !== undefined) return domain[ERROR_DOMAIN];
132
+ if (typeof domain === 'string') return { name: domain };
133
+ if (typeof domain === 'number') return { quark: domain };
134
+ return null;
135
+ }
136
+
137
+ class GLibError extends Error {
138
+ // GJS-shaped public constructor: `new GLib.Error(domain, code, message)` where
139
+ // `domain` may be an error-enum object, a quark name string, or a numeric quark.
140
+ constructor(domain, code, message) {
141
+ super(typeof message === 'string' ? message : '');
142
+ this.name = 'GLib.Error';
143
+ const d = errorDomainOf(domain);
144
+ this.domain = d !== null && d.name !== undefined ? d.name : typeof domain === 'string' ? domain : undefined;
145
+ this.domainQuark = d !== null && d.quark !== undefined ? d.quark : typeof domain === 'number' ? domain : undefined;
146
+ this.code = code;
147
+ }
148
+
149
+ // g_error_matches: true when the error's domain + code both match. Accepts the
150
+ // domain as an error-enum object / name string / numeric quark (errorDomainOf).
151
+ matches(domain, code) {
152
+ const d = errorDomainOf(domain);
153
+ if (d === null) return false;
154
+ const domainMatch =
155
+ (d.name !== undefined && d.name === this.domain) ||
156
+ (d.quark !== undefined && d.quark === this.domainQuark);
157
+ return domainMatch && code === this.code;
158
+ }
159
+
160
+ toString() {
161
+ return `GLib.Error ${this.domain}: ${this.message}`;
162
+ }
163
+ }
164
+
165
+ // The engine calls this on a failed GI invoke with the GError's authoritative
166
+ // fields (quark name + numeric quark + code + message) to build the thrown error.
167
+ function buildGError(domainName, domainQuark, code, message) {
168
+ const error = new GLibError(domainName, code, message);
169
+ error.domain = domainName;
170
+ error.domainQuark = domainQuark;
171
+ return error;
172
+ }
173
+ native.setErrorBuilder(buildGError);
174
+
175
+ // Symbol stamping a makeClass prototype with its { namespace, typeName }, so
176
+ // Gio._promisify can record WHICH introspected class a registration belongs to
177
+ // (introspected instances have no live prototype chain to resolve by). Read in
178
+ // _promisify; matched against the instance's GType via native.isInstanceOf.
179
+ const CLASS_INFO = Symbol('nodeGiClassInfo');
180
+
181
+ // Promisified async methods, keyed by GI method name (snake_case) → an array of
182
+ // registrations `{ namespace?, typeName?, wrapper }`. Introspected-class instances
183
+ // resolve methods dynamically (no prototype chain), so the instance proxy consults
184
+ // this registry. Usually one registration per name (fast path); when two classes
185
+ // promisify the SAME method name, the instance proxy picks the registration whose
186
+ // class the instance is-a (native.isInstanceOf). See _promisify / wrapInstance.
187
+ const promisifiedMethods = new Map();
188
+
57
189
  // Object-typed return values become chainable instance proxies; boxed/struct
58
190
  // handles (e.g. GMainLoop) become method-routing proxies; everything else
59
191
  // (primitives, strings, null) passes through.
60
192
  function wrapReturn(value) {
61
193
  if (native.isGObjectHandle(value)) return wrapInstance(value);
194
+ // A GLib.Variant is ALSO a boxed handle (by tag), so it must be checked first
195
+ // to give it the Variant ergonomics rather than a plain method-routing proxy.
196
+ if (native.isVariantHandle(value)) return wrapVariant(value);
197
+ // A GParamSpec is a distinct GObject fundamental (tagged separately from boxed),
198
+ // surfaced with the GObject.ParamSpec ergonomics (name/value_type/get_name()/…).
199
+ if (native.isParamSpecHandle(value)) return wrapParamSpec(value);
200
+ // A non-GObject GObject-fundamental (e.g. a GskRenderNode from Gtk.Snapshot.to_node):
201
+ // an opaque, round-trippable pass-through handle (see wrapFundamental).
202
+ if (native.isFundamentalHandle(value)) return wrapFundamental(value);
62
203
  if (native.isBoxedHandle(value)) return wrapBoxed(value);
204
+ // A multi-value OUT tuple (return value + OUT params) — or a container OUT — is a
205
+ // plain JS Array whose ELEMENTS may themselves be GObject/boxed/variant handles
206
+ // (e.g. GLib.Regex.match → [matched, GLib.MatchInfo]; an array-of-objects OUT).
207
+ // Recurse so a handle sitting inside the tuple is wrapped into a usable proxy,
208
+ // not left as a raw External. A Node Buffer (byte array) is NOT Array.isArray, so
209
+ // it passes through untouched; primitives/strings map to themselves.
210
+ if (Array.isArray(value)) return value.map(wrapReturn);
63
211
  return value;
64
212
  }
65
213
 
66
- // Wrap a boxed/struct handle so its methods are callable GJS-style
67
- // (`mainLoop.run()`, `mainLoop.quit()`, snake_case or camelCase). Boxed types
68
- // have no GObject properties/signals, so only method routing is provided.
214
+ // Wrap a user's signal callback so each native signal argument is passed through
215
+ // {@link wrapReturn} a GObject arg becomes a chainable instance, a GVariant
216
+ // arg (e.g. the value on Gio.SimpleAction::change-state) becomes a GLib.Variant
217
+ // wrapper, primitives/plain objects pass through. GJS parity: the engine prepends
218
+ // the EMITTER (the object the signal fired on) as the first arg, so a positional
219
+ // handler `(obj, …params) => …` binds correctly; the emitter is itself a GObject
220
+ // arg, so wrapReturn turns it into the cached, toggle-ref-canonical instance. For
221
+ // `notify::x` the args are (object, pspec).
222
+ function wrapSignalCallback(cb) {
223
+ return (...args) => cb(...args.map(wrapReturn));
224
+ }
225
+
226
+ // Resolve a Gtk.Template `<signal handler="…">` handler NAME to a dispatcher that
227
+ // calls the instance's bound JS method, for the native template-callback scope
228
+ // (engine: NodeGiScopeCreateClosure via set_template_scope). Mirrors GJS's
229
+ // `_createClosure` (refs/gjs/modules/core/overrides/Gtk.js).
230
+ //
231
+ // LAZY by necessity: the engine resolves template signals during the C-side
232
+ // constructType (init_template → create_closure), which runs BEFORE this layer
233
+ // attaches the user-class prototype to the instance proxy. So the dispatcher
234
+ // defers the method lookup to FIRE time, by which point the instance proxy is the
235
+ // full, userProto-carrying, toggle-ref-canonical L1 wrapper. `this` inside the
236
+ // handler is therefore the widget — the SAME cached proxy the user constructed
237
+ // (wrapInstance is cached by the canonical handle). Native signal args are wrapped
238
+ // (wrapReturn) into chainable instances, exactly like wrapSignalCallback. GJS
239
+ // parity: the engine prepends the emitter at param 0, so a template handler
240
+ // `on_click(button)` receives the emitter first, then the signal's own params. A
241
+ // handler name with no matching user-prototype method throws a clear error when
242
+ // the signal fires.
243
+ function resolveTemplateCallback(handle, handlerName) {
244
+ return (...args) => {
245
+ const proxy = wrapInstance(handle);
246
+ const userProto = proxy[USER_PROTO];
247
+ const desc = userProto !== undefined ? findProtoDescriptor(userProto, handlerName) : undefined;
248
+ if (desc === undefined || typeof desc.value !== 'function') {
249
+ throw new Error(
250
+ `Gtk.Template: signal handler '${handlerName}' is not defined on ${proxy.constructor?.name ?? 'the instance'}`,
251
+ );
252
+ }
253
+ return desc.value.apply(proxy, args.map(wrapReturn));
254
+ };
255
+ }
256
+ native.setTemplateCallbackResolver(resolveTemplateCallback);
257
+
258
+ // Wrap a non-GObject GObject-fundamental (e.g. a GskRenderNode from
259
+ // Gtk.Snapshot.to_node, a GdkEvent) as an OPAQUE, round-trippable handle. The
260
+ // native tagged External already carries the raw pointer (its Data) + drops the
261
+ // held ref on GC via the type's introspected unref func, so all L1 has to do is
262
+ // expose it under HANDLE — then unwrapArg feeds it straight back into a
263
+ // fundamental-typed IN arg (e.g. Gsk.Renderer.render_texture(node, …)), where the
264
+ // OBJECT External branch reads the pointer. Method dispatch on fundamentals is a
265
+ // documented follow-up; today they are pass-through intermediates (build → hand to
266
+ // a consumer → drop), which is what the GSK screenshot/render path needs.
267
+ function wrapFundamental(handle) {
268
+ return { [HANDLE]: handle, [Symbol.toStringTag]: 'GIFundamental' };
269
+ }
270
+
271
+ // Wrap a boxed/struct/union handle so its methods are callable GJS-style
272
+ // (`mainLoop.run()`, snake_case or camelCase) AND its FIELDS are readable/writable
273
+ // as plain properties (`simpleStruct.long_`, `union.long_ = 5`). Resolution rule
274
+ // (VERIFIED against gjs 1.88 find_unique_js_field_name): a name that is BOTH a
275
+ // method and a field resolves to the METHOD — gjs renames the colliding field to
276
+ // `_name`. So the native boxedMemberKind checks methods first: 1 = method (a
277
+ // callable dispatcher), 2 = field (read via getBoxedField / write via
278
+ // setBoxedField), 0 = neither (fall back to a method dispatcher, so an unknown
279
+ // name still throws a clear "no method" at call time — the pre-fields behaviour).
69
280
  function wrapBoxed(handle) {
70
281
  const target = { [HANDLE]: handle };
282
+ const methodDispatch = (prop) => (...args) =>
283
+ wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
71
284
  return new Proxy(target, {
72
285
  get(t, prop) {
73
286
  if (prop === HANDLE) return handle;
74
287
  if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
288
+ // A field set through the set trap below is stored as an expando on the
289
+ // target (gjs allows writing a boxed field that GI declares unwritable to a
290
+ // JS expando); surface it back before consulting introspection.
291
+ if (Object.hasOwn(t, prop)) return t[prop];
292
+ // GBytes convenience: `.toArray()` → the raw bytes as a Uint8Array (Node
293
+ // Buffer). GBytes has no introspected `to_array`; gjs adds it to
294
+ // GLib.Bytes.prototype (imports._byteArrayNative.fromGBytes), so mirror it
295
+ // via the introspected get_data(). Gated on the boxed type so it never
296
+ // shadows a same-named method on some other struct.
297
+ if (prop === 'toArray' && native.boxedTypeName(handle) === 'GBytes') {
298
+ return () => wrapReturn(native.callBoxedMethod(handle, 'get_data', []));
299
+ }
300
+ const snake = camelToSnake(prop);
301
+ // boxedMemberKind: 0 = not a member (type info resolved) → `undefined`, so
302
+ // `typeof boxed.noSuchName === 'undefined'` matches gjs (a fabricated
303
+ // dispatcher would read `'function'` and break JS duck-typing such as
304
+ // `if (typeof value.toArray === 'function')`); 1 = method; 2 = field; 3 =
305
+ // undecidable (unregistered struct, no static info) → keep the dispatcher
306
+ // fallback so a genuine method still resolves + throws a clear error at call.
307
+ const kind = native.boxedMemberKind(handle, snake);
308
+ if (kind === 2) return wrapReturn(native.getBoxedField(handle, snake));
309
+ if (kind === 0) return undefined;
310
+ return methodDispatch(prop);
311
+ },
312
+ set(t, prop, value) {
313
+ if (prop === HANDLE || typeof prop !== 'string' || RESERVED.has(prop)) {
314
+ return Reflect.set(t, prop, value);
315
+ }
316
+ const snake = camelToSnake(prop);
317
+ if (native.boxedMemberKind(handle, snake) === 2) {
318
+ native.setBoxedField(handle, snake, unwrapArg(value));
319
+ return true;
320
+ }
321
+ return Reflect.set(t, prop, value);
322
+ },
323
+ has(t, prop) {
324
+ if (prop === HANDLE || prop in t) return true;
325
+ if (typeof prop !== 'string') return false;
326
+ return native.boxedMemberKind(handle, camelToSnake(prop)) !== 0;
327
+ },
328
+ });
329
+ }
330
+
331
+ // Wrap a GParamSpec handle (phase 2.7a) with the GJS-shaped GObject.ParamSpec
332
+ // surface: `.name`/`.nick`/`.blurb`/`.flags`/`.value_type`/`.owner_type`/
333
+ // `.default_value` getters + the `get_name()`/`get_nick()`/`get_blurb()`/
334
+ // `get_default_value()` methods. This is what a `notify` handler's second arg and
335
+ // a GParamSpec-typed value now surface as (fixing the old `{name, valueType}`
336
+ // plain-object shape). Carries [HANDLE] so it can round-trip back into the engine
337
+ // and so `instanceof GObject.ParamSpec` recognises it (Symbol.hasInstance below).
338
+ // value_type/owner_type are native GType handles (like gjs's GType objects); the
339
+ // GTypes' `.name` reads their type name via the introspected surface.
340
+ function wrapParamSpec(handle) {
341
+ const prop = (which) => native.paramSpecProp(handle, which);
342
+ const pspec = {
343
+ [HANDLE]: handle,
344
+ get name() {
345
+ return prop('name');
346
+ },
347
+ get nick() {
348
+ return prop('nick');
349
+ },
350
+ get blurb() {
351
+ return prop('blurb');
352
+ },
353
+ get flags() {
354
+ return prop('flags');
355
+ },
356
+ get value_type() {
357
+ return prop('valueType');
358
+ },
359
+ get owner_type() {
360
+ return prop('ownerType');
361
+ },
362
+ get default_value() {
363
+ return wrapReturn(prop('defaultValue'));
364
+ },
365
+ get_name: () => prop('name'),
366
+ get_nick: () => prop('nick'),
367
+ get_blurb: () => prop('blurb'),
368
+ get_default_value: () => wrapReturn(prop('defaultValue')),
369
+ };
370
+ return pspec;
371
+ }
372
+
373
+ // ---- GLib.Variant ergonomics (the GJS GLib.Variant override, L1) ----
374
+ //
375
+ // Mirrors gjs/modules/core/overrides/GLib.js: `new GLib.Variant(sig, value)`
376
+ // recursively packs (native variantNew), and the wrapper exposes `.unpack()`,
377
+ // `.deepUnpack()`/`.deep_unpack`, `.recursiveUnpack()`, `.get_type_string()` and
378
+ // the GJS toString, plus routing of any other GVariant method (n_children,
379
+ // get_child_value, print, …) through the boxed-method engine. The deep-vs-
380
+ // recursive distinction is honoured in the native unpacker (variantUnpack):
381
+ // unpack() → variantUnpack(h, false, false): children stay Variants
382
+ // deepUnpack() → variantUnpack(h, true, false): one level; nested `v`
383
+ // values (e.g. an a{sv} value) STAY Variants
384
+ // recursiveUnpack() → variantUnpack(h, true, true): fully plain JS, no
385
+ // Variants (discards `v` type info)
386
+
387
+ // Re-wrap the result of variantUnpack: any GLib.Variant handle that the native
388
+ // unpacker left in place (an `a{sv}` value under deepUnpack, every child under
389
+ // unpack(), …) becomes a GLib.Variant wrapper; arrays/plain dicts are walked.
390
+ function wrapVariantResult(value) {
391
+ if (value === null || typeof value !== 'object') return value;
392
+ if (native.isVariantHandle(value)) return wrapVariant(value);
393
+ if (value instanceof Uint8Array) return value; // `ay` bytes
394
+ if (Array.isArray(value)) return value.map(wrapVariantResult);
395
+ const out = {};
396
+ for (const key of Object.keys(value)) out[key] = wrapVariantResult(value[key]);
397
+ return out;
398
+ }
399
+
400
+ // Wrap a boxed GLib.Variant handle with the GJS-shaped Variant surface. Carries
401
+ // [HANDLE] so it round-trips back into the engine as a GVariant IN argument
402
+ // (action.activate(variant), new_stateful state, change_state value, …).
403
+ function wrapVariant(handle) {
404
+ const target = { [HANDLE]: handle };
405
+ const api = {
406
+ unpack: () => wrapVariantResult(native.variantUnpack(handle, false, false)),
407
+ deepUnpack: () => wrapVariantResult(native.variantUnpack(handle, true, false)),
408
+ deep_unpack: () => wrapVariantResult(native.variantUnpack(handle, true, false)),
409
+ recursiveUnpack: () => native.variantUnpack(handle, true, true),
410
+ get_type_string: () => native.variantGetTypeString(handle),
411
+ toString: () => `[object variant of type "${native.variantGetTypeString(handle)}"]`,
412
+ };
413
+ return new Proxy(target, {
414
+ get(t, prop) {
415
+ if (prop === HANDLE) return handle;
416
+ if (typeof prop !== 'string') return t[prop];
417
+ if (Object.hasOwn(api, prop)) return api[prop];
418
+ if (RESERVED.has(prop)) return t[prop];
419
+ // Any other GVariant method (n_children, get_child_value, print, …).
75
420
  return (...args) =>
76
421
  wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
77
422
  },
78
423
  has(t, prop) {
79
- return prop === HANDLE || prop in t;
424
+ return prop === HANDLE || (typeof prop === 'string' && Object.hasOwn(api, prop)) || prop in t;
425
+ },
426
+ });
427
+ }
428
+
429
+ // Deep-unwrap a pack value so any nested GLib.Variant wrapper (carried at a `v`
430
+ // position, e.g. an a{sv} value) reaches the native packer as its raw handle —
431
+ // the native `v` case reads a raw boxed handle, not an L1 Proxy. Primitives,
432
+ // byte arrays, plain arrays and dict objects are walked; HANDLE-carrying
433
+ // wrappers collapse to their handle.
434
+ function unwrapVariantValue(value) {
435
+ if (value === null || typeof value !== 'object') return value;
436
+ if (value[HANDLE] !== undefined) return value[HANDLE];
437
+ if (value instanceof Uint8Array) return value; // `ay` bytes
438
+ if (Array.isArray(value)) return value.map(unwrapVariantValue);
439
+ const out = {};
440
+ for (const key of Object.keys(value)) out[key] = unwrapVariantValue(value[key]);
441
+ return out;
442
+ }
443
+
444
+ function packVariant(signature, value) {
445
+ return wrapVariant(native.variantNew(signature, unwrapVariantValue(value)));
446
+ }
447
+
448
+ // `GLib.Variant` as a class-like object: `new GLib.Variant(sig, value)` and the
449
+ // deprecated `GLib.Variant.new(sig, value)` both pack via the native engine.
450
+ function makeVariantClass() {
451
+ const ctor = function Variant(signature, value) {
452
+ return packVariant(signature, value);
453
+ };
454
+ Object.defineProperty(ctor, 'name', { value: 'Variant', configurable: true });
455
+ ctor.$gtypeName = 'GLib.Variant';
456
+ ctor.new = (signature, value) => packVariant(signature, value);
457
+ // `value instanceof GLib.Variant` — a wrapped Variant is a Proxy over a bare
458
+ // `{[HANDLE]}` target (no class prototype), so the default instanceof always
459
+ // returned false. Recognise any wrapper whose [HANDLE] is a native GVariant
460
+ // boxed handle. Real GAction/GSettings/GLib.log_structured code branches on it.
461
+ Object.defineProperty(ctor, Symbol.hasInstance, {
462
+ value(instance) {
463
+ if (instance === null || typeof instance !== 'object') return false;
464
+ const handle = instance[HANDLE];
465
+ return handle !== undefined && native.isVariantHandle(handle);
80
466
  },
467
+ configurable: true,
81
468
  });
469
+ return new Proxy(ctor, {
470
+ get(t, prop) {
471
+ if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
472
+ // Other static constructors (e.g. new_from_bytes) route through the engine.
473
+ const giName = camelToSnake(prop);
474
+ return (...args) =>
475
+ wrapReturn(native.callStaticMethod('GLib', 'Variant', giName, unwrapArgs(args)));
476
+ },
477
+ construct(_t, args) {
478
+ return packVariant(args[0], args[1]);
479
+ },
480
+ });
481
+ }
482
+
483
+ const variantClass = makeVariantClass();
484
+
485
+ // ---- GObject.Value ergonomics (the GJS GObject.Value override, L1) ----
486
+ //
487
+ // gjs exposes `new GObject.Value()` (an empty GValue) + the convenience
488
+ // `new GObject.Value(gtype, value)` (init + a set_* chosen by the value's type),
489
+ // over the introspected GValue struct methods (.init/.set_*/.get_*/.copy/.reset/
490
+ // .unset) + the static type_compatible/type_transformable —
491
+ // refs/gjs/modules/core/overrides/GObject.js gValueConstructorFunc over the real
492
+ // GValue class. node-gi has no g_value_new(), so construction goes through
493
+ // native.newGValue (a zeroed G_TYPE_VALUE boxed handle) and the method surface then
494
+ // routes through wrapBoxed's boxed-method dispatch (verified: init/set_int/get_int
495
+ // resolve as GValue methods).
496
+
497
+ // Fundamental GType name → GValue setter, for the `new GObject.Value(gtype, value)`
498
+ // convenience. Non-primitive types (enum/flags/boxed/object/param/variant) are
499
+ // matched by g_type_is_a below, exactly like gjs's default switch branch.
500
+ const GVALUE_PRIMITIVE_SETTERS = {
501
+ gboolean: 'set_boolean',
502
+ gchar: 'set_schar',
503
+ guchar: 'set_uchar',
504
+ gint: 'set_int',
505
+ guint: 'set_uint',
506
+ glong: 'set_long',
507
+ gulong: 'set_ulong',
508
+ gint64: 'set_int64',
509
+ guint64: 'set_uint64',
510
+ gfloat: 'set_float',
511
+ gdouble: 'set_double',
512
+ gchararray: 'set_string',
513
+ GType: 'set_gtype',
514
+ GVariant: 'set_variant',
515
+ GParam: 'set_param',
516
+ };
517
+
518
+ // Pick the GValue setter for a (resolved) GType handle, mirroring gjs's constructor
519
+ // switch: an exact fundamental → its setter, else g_type_is_a → flags/enum/boxed/…
520
+ // (introspected GObject.type_name / type_is_a — both verified on node-gi).
521
+ function gvalueSetterFor(gt) {
522
+ const G = requireGi('GObject', '2.0');
523
+ const prim = GVALUE_PRIMITIVE_SETTERS[G.type_name(gt)];
524
+ if (prim !== undefined) return prim;
525
+ if (G.type_is_a(gt, G.TYPE_FLAGS)) return 'set_flags';
526
+ if (G.type_is_a(gt, G.TYPE_ENUM)) return 'set_enum';
527
+ if (G.type_is_a(gt, G.TYPE_VARIANT)) return 'set_variant';
528
+ if (G.type_is_a(gt, G.TYPE_PARAM)) return 'set_param';
529
+ if (G.type_is_a(gt, G.TYPE_BOXED)) return 'set_boxed';
530
+ if (G.type_is_a(gt, G.TYPE_OBJECT)) return 'set_object';
531
+ throw new TypeError(`Invalid type argument '${G.type_name(gt)}' to the GObject.Value constructor`);
532
+ }
533
+
534
+ // Build the empty/convenience GValue: a zeroed GValue boxed handle wrapped with the
535
+ // GValue struct methods; the 2-arg form inits it to `gtype` (a class ctor's $gtype
536
+ // or a GType handle) and sets `value` via the matching setter.
537
+ function buildValue(args) {
538
+ const v = wrapBoxed(native.newGValue());
539
+ if (args.length >= 2) {
540
+ const gt = resolveGTypeArg(args[0]);
541
+ // Resolve the setter BEFORE init so an unsupported type throws a clean JS
542
+ // TypeError rather than driving g_value_init with a value-table-less type (a
543
+ // GLib-CRITICAL that would abort under G_DEBUG=fatal-criticals) — same net
544
+ // result as gjs's constructor, minus the noisy failed init.
545
+ const setter = gvalueSetterFor(gt);
546
+ v.init(gt);
547
+ v[setter](args[1]);
548
+ }
549
+ return v;
550
+ }
551
+
552
+ // `GObject.Value` as a class-like object: `new GObject.Value()` /
553
+ // `new GObject.Value(gtype, value)` construct via buildValue, static methods
554
+ // (type_compatible / type_transformable + camelCase) route through the engine, and
555
+ // `value instanceof GObject.Value` recognises any wrapper over a GValue boxed handle.
556
+ function makeValueClass() {
557
+ const ctor = function Value(...args) {
558
+ return buildValue(args);
559
+ };
560
+ Object.defineProperty(ctor, 'name', { value: 'Value', configurable: true });
561
+ ctor.$gtypeName = 'GObject.Value';
562
+ Object.defineProperty(ctor, Symbol.hasInstance, {
563
+ value(instance) {
564
+ if (instance === null || typeof instance !== 'object') return false;
565
+ const handle = instance[HANDLE];
566
+ return (
567
+ handle !== undefined &&
568
+ native.isBoxedHandle(handle) &&
569
+ native.boxedTypeName(handle) === 'GValue'
570
+ );
571
+ },
572
+ configurable: true,
573
+ });
574
+ return new Proxy(ctor, {
575
+ get(t, prop) {
576
+ if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
577
+ const giName = camelToSnake(prop);
578
+ return (...args) =>
579
+ wrapReturn(native.callStaticMethod('GObject', 'Value', giName, unwrapArgs(args)));
580
+ },
581
+ construct(_t, args) {
582
+ return buildValue(args);
583
+ },
584
+ });
585
+ }
586
+
587
+ const valueClass = makeValueClass();
588
+
589
+ // Overlay the GJS Variant ergonomics on the introspected GLib namespace, leaving
590
+ // every other member resolving from introspection. (Additive, like the GObject
591
+ // overlay; the introspected struct-based `GLib.Variant` is replaced by the
592
+ // ergonomic wrapper class so `new GLib.Variant(...)` + `.deepUnpack()` work.)
593
+ // The GLib names the L1 overlay adds/replaces on top of introspection.
594
+ // log_set_writer_func / log_set_writer_default mirror gjs's GLib.js overrides:
595
+ // gjs routes them through GjsPrivate (a C wrapper) because a GLogWriterFunc can
596
+ // fire on any thread and its GLogField array is not generically introspectable —
597
+ // node-gi's engine ships the same wrapper natively (logSetWriterFunc /
598
+ // logSetWriterDefault, src/private.cc). The JS writer receives
599
+ // (logLevel, fields) where fields is a plain object whose values are Uint8Arrays
600
+ // of the field bytes (or null for empty fields) — byte-for-byte the shape gjs's
601
+ // `{...stringFields.recursiveUnpack()}` produces (verified vs gjs 1.88).
602
+ const GLIB_OVERLAY_NAMES = new Set([
603
+ 'log_structured',
604
+ 'idle_add_once',
605
+ 'timeout_add_once',
606
+ 'timeout_add_seconds_once',
607
+ 'log_set_writer_func',
608
+ 'log_set_writer_default',
609
+ ]);
610
+
611
+ // GLib.log_structured(domain, level, fields): pack `fields` into an `a{sv}` and hand
612
+ // it to the introspected GLib.log_variant — refs/gjs/modules/core/overrides/GLib.js
613
+ // log_structured. Each field value is `ay` (Uint8Array), `s` (string) or a Variant
614
+ // passed through; anything else is a TypeError, matching gjs.
615
+ function makeLogStructured(baseNs) {
616
+ return (logDomain, logLevel, fields) => {
617
+ const variantFields = {};
618
+ for (const key of Object.keys(fields)) {
619
+ const field = fields[key];
620
+ if (field instanceof Uint8Array) variantFields[key] = variantClass('ay', field);
621
+ else if (typeof field === 'string') variantFields[key] = variantClass('s', field);
622
+ else if (field instanceof variantClass) variantFields[key] = field;
623
+ else {
624
+ throw new TypeError(
625
+ `Unsupported value ${field}, log_structured supports GLib.Variant, Uint8Array, and string values.`,
626
+ );
627
+ }
628
+ }
629
+ baseNs.log_variant(logDomain, logLevel, variantClass('a{sv}', variantFields));
630
+ };
631
+ }
632
+
633
+ function decorateGLibNamespace(baseNs) {
634
+ // Per-namespace cache of the overlay functions that need the introspected baseNs
635
+ // (log_structured → log_variant; the *_once helpers → idle_add/timeout_add).
636
+ const cache = new Map();
637
+ const overlay = (prop) => {
638
+ if (cache.has(prop)) return cache.get(prop);
639
+ let value;
640
+ if (prop === 'log_structured') value = makeLogStructured(baseNs);
641
+ // The one-shot idle/timeout conveniences (gjs GLib.js): auto-remove the source
642
+ // after the callback runs, so the callback needs no `return SOURCE_REMOVE`.
643
+ else if (prop === 'idle_add_once') {
644
+ value = (priority, callback) =>
645
+ baseNs.idle_add(priority, () => {
646
+ callback();
647
+ return baseNs.SOURCE_REMOVE;
648
+ });
649
+ } else if (prop === 'timeout_add_once') {
650
+ value = (priority, interval, callback) =>
651
+ baseNs.timeout_add(priority, interval, () => {
652
+ callback();
653
+ return baseNs.SOURCE_REMOVE;
654
+ });
655
+ } else if (prop === 'timeout_add_seconds_once') {
656
+ value = (priority, interval, callback) =>
657
+ baseNs.timeout_add_seconds(priority, interval, () => {
658
+ callback();
659
+ return baseNs.SOURCE_REMOVE;
660
+ });
661
+ } else if (prop === 'log_set_writer_func') {
662
+ // The gjs GLib.js shape: a non-function clears the JS writer (logs fall
663
+ // back to the default writer); a function becomes the structured-log
664
+ // writer. NOTE (GLib contract, identical under gjs): the underlying
665
+ // g_log_set_writer_func may only ever be installed ONCE per process — a
666
+ // second install aborts inside GLib itself.
667
+ value = (writerFunc) => {
668
+ if (typeof writerFunc !== 'function') native.logSetWriterFunc(null);
669
+ else native.logSetWriterFunc(writerFunc);
670
+ };
671
+ } else if (prop === 'log_set_writer_default') {
672
+ value = () => native.logSetWriterDefault();
673
+ }
674
+ cache.set(prop, value);
675
+ return value;
676
+ };
677
+ return new Proxy(baseNs, {
678
+ get(t, prop) {
679
+ if (prop === 'Variant') return variantClass;
680
+ // GLib.Error is the L1 GError subclass (the engine throws instances of it,
681
+ // and `new GLib.Error(domain, code, message)` constructs one), shadowing the
682
+ // introspected boxed type so `instanceof GLib.Error` + `.matches()` work.
683
+ if (prop === 'Error') return GLibError;
684
+ if (typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) return overlay(prop);
685
+ return t[prop];
686
+ },
687
+ has(t, prop) {
688
+ return (
689
+ prop === 'Variant' ||
690
+ prop === 'Error' ||
691
+ (typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) ||
692
+ prop in t
693
+ );
694
+ },
695
+ });
696
+ }
697
+
698
+ // Overlay the GJS Gio runtime statics on the introspected Gio namespace —
699
+ // additively. `_promisify` is the genuinely-new helper (refs/gjs Gio.js); the
700
+ // DBus surface (Gio.DBus, Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject)
701
+ // is built by createGioDBus (overrides/gio-dbus.js) over the introspected Gio +
702
+ // the ergonomic GLib. Every other member keeps resolving from introspection.
703
+ function decorateGioNamespace(baseNs) {
704
+ // Lazily construct the DBus surface: it needs the ergonomic GLib (`new
705
+ // GLib.Variant`), and building it only on first `Gio.DBus*` access keeps a plain
706
+ // `import Gio` (no DBus) from pulling GLib in.
707
+ let dbus;
708
+ const getDBus = () => {
709
+ if (dbus === undefined) {
710
+ dbus = createGioDBus({ Gio: baseNs, GLib: requireGi('GLib', '2.0'), unwrap: unwrapArg, native });
711
+ }
712
+ return dbus;
713
+ };
714
+ return new Proxy(baseNs, {
715
+ get(t, prop) {
716
+ if (prop === '_promisify') return promisify;
717
+ if (prop === 'DBus') return getDBus().DBus;
718
+ if (prop === 'DBusProxy') return getDBus().DBusProxy;
719
+ if (prop === 'DBusExportedObject') return getDBus().DBusExportedObject;
720
+ return t[prop];
721
+ },
722
+ has(t, prop) {
723
+ return (
724
+ prop === '_promisify' ||
725
+ prop === 'DBus' ||
726
+ prop === 'DBusProxy' ||
727
+ prop === 'DBusExportedObject' ||
728
+ prop in t
729
+ );
730
+ },
731
+ });
732
+ }
733
+
734
+ // Walk a JS prototype chain (excluding Object.prototype) for an own property
735
+ // descriptor of `prop` — used to surface a registerClass subclass's own methods
736
+ // /getters/setters on the GObject wrapper.
737
+ function findProtoDescriptor(proto, prop) {
738
+ for (let p = proto; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
739
+ const d = Object.getOwnPropertyDescriptor(p, prop);
740
+ if (d !== undefined) return d;
741
+ }
742
+ return undefined;
743
+ }
744
+
745
+ // Whether `proto` is the SAME as, or a more-derived descendant of, `maybeAncestor`
746
+ // (i.e. `maybeAncestor` lies on `proto`'s prototype chain). Used to decide whether a
747
+ // later wrapInstance userProto should UPGRADE a cached one: on a multi-level chain a
748
+ // vfunc firing during construction wraps the instance with the OVERRIDING ancestor's
749
+ // prototype before the leaf ctor wraps it with the leaf's, so the leaf (descendant)
750
+ // proto must be allowed to supersede the ancestor's — but never the reverse.
751
+ function isProtoSameOrDescendantOf(proto, maybeAncestor) {
752
+ for (let p = proto; p !== null; p = Object.getPrototypeOf(p)) {
753
+ if (p === maybeAncestor) return true;
754
+ }
755
+ return false;
756
+ }
757
+
758
+ // L1 proxy-identity cache (the user-visible half of the toggle-ref bridge). The
759
+ // native engine now returns the CANONICAL External per GObject (same GObject ⇒
760
+ // same handle), so we cache the per-instance Proxy keyed by that handle: the same
761
+ // GObject always yields the same L1 wrapper, so `===` holds at the ergonomic
762
+ // layer and a plain JS field set on a wrapper survives a round-trip + GC (the
763
+ // External is kept alive by the toggle-up root while C owns the object, which in
764
+ // turn keeps this WeakMap entry — and the proxy + its fields — alive).
765
+ const instanceCache = new WeakMap();
766
+
767
+ // ---- signal-handler bookkeeping (JS function → connected handler ids) ----
768
+ //
769
+ // node-gi connects signals through PRIVATE GClosures (a JS callback wrapped in a C
770
+ // closure — see signals.cc), so the GObject cannot map a JS function back to its
771
+ // handler ids the way gjs's signal_handler_find can. gjs reaches this via its own
772
+ // per-instance private-closure registry (Gi.signals_{block,unblock,disconnect}_
773
+ // symbol, refs/gjs/modules/core/overrides/GObject.js); we keep the equivalent map
774
+ // here, populated at connect() time, so GObject.signal_handlers_{block,unblock,
775
+ // disconnect}_by_func can resolve a function to its ids. Keyed by the canonical
776
+ // native handle (a WeakMap, so it is collected with the object). Divergence
777
+ // (documented): disconnecting a handler by a route OTHER than the L1 `.disconnect(id)`
778
+ // / a by-func disconnect (e.g. the introspected GObject.signal_handler_disconnect)
779
+ // leaves a stale id here; a following block/unblock/disconnect-by-func would then act
780
+ // on a dead id (native.disconnectSignal already guards is_connected; block/unblock
781
+ // would g_warning). Normal connect→block/unblock/disconnect flows are exact.
782
+ const signalHandlerIds = new WeakMap(); // handle → Map<jsFunc, Set<handlerId>>
783
+
784
+ function recordSignalHandler(handle, fn, id) {
785
+ let byFn = signalHandlerIds.get(handle);
786
+ if (byFn === undefined) {
787
+ byFn = new Map();
788
+ signalHandlerIds.set(handle, byFn);
789
+ }
790
+ let ids = byFn.get(fn);
791
+ if (ids === undefined) {
792
+ ids = new Set();
793
+ byFn.set(fn, ids);
794
+ }
795
+ ids.add(id);
796
+ }
797
+
798
+ // Drop a handler id from the per-instance registry (on disconnect), pruning an
799
+ // emptied function entry so a later by-func lookup reports zero matches.
800
+ function forgetSignalHandlerId(handle, id) {
801
+ const byFn = signalHandlerIds.get(handle);
802
+ if (byFn === undefined) return;
803
+ for (const [fn, ids] of byFn) {
804
+ if (ids.delete(id) && ids.size === 0) byFn.delete(fn);
805
+ }
806
+ }
807
+
808
+ // The connected handler ids for a given JS function on an instance (a fresh array;
809
+ // empty when the function was never connected on this instance).
810
+ function handlerIdsForFunc(handle, fn) {
811
+ const ids = signalHandlerIds.get(handle)?.get(fn);
812
+ return ids === undefined ? [] : [...ids];
813
+ }
814
+
815
+ // ---- G3 mutate-in-place registry ----
816
+ //
817
+ // registerClass registers the GType for the GIVEN class and returns that SAME
818
+ // class (GJS-faithful — refs/gjs/modules/core/overrides/GObject.js mutates `klass`
819
+ // and returns it), so both `const X = registerClass(…,C)` and
820
+ // `static { registerClass(…,C) }` (return discarded) leave the same symbol bound
821
+ // to the registered GType. The construction that produces the registered GType is
822
+ // moved to the introspected base ctor (makeClass), keyed on `new.target`: when
823
+ // `new Sub(args)` runs and `Sub` is registered here, the base ctor builds the
824
+ // registered GType via constructType + wraps it, and RETURNS that wrapper. Because
825
+ // a base constructor that returns an object substitutes `this` in every derived
826
+ // ctor (ES `super` semantics), this single routing point makes the whole super()
827
+ // chain construct the right GType, return the canonical toggle-ref wrapper carrying
828
+ // the leaf's prototype, and run every user ctor body against it — exactly how GJS
829
+ // instantiates `new.target`'s `$gtype` rather than the literal base. Keyed by the
830
+ // JS class → { typeHandle, children?, internalChildren? } (children move here from
831
+ // the old throwaway Subclass so init_template'd children are surfaced on the
832
+ // instance). Multi-level registered-of-registered chains (G2) are supported: the
833
+ // registry is also consulted by findParentGType to resolve a REGISTERED parent's
834
+ // GType handle (subclassing via native.registerClassFromGType), and the new.target
835
+ // routing keys on the leaf so construction is correct at any depth.
836
+ const registeredClasses = new Map();
837
+
838
+ // Surface a templated type's bound children on a freshly-constructed instance
839
+ // (GJS convention: public `this.<name>`, internal `this._<name>`, '-' → '_'). The
840
+ // engine already ran init_template during constructType, so getTemplateChild
841
+ // resolves each. Assigned through the proxy (set trap → stored as an instance
842
+ // expando), exactly as the old Subclass did.
843
+ function assignTemplateChildren(instance, handle, reg) {
844
+ if (reg.children !== undefined) {
845
+ for (const childName of reg.children) {
846
+ instance[childName.replace(/-/g, '_')] = wrapReturn(native.getTemplateChild(handle, childName));
847
+ }
848
+ }
849
+ if (reg.internalChildren !== undefined) {
850
+ for (const childName of reg.internalChildren) {
851
+ instance[`_${childName.replace(/-/g, '_')}`] = wrapReturn(native.getTemplateChild(handle, childName));
852
+ }
853
+ }
854
+ }
855
+
856
+ // ---- Gio.Application.runAsync (L1, GJS-shaped) ----
857
+ //
858
+ // The Node twin of GJS's `Gio.Application.prototype.runAsync`
859
+ // (refs/gjs/modules/core/overrides/Gio.js → `GLib.MainLoop.prototype.runAsync`
860
+ // in GLib.js). GJS returns a Promise and defers the blocking run() via
861
+ // `setMainLoopHook`, so the program's already-queued microtasks/promises settle
862
+ // before the GLib loop takes over the thread, then resolves with run()'s integer
863
+ // exit status when the app quits/exits.
864
+ //
865
+ // On Node there is no main-loop hook — libuv is always the process loop and the
866
+ // libuv↔GLib bridge (addon.cc UvLoopSource, attached to the DEFAULT GMainContext
867
+ // that g_application_run iterates) co-pumps libuv during a blocking run(). The
868
+ // faithful analogue is therefore to defer the blocking run() to a MACROTASK
869
+ // (setImmediate). This:
870
+ // • returns the Promise immediately — runAsync does NOT block the caller the
871
+ // way a bare `app.run()` would,
872
+ // • lets the caller's already-queued microtasks/promises drain first, and
873
+ // • runs run() OUTSIDE the caller's async/await scope, so the node-gtk
874
+ // #442/#121 nested-microtask-checkpoint caveat (see mainloop.test.mjs) does
875
+ // NOT bite: promise continuations queued before runAsync drain normally WHILE
876
+ // the app runs, because the bridge's microtask checkpoint is no longer
877
+ // deferred to an outer await frame.
878
+ //
879
+ // Mirrors GJS exactly in that the `argv` argument is ignored (GJS's runAsync
880
+ // takes no parameters and calls `this.run()` with an empty command-line) — so an
881
+ // `app.runAsync([programInvocationName, ...ARGV])` source behaves identically on
882
+ // gjs and node. Resolves once; run() is invoked once (no double-run / leak).
883
+ //
884
+ // Same shape on all three runtimes: the deferred blocking run() drives the app's
885
+ // GLib loop (activate → the app's own GLib sources → quit → run returns the exit
886
+ // status). The Node-only EXTRA is the uv↔GLib bridge co-pumping Node's own event
887
+ // loop DURING the blocking run, so a Node timer/promise scheduled before runAsync
888
+ // fires while the app runs. Bun/Deno have no such bridge, so their own event loop
889
+ // is paused for the app's lifetime (exactly as GJS, where the GLib loop IS the
890
+ // process loop) — for concurrent runtime-loop + GLib work there, drive GLib with
891
+ // startMainContextPump instead of a blocking run.
892
+ function applicationRunAsync(handle) {
893
+ return new Promise((resolve, reject) => {
894
+ setImmediate(() => {
895
+ try {
896
+ resolve(wrapReturn(native.callMethod(handle, 'run', unwrapArgs([[]]))));
897
+ } catch (error) {
898
+ reject(error);
899
+ }
900
+ });
901
+ });
902
+ }
903
+
904
+ // ---- portable GLib main-context pump (Bun/Deno) ----
905
+ //
906
+ // On Node the uv-in-GLib bridge co-pumps libuv during a blocking GLib loop, so a
907
+ // GLib loop keeps Node's timers/promises/IO alive. Bun/Deno have no usable libuv,
908
+ // so we co-pump the OTHER way: a repeating runtime timer iterates the default GLib
909
+ // main context non-blockingly, letting GIO async callbacks / GLib timeouts / DBus
910
+ // fire while the runtime's own event loop stays in control — GJS's non-blocking
911
+ // main loop, reached from the runtime side. Reference-counted so nested pumps /
912
+ // concurrent runAsync calls share one timer.
913
+ let pumpTimer = null;
914
+ let pumpRefCount = 0;
915
+
916
+ /**
917
+ * Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
918
+ * No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
919
+ * returns a disposer that decrements the count (clearing the timer at zero).
920
+ * @returns {() => void}
921
+ */
922
+ export function startMainContextPump() {
923
+ if (native.isNodeRuntime) return () => {};
924
+ pumpRefCount++;
925
+ if (pumpTimer === null) {
926
+ // ~4 ms cadence: low latency without busy-spinning. Each tick drains every
927
+ // currently-ready source (iterateMainContext(false) returns false when none
928
+ // remain, bounding the inner loop), then yields to the runtime loop.
929
+ pumpTimer = setInterval(() => {
930
+ let guard = 0;
931
+ while (native.iterateMainContext(false) && guard++ < 100000) {
932
+ /* drain ready sources */
933
+ }
934
+ }, 4);
935
+ // Don't let the pump alone keep the process alive where the runtime allows it
936
+ // (Node/Bun expose Timeout.unref; Deno's numeric handle has none).
937
+ if (typeof pumpTimer.unref === 'function') pumpTimer.unref();
938
+ }
939
+ let disposed = false;
940
+ return () => {
941
+ if (disposed) return;
942
+ disposed = true;
943
+ stopMainContextPump();
944
+ };
945
+ }
946
+
947
+ /** Decrement the main-context pump reference count; clears the timer at zero. */
948
+ export function stopMainContextPump() {
949
+ if (native.isNodeRuntime || pumpRefCount === 0) return;
950
+ pumpRefCount--;
951
+ if (pumpRefCount === 0 && pumpTimer !== null) {
952
+ clearInterval(pumpTimer);
953
+ pumpTimer = null;
954
+ }
82
955
  }
83
956
 
84
- function wrapInstance(handle) {
957
+ // bind_property_full / BindingGroup.bind_full: the transform functions are driven
958
+ // by the native GjsPrivate-mirror (src/private.cc) — the same architecture gjs
959
+ // uses (GjsPrivate.g_object_bind_property_full / g_binding_group_bind_full),
960
+ // because the transform's `to_value` is a write-back GValue no marshaled GClosure
961
+ // can reach. The JS contract matches gjs byte-for-byte (verified vs gjs 1.88):
962
+ // (binding, sourceValue) => [ok: boolean, targetValue]
963
+ // — sourceValue arrives unpacked, [false, …] leaves the target unchanged, a
964
+ // non-Array return is reported and treated as no-transform. This wrapper wraps
965
+ // the incoming binding/source into chainable L1 values and unwraps the returned
966
+ // target value back to a native handle (object/variant-valued properties).
967
+ function wrapBindingTransform(fn) {
968
+ if (typeof fn !== 'function') return null;
969
+ return (binding, value) => {
970
+ const result = fn(wrapReturn(binding), wrapReturn(value));
971
+ if (!Array.isArray(result)) return result; // native reports + treats as FALSE
972
+ return [result[0], unwrap(result[1])];
973
+ };
974
+ }
975
+
976
+ // gjs's GObject.Object.prototype signal-convenience methods + bind_property_full
977
+ // (and BindingGroup.bind_full), resolved by JS-accessor name (snake_case or
978
+ // camelCase). Returns a `(handle, args) => result` applier, or undefined for a
979
+ // name we do not shim. block/unblock route through the introspected
980
+ // single-handler g_signal_handler_* functions (verified to marshal a node-gi
981
+ // GObject IN-arg); stop_emission_by_name through g_signal_stop_emission_by_name.
982
+ function objectPrototypeShim(prop) {
983
+ switch (prop) {
984
+ case 'block_signal_handler':
985
+ case 'blockSignalHandler':
986
+ return (handle, args) =>
987
+ wrapReturn(native.callFunction('GObject', 'signal_handler_block', [handle, args[0]]));
988
+ case 'unblock_signal_handler':
989
+ case 'unblockSignalHandler':
990
+ return (handle, args) =>
991
+ wrapReturn(native.callFunction('GObject', 'signal_handler_unblock', [handle, args[0]]));
992
+ case 'stop_emission_by_name':
993
+ case 'stopEmissionByName':
994
+ return (handle, args) =>
995
+ wrapReturn(native.callFunction('GObject', 'signal_stop_emission_by_name', [handle, args[0]]));
996
+ case 'bind_property_full':
997
+ case 'bindPropertyFull':
998
+ return (handle, args) =>
999
+ wrapReturn(
1000
+ native.bindPropertyFull(
1001
+ handle,
1002
+ args[0],
1003
+ unwrap(args[1]),
1004
+ args[2],
1005
+ args[3],
1006
+ wrapBindingTransform(args[4]),
1007
+ wrapBindingTransform(args[5]),
1008
+ ),
1009
+ );
1010
+ case 'bind_full':
1011
+ case 'bindFull':
1012
+ // GObject.BindingGroup.bind_full (gjs: GjsPrivate.g_binding_group_bind_full).
1013
+ // Gated per instance: on any OTHER class a genuine `bind_full` method keeps
1014
+ // resolving through the normal GI route. NOTE: without this shim the
1015
+ // introspected name would resolve to bind_with_closures (its GIR shadow),
1016
+ // whose write-back GValue contract a marshaled GClosure cannot satisfy.
1017
+ return (handle, args) => {
1018
+ if (!native.isInstanceOf(handle, 'GObject', 'BindingGroup')) {
1019
+ return wrapReturn(native.callMethod(handle, 'bind_full', unwrapArgs(args)));
1020
+ }
1021
+ return wrapReturn(
1022
+ native.bindingGroupBindFull(
1023
+ handle,
1024
+ args[0],
1025
+ unwrap(args[1]),
1026
+ args[2],
1027
+ args[3],
1028
+ wrapBindingTransform(args[4]),
1029
+ wrapBindingTransform(args[5]),
1030
+ ),
1031
+ );
1032
+ };
1033
+ default:
1034
+ return undefined;
1035
+ }
1036
+ }
1037
+
1038
+ // Per-GType method feature-detection cache backing the wrapper's GJS-parity
1039
+ // `get` trap: `native.hasMethod` walks the full introspection chain and method
1040
+ // GETs happen on every `obj.method(...)` call, so presence is memoized per
1041
+ // concrete GType name (stable for the process lifetime — typelibs don't gain
1042
+ // methods at runtime).
1043
+ const methodPresenceCache = new Map(); // typeName -> Map<methodName, boolean>
1044
+
1045
+ function instanceHasMethod(handle, name) {
1046
+ const typeName = native.getTypeName(handle);
1047
+ let perType = methodPresenceCache.get(typeName);
1048
+ if (perType === undefined) {
1049
+ perType = new Map();
1050
+ methodPresenceCache.set(typeName, perType);
1051
+ }
1052
+ let present = perType.get(name);
1053
+ if (present === undefined) {
1054
+ present = native.hasMethod(handle, name);
1055
+ perType.set(name, present);
1056
+ }
1057
+ return present;
1058
+ }
1059
+
1060
+ // Wrap a live GObject handle as a GJS-shaped instance. When `userProto` is given
1061
+ // (a registerClass subclass's prototype) the wrapper resolves the user class's
1062
+ // own prototype members FIRST — so `inst.myMethod()` runs the JS method with the
1063
+ // wrapper as `this` — then falls back to GObject property get/set and GI method
1064
+ // routing. `.connect()/.emit()/.disconnect()` work in both modes.
1065
+ function wrapInstance(handle, userProto) {
1066
+ const cached = instanceCache.get(handle);
1067
+ if (cached !== undefined) {
1068
+ // UPGRADE a cached wrapper's user prototype when this wrap supplies a MORE
1069
+ // DERIVED one: an undefined cache (a subclass instance first seen generically —
1070
+ // returned from store.get_item / a signal sender / resurrection), OR a cached
1071
+ // ANCESTOR prototype (a multi-level chain where a vfunc fired during constructType
1072
+ // wrapped the instance with the OVERRIDING ancestor's prototype, before the leaf
1073
+ // ctor's wrapInstance(handle, leaf.prototype) runs — the leaf proto must win so
1074
+ // the leaf's OWN methods resolve). Never DOWNGRADE: a more-derived cached proto is
1075
+ // kept. The userProto lives on the target (read via the proxy here), so the
1076
+ // upgrade is in place — identity (===) and any expando fields are preserved.
1077
+ if (userProto !== undefined && userProto !== cached[USER_PROTO]) {
1078
+ const current = cached[USER_PROTO];
1079
+ if (current === undefined || isProtoSameOrDescendantOf(userProto, current)) {
1080
+ cached[USER_PROTO] = userProto;
1081
+ }
1082
+ }
1083
+ return cached;
1084
+ }
85
1085
  const target = { [HANDLE]: handle };
86
- return new Proxy(target, {
1086
+ if (userProto !== undefined) target[USER_PROTO] = userProto;
1087
+ const proxy = new Proxy(target, {
87
1088
  get(t, prop) {
88
1089
  if (prop === HANDLE) return handle;
89
1090
  if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
90
1091
  switch (prop) {
1092
+ case '$typeName':
1093
+ // The instance's concrete RUNTIME GType name —
1094
+ // g_type_name(G_OBJECT_TYPE(obj)) via native.getTypeName. node-gi hands
1095
+ // back a GENERIC wrapper for a returned handle (it does not downcast to
1096
+ // the runtime GType), so `constructor.$gtype` is the STATIC declared
1097
+ // type; this getter reads the TRUE runtime type. It is the portable seam
1098
+ // @gjsify/devtools' widget-tree DumpTree uses (GJS instead reads the
1099
+ // concrete type off its already-downcast `constructor.$gtype.name`).
1100
+ // Distinct from the CLASS-level `$gtypeName`, which is the DECLARED
1101
+ // namespaced string (e.g. 'Gtk.Widget'); this is the raw runtime GType
1102
+ // name (e.g. 'GtkWidget', 'AdwBin', 'FireworksWindow').
1103
+ return native.getTypeName(handle);
91
1104
  case 'connect':
92
- return (signal, cb) => native.connectSignal(handle, signal, cb, false);
1105
+ // Record the (user fn → id) mapping so signal_handlers_*_by_func can
1106
+ // resolve the private-closure handler back to its ids (see the registry).
1107
+ return (signal, cb) => {
1108
+ const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
1109
+ recordSignalHandler(handle, cb, id);
1110
+ return id;
1111
+ };
93
1112
  case 'connect_after':
94
- return (signal, cb) => native.connectSignal(handle, signal, cb, true);
1113
+ return (signal, cb) => {
1114
+ const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
1115
+ recordSignalHandler(handle, cb, id);
1116
+ return id;
1117
+ };
95
1118
  case 'emit':
96
1119
  return (signal, ...args) =>
97
1120
  wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
98
1121
  case 'disconnect':
99
- return (id) => native.disconnectSignal(handle, id);
1122
+ return (id) => {
1123
+ forgetSignalHandlerId(handle, id);
1124
+ return native.disconnectSignal(handle, id);
1125
+ };
100
1126
  default:
101
1127
  break;
102
1128
  }
1129
+ const up = t[USER_PROTO];
1130
+ if (up !== undefined) {
1131
+ const desc = findProtoDescriptor(up, prop);
1132
+ if (desc !== undefined) {
1133
+ if (typeof desc.value === 'function') return (...args) => desc.value.apply(proxy, args);
1134
+ if (typeof desc.get === 'function') return desc.get.call(proxy);
1135
+ return desc.value;
1136
+ }
1137
+ }
1138
+ // Gio.Application.runAsync — the GJS override (refs/gjs Gio.js + GLib.js):
1139
+ // a Promise-returning run() that does NOT block the Node microtask/event
1140
+ // loop (see applicationRunAsync). Gated to GApplication instances
1141
+ // (g_type_is_a, so Gtk/Adw.Application and registerClass subclasses qualify);
1142
+ // placed AFTER the userProto lookup so a subclass may still override it, and
1143
+ // before the GI-method fallback since there is no introspected `run_async`.
1144
+ if (prop === 'runAsync' && native.isInstanceOf(handle, 'Gio', 'Application')) {
1145
+ return () => applicationRunAsync(handle);
1146
+ }
1147
+ // gjs's GObject.Object.prototype signal conveniences (block_signal_handler /
1148
+ // unblock_signal_handler / stop_emission_by_name) + bind_property_full and
1149
+ // BindingGroup.bind_full (refs/gjs GObject.js). These are NOT introspected instance
1150
+ // methods — they are gjs shims over the namespace-level g_signal_* functions —
1151
+ // so route them explicitly before property / GI-method resolution.
1152
+ const shim = objectPrototypeShim(prop);
1153
+ if (shim !== undefined) return (...args) => shim(handle, args);
103
1154
  const propName = toKebab(prop);
104
1155
  if (native.hasProperty(handle, propName)) {
105
1156
  return wrapReturn(native.getProperty(handle, propName));
106
1157
  }
107
- return (...args) => wrapReturn(native.callMethod(handle, camelToSnake(prop), unwrapArgs(args)));
1158
+ // Surface a plain JS field previously written on THIS wrapper. With the
1159
+ // toggle-ref bridge the wrapper is now CANONICAL (one proxy per GObject,
1160
+ // cached by the canonical native handle), so a plain field IS shared across
1161
+ // the vfunc<->instance boundary and survives a round-trip + GC while C owns
1162
+ // the object — a vfunc's `this` resolves to the same cached proxy as
1163
+ // construct, and `store.get_item(x)` returns the same proxy a setter wrote
1164
+ // to. Own-property only (set via the `set` trap), so an introspected GI
1165
+ // method of the same name is never shadowed unless the user explicitly
1166
+ // assigned an expando. (GObject PROPERTIES remain the right choice for state
1167
+ // that must also be visible to C / other language bindings.)
1168
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return t[prop];
1169
+ // LAST resort before treating an unknown name as a GI method: an INHERITED
1170
+ // member (Object.prototype.hasOwnProperty / isPrototypeOf / … — RESERVED
1171
+ // already covers toString/valueOf/etc.) must resolve to the real function,
1172
+ // not a GI callMethod thunk that would throw on `inst.hasOwnProperty('x')`.
1173
+ if (prop in t) return t[prop];
1174
+ // A Gio._promisify'd async method (registerClass subclasses pick it up via
1175
+ // their userProto above; introspected instances have no prototype chain, so
1176
+ // they resolve it here from the registry, per-class). Bound to this instance.
1177
+ const promisified = resolvePromisified(handle, camelToSnake(prop));
1178
+ if (promisified !== undefined) return (...args) => promisified.apply(proxy, args);
1179
+ // GJS parity: an UNKNOWN member is `undefined`, never a throw-on-call
1180
+ // thunk — real consumers feature-detect optional native methods
1181
+ // (`typeof gl.clearBufferfv === 'function'` gates `@gjsify/webgl`'s
1182
+ // clearBuffer emulation; Excalibur's RenderTarget.blitToScreen was the
1183
+ // exposing call on node-gi: the old unconditional thunk made the
1184
+ // detection lie, then threw mid-frame). Presence is resolved via the
1185
+ // SAME native walk callMethod uses (literal-first, snake alias second)
1186
+ // and memoized per concrete GType — method GETs happen on every call.
1187
+ // The LITERAL accessor name is passed through — the engine resolves it
1188
+ // verbatim first (GJS fidelity: GIR names are exposed as-is, incl. Vala
1189
+ // camelCase like Gwebgl's `getString`) and only falls back to the
1190
+ // snake_case alias. Converting here would destroy a literal camelCase
1191
+ // GI name.
1192
+ if (instanceHasMethod(handle, prop)) {
1193
+ return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1194
+ }
1195
+ return undefined;
108
1196
  },
109
1197
  set(t, prop, value) {
110
1198
  if (typeof prop === 'string') {
1199
+ const up = t[USER_PROTO];
1200
+ if (up !== undefined) {
1201
+ const desc = findProtoDescriptor(up, prop);
1202
+ if (desc !== undefined && typeof desc.set === 'function') {
1203
+ desc.set.call(proxy, value);
1204
+ return true;
1205
+ }
1206
+ }
111
1207
  const propName = toKebab(prop);
112
1208
  if (native.hasProperty(handle, propName)) {
113
1209
  native.setProperty(handle, propName, unwrapArg(value));
@@ -118,50 +1214,237 @@ function wrapInstance(handle) {
118
1214
  return true;
119
1215
  },
120
1216
  has(t, prop) {
121
- return prop === HANDLE || prop in t;
1217
+ if (prop === HANDLE || prop in t) return true;
1218
+ if (typeof prop === 'string') {
1219
+ // A subclass's own prototype member (method/getter) and any GObject
1220
+ // property of the instance both count as `in` the wrapper. (Introspected
1221
+ // GI methods are resolved dynamically and are intentionally not reported.)
1222
+ const up = t[USER_PROTO];
1223
+ if (up !== undefined && findProtoDescriptor(up, prop) !== undefined) return true;
1224
+ if (native.hasProperty(handle, toKebab(prop))) return true;
1225
+ }
1226
+ return false;
1227
+ },
1228
+ });
1229
+ instanceCache.set(handle, proxy);
1230
+ return proxy;
1231
+ }
1232
+
1233
+ // Shared object installed at the BASE of every introspected class's prototype
1234
+ // chain so `super.vfunc_<name>(...)` inside a registerClass override resolves to a
1235
+ // chain-up thunk. A registered subclass extends an introspected base (e.g.
1236
+ // `class X extends GObject.Object`), so `super.vfunc_x` looks up `vfunc_x` on the
1237
+ // base class's prototype; with this Proxy beneath it, any `vfunc_*` access yields
1238
+ // a thunk that invokes the captured parent (C) vfunc via native.callParentVfunc,
1239
+ // with `this` (the canonical wrapper instance) as the GObject. Mirrors GJS, where
1240
+ // the introspected parent's `vfunc_x` is a thunk into the actual C vtable entry.
1241
+ // Non-`vfunc_` lookups fall through to Object.prototype (so the base prototype
1242
+ // keeps its normal object behaviour).
1243
+ const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1244
+ get(target, prop, receiver) {
1245
+ if (typeof prop === 'string' && prop.startsWith('vfunc_')) {
1246
+ const vfuncName = prop.slice('vfunc_'.length);
1247
+ return function chainUpToParentVfunc(...args) {
1248
+ const handle = this !== null && this !== undefined ? this[HANDLE] : undefined;
1249
+ if (handle === undefined) {
1250
+ throw new TypeError(`super.${prop}(): chain-up requires a node-gi instance as \`this\``);
1251
+ }
1252
+ return wrapReturn(native.callParentVfunc(handle, vfuncName, unwrapArgs(args)));
1253
+ };
1254
+ }
1255
+ return Reflect.get(target, prop, receiver);
1256
+ },
1257
+ });
1258
+
1259
+ // Surface a lazy `$gtype` getter on an introspected class/struct ctor: the real
1260
+ // GObject GType (a node-gi GType handle), resolved on first read via the engine
1261
+ // and then cached as a value. GJS exposes `SomeClass.$gtype`; the storybook reads
1262
+ // `Adw.Clamp.$gtype` + passes `StoryWidget.$gtype` to GObject.type_ensure. Lazy
1263
+ // (defineProperty getter) so it costs nothing until accessed. `null` (unknown type)
1264
+ // is also cached. The makeClass/makeStruct Proxy forwards `'$gtype' in t` → the
1265
+ // target getter (it returns `t[prop]` when `prop in t`).
1266
+ function defineLazyGType(ctor, namespace, typeName) {
1267
+ Object.defineProperty(ctor, '$gtype', {
1268
+ configurable: true,
1269
+ enumerable: false,
1270
+ get() {
1271
+ const gt = native.getGType(namespace, typeName);
1272
+ Object.defineProperty(ctor, '$gtype', {
1273
+ value: gt,
1274
+ configurable: true,
1275
+ enumerable: false,
1276
+ writable: false,
1277
+ });
1278
+ return gt;
122
1279
  },
123
1280
  });
124
1281
  }
125
1282
 
126
1283
  function makeClass(namespace, typeName) {
127
1284
  const ctor = function ctor(props) {
1285
+ // G3/G2 construction routing. `new.target` is the leaf class the `new` was
1286
+ // applied to and is preserved through every super() hop, so when a registerClass'd
1287
+ // subclass is being constructed (directly or up a super() chain — at ANY depth,
1288
+ // since the leaf's registered GType already IS the full multi-level type) we build
1289
+ // the REGISTERED GType and return its canonical toggle-ref wrapper (carrying the
1290
+ // leaf's prototype as USER_PROTO) — which then substitutes `this` in every derived
1291
+ // ctor body. `new.target === undefined` (a call without `new`) or `=== proxy` (a
1292
+ // direct `new Ns.Class(...)` on the introspected class itself) falls through to the
1293
+ // unchanged introspected construction.
1294
+ const nt = new.target;
1295
+ if (nt !== undefined && nt !== proxy) {
1296
+ const reg = registeredClasses.get(nt);
1297
+ if (reg !== undefined) {
1298
+ const handle = native.constructType(reg.typeHandle, props ? unwrapProps(props) : {});
1299
+ const instance = wrapInstance(handle, nt.prototype);
1300
+ assignTemplateChildren(instance, handle, reg);
1301
+ return instance;
1302
+ }
1303
+ // `nt` is a subclass of this introspected GObject class but was NEVER passed to
1304
+ // GObject.registerClass(): silently building the introspected BASE type would
1305
+ // produce the wrong GType (missing the subclass's props/signals/vfuncs). Throw
1306
+ // the GJS-shaped error instead of constructing a misleading instance — the L1
1307
+ // analogue of GJS's "are you using GObject.registerClass()?" construct guard
1308
+ // (refs/gjs/gi/object.cpp).
1309
+ throw new Error(
1310
+ `Object ${nt.name || '(anonymous)'} is not registered with GObject.registerClass()`,
1311
+ );
1312
+ }
128
1313
  const handle = native.newObject(namespace, typeName, props ? unwrapProps(props) : {});
129
1314
  return wrapInstance(handle);
130
1315
  };
131
1316
  Object.defineProperty(ctor, 'name', { value: typeName, configurable: true });
132
1317
  ctor.$gtypeName = `${namespace}.${typeName}`;
1318
+ defineLazyGType(ctor, namespace, typeName);
1319
+ // Put the vfunc chain-up Proxy beneath this base class's prototype so a
1320
+ // registerClass subclass's `super.vfunc_<name>(...)` resolves (see above).
1321
+ Object.setPrototypeOf(ctor.prototype, vfuncChainProto);
1322
+ // Stamp the prototype with its class identity so Gio._promisify(Cls.prototype, …)
1323
+ // can record which class a registration belongs to (non-enumerable).
1324
+ Object.defineProperty(ctor.prototype, CLASS_INFO, {
1325
+ value: { namespace, typeName },
1326
+ enumerable: false,
1327
+ configurable: true,
1328
+ });
1329
+ // `inst instanceof Ns.Class` — GJS parity for the WHOLE GObject hierarchy, not
1330
+ // just the leaf class. An instance is a Proxy over a bare `{[HANDLE]}` (no live JS
1331
+ // prototype chain linking Adw.ApplicationWindow → Gtk.ApplicationWindow → …), so
1332
+ // the default instanceof (a prototype-chain walk) reported `false` for every base
1333
+ // class. Resolve it by the GObject type system instead: `native.isInstanceOf`
1334
+ // (g_type_is_a) recognises subclasses AND implemented interfaces, exactly like GJS.
1335
+ // Guarded by `isGObjectHandle` first — a bare/boxed/variant handle (or any non-node-gi
1336
+ // value) is NOT a GObject, so it must be `false`, never reach `isInstanceOf` (which
1337
+ // throws on a non-GObject handle). The makeClass Proxy `get` trap forwards this
1338
+ // symbol to the target (`typeof prop !== 'string'` → `t[prop]`), so `instanceof`
1339
+ // finds it on the proxy. Overlay-built classes (GLib.Variant / GObject.Value /
1340
+ // GObject.ParamSpec) bypass makeClass and keep their own dedicated hasInstance.
1341
+ Object.defineProperty(ctor, Symbol.hasInstance, {
1342
+ configurable: true,
1343
+ value(instance) {
1344
+ if (instance === null || typeof instance !== 'object') return false;
1345
+ const handle = instance[HANDLE];
1346
+ if (handle === undefined || !native.isGObjectHandle(handle)) return false;
1347
+ // The introspected class ITSELF → resolve by name: g_type_is_a recognises every
1348
+ // subclass AND implemented interface, matching GJS exactly.
1349
+ if (this === proxy) return native.isInstanceOf(handle, namespace, typeName);
1350
+ // A registerClass'd subclass INHERITS this method (it `extends` the proxy) but
1351
+ // must match ITS OWN registered GType, not the shared introspected base — else a
1352
+ // sibling subclass or a bare-base instance would wrongly test true. Resolve the
1353
+ // instance's runtime GType against the subclass's own `$gtype` through the GObject
1354
+ // type system (a live JS prototype chain the subclass instance lacks — the L1
1355
+ // wrapper carries the user prototype as a symbol, not as [[Prototype]]).
1356
+ const subGType = this.$gtype;
1357
+ if (subGType !== undefined && subGType !== null) {
1358
+ const G = requireGi('GObject', '2.0');
1359
+ return G.type_is_a(G.type_from_name(native.getTypeName(handle)), subGType);
1360
+ }
1361
+ // A raw (unregistered) subclass / unrelated ctor: ordinary prototype-chain.
1362
+ return Function.prototype[Symbol.hasInstance].call(this, instance);
1363
+ },
1364
+ });
133
1365
  // Expose constructor/static methods lazily: Ns.Class.new(...) /
134
1366
  // Ns.Class.new_for_path(...) (and the camelCase aliases). `new Ns.Class({...})`
135
1367
  // still goes through the target's [[Construct]] (the default Proxy behaviour).
136
- return new Proxy(ctor, {
137
- get(t, prop) {
1368
+ const proxy = new Proxy(ctor, {
1369
+ get(t, prop, receiver) {
1370
+ // #667: a raw (UNregistered) subclass that `extends` this introspected class
1371
+ // must NOT inherit-read the parent's GType. A registered subclass has its OWN
1372
+ // `$gtype` (set in place by registerClass, so it shadows + never reaches this
1373
+ // trap); a raw subclass is not a GType, so an inherited read (receiver is the
1374
+ // subclass, not this proxy) resolves to undefined. A direct read on the
1375
+ // introspected class itself (receiver === proxy) resolves the real lazy GType.
1376
+ if (prop === '$gtype' && receiver !== proxy) return undefined;
138
1377
  if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
1378
+ // GObject.Object.new(gtype, props) / .new_with_properties(gtype, names,
1379
+ // values): gjs's GObject.js statics that construct a GObject from a runtime
1380
+ // GType. Not introspected (g_object_new is variadic), so route them to the
1381
+ // by-GType constructor (native.constructType accepts any GType handle).
1382
+ if (namespace === 'GObject' && typeName === 'Object' &&
1383
+ (prop === 'new' || prop === 'new_with_properties' || prop === 'newWithProperties')) {
1384
+ return objectStaticConstructor(prop);
1385
+ }
139
1386
  const giName = camelToSnake(prop);
140
1387
  return (...args) =>
141
1388
  wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
142
1389
  },
143
1390
  });
1391
+ return proxy;
1392
+ }
1393
+
1394
+ // The GObject.Object.new / new_with_properties statics (gjs GObject.js). `new`
1395
+ // resolves the GType (a class ctor's $gtype or a GType handle) and constructs it via
1396
+ // constructType (which accepts an arbitrary GType, returning the canonical wrapper —
1397
+ // the same routing `new Ns.Class({...})` uses); `new_with_properties` zips the two
1398
+ // arrays into a prop dict and delegates. Mirrors GJS's `GObject.Object.new`, which
1399
+ // looks up the constructor for `gtype` and `new`s it with the prop dict.
1400
+ function objectStaticConstructor(prop) {
1401
+ const objectNew = (gtype, props = {}) => {
1402
+ const gt = resolveGTypeArg(gtype);
1403
+ if (gt === undefined || gt === null) {
1404
+ throw new TypeError('GObject.Object.new: a GType (class or Class.$gtype) is required');
1405
+ }
1406
+ return wrapInstance(native.constructType(gt, props ? unwrapProps(props) : {}));
1407
+ };
1408
+ if (prop === 'new') return objectNew;
1409
+ return (gtype, names, values) => {
1410
+ if (!Array.isArray(names) || !Array.isArray(values) || names.length !== values.length) {
1411
+ throw new Error(
1412
+ 'GObject.Object.new_with_properties takes two equal-length arrays (names, values)',
1413
+ );
1414
+ }
1415
+ const props = {};
1416
+ for (let i = 0; i < names.length; i++) props[names[i]] = values[i];
1417
+ return objectNew(gtype, props);
1418
+ };
144
1419
  }
145
1420
 
146
1421
  // Surface a boxed/struct type (e.g. GLib.MainLoop) as a class-like object whose
147
1422
  // static/constructor methods route through the engine: `GLib.MainLoop.new(...)`
148
- // and the camelCase alias, plus `new GLib.MainLoop(...)` mapped to the `new`
149
- // constructor. Returned boxed instances are wrapped by {@link wrapBoxed}.
1423
+ // and the camelCase alias. `new GLib.MainLoop(...)` routes through the native
1424
+ // constructStruct — the `new` constructor when the struct has one, else (GJS
1425
+ // gi/boxed.cpp parity) a ZERO-INITIALIZED instance for zero args
1426
+ // (`new Graphene.Rect()`, `new Gdk.RGBA()` — the devtools screenshot chain).
1427
+ // Returned boxed instances are wrapped by {@link wrapBoxed}.
150
1428
  function makeStruct(namespace, typeName) {
151
1429
  const base = function () {};
152
1430
  Object.defineProperty(base, 'name', { value: typeName, configurable: true });
153
1431
  base.$gtypeName = `${namespace}.${typeName}`;
154
- return new Proxy(base, {
155
- get(t, prop) {
1432
+ defineLazyGType(base, namespace, typeName);
1433
+ const proxy = new Proxy(base, {
1434
+ get(t, prop, receiver) {
1435
+ // #667: guard inherited `$gtype` reads (see makeClass) — a raw subclass of a
1436
+ // boxed/struct type does not inherit the parent's GType.
1437
+ if (prop === '$gtype' && receiver !== proxy) return undefined;
156
1438
  if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
157
1439
  const giName = camelToSnake(prop);
158
1440
  return (...args) =>
159
1441
  wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
160
1442
  },
161
1443
  construct(_t, args) {
162
- return wrapReturn(native.callStaticMethod(namespace, typeName, 'new', unwrapArgs(args)));
1444
+ return wrapReturn(native.constructStruct(namespace, typeName, unwrapArgs(args)));
163
1445
  },
164
1446
  });
1447
+ return proxy;
165
1448
  }
166
1449
 
167
1450
  // Build a frozen enum/flags object keyed GJS-style: member names UPPER_CASED
@@ -172,9 +1455,690 @@ function makeEnum(namespace, typeName) {
172
1455
  for (const key of Object.keys(raw)) {
173
1456
  out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
174
1457
  }
1458
+ // If this enum is registered as a GError domain (Gio.IOErrorEnum, …), attach
1459
+ // its domain descriptor (non-enumerable) so GLib.Error.matches(enum, code) can
1460
+ // resolve the enum to its domain. A plain enum gets nothing (getErrorDomain → null).
1461
+ const domain = native.getErrorDomain(namespace, typeName);
1462
+ if (domain !== null) {
1463
+ Object.defineProperty(out, ERROR_DOMAIN, { value: domain, enumerable: false });
1464
+ }
175
1465
  return Object.freeze(out);
176
1466
  }
177
1467
 
1468
+ // ---- Gio._promisify (L1, GJS-shaped) ----
1469
+ //
1470
+ // The Node twin of GJS's Gio._promisify (refs/gjs/modules/core/overrides/Gio.js).
1471
+ // Wraps an async method so calling it WITHOUT a trailing GAsyncReadyCallback
1472
+ // returns a Promise: it invokes the underlying async method passing a callback
1473
+ // that runs `finishName` and resolves with its return (the OUT-param tuple), or
1474
+ // rejects with the GLib.Error the finish call throws. Called WITH a trailing
1475
+ // callback it behaves as the plain async method (no Promise).
1476
+ //
1477
+ // Builds on: OUT/INOUT params (finish returns its results via out-params), the
1478
+ // libuv↔GLib mainloop bridge (the async completion fires on the loop) and GI
1479
+ // callbacks (the GAsyncReadyCallback). The async op holds a ref on the source
1480
+ // object and the GAsyncReadyCallback wrapper holds a strong ref to the JS callback
1481
+ // (which captures the instance proxy), so the wrapper + its GObject stay alive
1482
+ // until completion — instance identity, toggle-ref #656.
1483
+
1484
+ // _async / _begin → _finish; otherwise <name>_finish (GJS's convention).
1485
+ function deriveFinishName(asyncName) {
1486
+ if (asyncName.endsWith('_begin') || asyncName.endsWith('_async')) {
1487
+ return `${asyncName.slice(0, -5)}finish`;
1488
+ }
1489
+ return `${asyncName}_finish`;
1490
+ }
1491
+
1492
+ // Build the promisified wrapper for `asyncName`. Uses `this` (the instance proxy)
1493
+ // so it works both bound from the registry and as a prototype method.
1494
+ function makePromisified(asyncName, finishName) {
1495
+ const giAsync = camelToSnake(asyncName);
1496
+ return function promisified(...args) {
1497
+ const handle = this[HANDLE];
1498
+ // A trailing callback → plain async method (the caller drives the callback).
1499
+ // Wrap it so its (source, result) args are marshalled to wrapped instances —
1500
+ // matching the Promise path and GJS, so `source.finish(result)` works.
1501
+ if (args.length > 0 && typeof args[args.length - 1] === 'function') {
1502
+ const userCb = args[args.length - 1];
1503
+ const forwarded = [...args.slice(0, -1), wrapSignalCallback(userCb)];
1504
+ return wrapReturn(native.callMethod(handle, giAsync, unwrapArgs(forwarded)));
1505
+ }
1506
+ const self = this;
1507
+ return new Promise((resolve, reject) => {
1508
+ // wrapSignalCallback marshals each native callback arg (source, res) through
1509
+ // wrapReturn → wrapped instances, so both `source[finishName](res)` and the
1510
+ // captured-instance fallback round-trip cleanly.
1511
+ const onReady = wrapSignalCallback((source, res) => {
1512
+ try {
1513
+ const finisher =
1514
+ source !== null && source !== undefined && typeof source[finishName] === 'function'
1515
+ ? source[finishName].bind(source)
1516
+ : self[finishName].bind(self);
1517
+ const value = finisher(res);
1518
+ // GJS's _promisify drops a leading `true` ok-flag so the dominant
1519
+ // finish shape (load_contents_finish, query_info_finish,
1520
+ // communicate_utf8_finish, … → [true, …]) resolves to just the
1521
+ // payload — keeping one source byte-identical on gjs + node.
1522
+ if (Array.isArray(value) && value.length > 1 && value[0] === true) {
1523
+ value.shift();
1524
+ }
1525
+ resolve(value);
1526
+ } catch (error) {
1527
+ reject(error);
1528
+ }
1529
+ });
1530
+ native.callMethod(handle, giAsync, unwrapArgs([...args, onReady]));
1531
+ });
1532
+ };
1533
+ }
1534
+
1535
+ /**
1536
+ * Gio._promisify(prototype, asyncName, finishName?) — replace an async method so a
1537
+ * call without a trailing callback returns a Promise. `finishName` defaults per
1538
+ * GJS's convention (`_async`/`_begin` → `_finish`, else `<name>_finish`). Works on
1539
+ * any introspected class prototype (e.g. Gio.File.prototype) or a registerClass
1540
+ * subclass prototype.
1541
+ * @param {object} proto e.g. Gio.File.prototype
1542
+ * @param {string} asyncName e.g. "load_contents_async"
1543
+ * @param {string} [finishName] e.g. "load_contents_finish"
1544
+ * @returns {void}
1545
+ */
1546
+ function promisify(proto, asyncName, finishName = undefined) {
1547
+ if (proto === null || typeof proto !== 'object') {
1548
+ throw new TypeError('Gio._promisify: prototype must be an object');
1549
+ }
1550
+ if (typeof asyncName !== 'string') {
1551
+ throw new TypeError('Gio._promisify: asyncName must be a string');
1552
+ }
1553
+ const finish = finishName === undefined ? deriveFinishName(asyncName) : finishName;
1554
+ const wrapper = makePromisified(asyncName, finish);
1555
+ // Install on the prototype (so proto[asyncName] IS the promisified method, and a
1556
+ // registerClass subclass — which resolves its userProto — picks it up) AND in
1557
+ // the global registry (introspected instances resolve methods dynamically, with
1558
+ // no live prototype chain to walk).
1559
+ try {
1560
+ proto[asyncName] = wrapper;
1561
+ } catch {
1562
+ // A frozen/sealed prototype — the registry still makes it work.
1563
+ }
1564
+ // Record the registration keyed by the GI method name, tagged with the class it
1565
+ // was registered on (CLASS_INFO from makeClass) so two classes promisifying the
1566
+ // same method name with different finish methods stay disambiguated per-class.
1567
+ const info = proto[CLASS_INFO];
1568
+ const registration = {
1569
+ namespace: info ? info.namespace : undefined,
1570
+ typeName: info ? info.typeName : undefined,
1571
+ wrapper,
1572
+ };
1573
+ const key = camelToSnake(asyncName);
1574
+ const existing = promisifiedMethods.get(key);
1575
+ if (existing === undefined) {
1576
+ promisifiedMethods.set(key, [registration]);
1577
+ } else {
1578
+ // Replace a same-class registration in place; otherwise append (a second class).
1579
+ const i = existing.findIndex(
1580
+ (r) => r.namespace === registration.namespace && r.typeName === registration.typeName,
1581
+ );
1582
+ if (i >= 0) existing[i] = registration;
1583
+ else existing.push(registration);
1584
+ }
1585
+ }
1586
+
1587
+ // Resolve the promisified wrapper for `methodName` on an instance handle. The
1588
+ // common case is a single registration (fast path); when several classes
1589
+ // promisified the same method name, pick the one whose class the instance is-a.
1590
+ function resolvePromisified(handle, methodName) {
1591
+ const registrations = promisifiedMethods.get(methodName);
1592
+ if (registrations === undefined) return undefined;
1593
+ if (registrations.length === 1) return registrations[0].wrapper;
1594
+ for (const r of registrations) {
1595
+ if (r.namespace !== undefined && native.isInstanceOf(handle, r.namespace, r.typeName)) {
1596
+ return r.wrapper;
1597
+ }
1598
+ }
1599
+ // No class matched (or a registerClass registration with no CLASS_INFO) — fall
1600
+ // back to the last registration so a single-class misconfig still resolves.
1601
+ return registrations[registrations.length - 1].wrapper;
1602
+ }
1603
+
1604
+ // ---- GObject.registerClass decorator (L1, GJS-shaped) ----
1605
+ //
1606
+ // The Node twin of GJS's `GObject.registerClass(meta, class)`. It accepts both
1607
+ // `registerClass(class)` and `registerClass({ GTypeName?, Properties?, Signals?,
1608
+ // ... }, class)`, registers a GObject subtype via the native engine, registers the
1609
+ // GIVEN class IN PLACE, and returns that SAME class (GJS-faithful — see the G3
1610
+ // registry above). So both `const X = registerClass(…,C)` and the GJS
1611
+ // `static { registerClass(…,C) }` idiom (return discarded) leave the same symbol
1612
+ // bound to the registered GType. `new`-ed instances are usable as GObjects
1613
+ // (property get/set, `.connect/.emit/.disconnect`) AND expose the user class's own
1614
+ // prototype methods (resolved before the GObject routing — see wrapInstance).
1615
+ //
1616
+ // G3 contract (the user JS constructor body now RUNS):
1617
+ // - The construction routing lives in the introspected base ctor (makeClass),
1618
+ // keyed on `new.target`. `new X(args)` runs X's ctor body against the canonical
1619
+ // toggle-ref wrapper, with working `super(args)` semantics (the base ctor
1620
+ // returns the wrapper → it substitutes `this` up the whole chain). This REVERSES
1621
+ // the previous "ctor body is not run" caveat. `vfunc_constructed` still fires
1622
+ // once during construction; a class that inits in BOTH a ctor body and
1623
+ // `vfunc_constructed` will run both (normal GJS behaviour) — do not duplicate.
1624
+ // - Toggle-ref identity (#656): the `this` in the ctor body, the post-construct
1625
+ // wrapper, a vfunc's `this` and a signal-handler's `this` are all the SAME
1626
+ // canonical wrapper (===), and a plain JS field set on it survives a round-trip
1627
+ // + GC while C owns the object. (A GObject PROPERTY is still the right choice for
1628
+ // state that must also be visible to C / other bindings.)
1629
+ // - Instances are Proxies over a native handle, not real `instanceof` instances.
1630
+ // - Ordering caveat: a `vfunc_constructed` that fires DURING constructType sees a
1631
+ // wrapper whose USER_PROTO is attached by the vfunc trampoline (collectVfuncs),
1632
+ // so its own user-proto methods resolve; the post-construct `wrapInstance(handle,
1633
+ // nt.prototype)` then upgrades the same cached wrapper in place (identity kept).
1634
+ // - Multi-level registered subclassing (registering a subclass of a registerClass'd
1635
+ // type, at any depth) IS supported (G2): findParentGType resolves a registered
1636
+ // parent to its runtime GType handle and registerClass subclasses from it via
1637
+ // native.registerClassFromGType. Inherited custom properties, signals and vfuncs
1638
+ // of registered ANCESTORS compose for free through normal GObject inheritance
1639
+ // (g_object_class_find_property / g_signal_lookup / the owner-type-keyed property
1640
+ // store all walk the ancestry). A registered parent must be registered BEFORE the
1641
+ // child's static{} runs — ES module evaluation order guarantees this.
1642
+
1643
+ // GObject.ParamFlags — the GParamFlags bits the native property builder consumes.
1644
+ const ParamFlags = Object.freeze({
1645
+ READABLE: 1,
1646
+ WRITABLE: 2,
1647
+ READWRITE: 3,
1648
+ CONSTRUCT: 4,
1649
+ CONSTRUCT_ONLY: 8,
1650
+ });
1651
+
1652
+ // GObject.SignalFlags — the GSignalFlags bits (native defaults to RUN_LAST).
1653
+ const SignalFlags = Object.freeze({
1654
+ RUN_FIRST: 1,
1655
+ RUN_LAST: 2,
1656
+ RUN_CLEANUP: 4,
1657
+ });
1658
+
1659
+ // A ParamSpec factory returns a plain descriptor the decorator maps to the
1660
+ // native PropertySpec. `string`/`boolean` capture (name, nick, blurb, flags,
1661
+ // default); the numeric kinds capture (name, nick, blurb, flags, min, max,
1662
+ // default) — matching GJS's GObject.ParamSpec.* argument order.
1663
+ function paramSpecPlain(type) {
1664
+ return (name, nick, blurb, flags, defaultValue) => ({
1665
+ $paramSpec: true,
1666
+ type,
1667
+ name,
1668
+ nick,
1669
+ blurb,
1670
+ flags,
1671
+ default: defaultValue,
1672
+ });
1673
+ }
1674
+
1675
+ function paramSpecRanged(type) {
1676
+ return (name, nick, blurb, flags, minimum, maximum, defaultValue) => ({
1677
+ $paramSpec: true,
1678
+ type,
1679
+ name,
1680
+ nick,
1681
+ blurb,
1682
+ flags,
1683
+ minimum,
1684
+ maximum,
1685
+ default: defaultValue,
1686
+ });
1687
+ }
1688
+
1689
+ // Resolve a GObject.ParamSpec.object/.boxed `gtype` argument to a native GType
1690
+ // handle the engine's BuildParamSpec can consume. The arg is GJS-shaped: a class
1691
+ // ctor (introspected `Adw.Bin` / `GObject.Object`, or a registered class) whose
1692
+ // real `$gtype` is the GType handle (G4), or an already-resolved GType handle.
1693
+ function resolveGTypeArg(gtype) {
1694
+ if (gtype === null || gtype === undefined) return gtype;
1695
+ if (typeof gtype === 'function' || typeof gtype === 'object') {
1696
+ const gt = gtype.$gtype;
1697
+ if (gt !== undefined) return gt;
1698
+ }
1699
+ return gtype; // assume it is already a GType handle
1700
+ }
1701
+
1702
+ // object/boxed ParamSpec factories: `(name, nick, blurb, flags, gtype)` — matching
1703
+ // GJS's GObject.ParamSpec.object/.boxed argument order. `gtype` is a class ctor
1704
+ // (its `$gtype` is read) or a GType handle. Produces a real g_param_spec_object /
1705
+ // g_param_spec_boxed via the engine's BuildParamSpec.
1706
+ function paramSpecGTyped(type) {
1707
+ return (name, nick, blurb, flags, gtype) => ({
1708
+ $paramSpec: true,
1709
+ type,
1710
+ name,
1711
+ nick,
1712
+ blurb,
1713
+ flags,
1714
+ gtype: resolveGTypeArg(gtype),
1715
+ });
1716
+ }
1717
+
1718
+ const ParamSpec = Object.freeze({
1719
+ string: paramSpecPlain('string'),
1720
+ boolean: paramSpecPlain('boolean'),
1721
+ int: paramSpecRanged('int'),
1722
+ uint: paramSpecRanged('uint'),
1723
+ int64: paramSpecRanged('int64'),
1724
+ uint64: paramSpecRanged('uint64'),
1725
+ double: paramSpecRanged('double'),
1726
+ float: paramSpecRanged('float'),
1727
+ object: paramSpecGTyped('object'),
1728
+ boxed: paramSpecGTyped('boxed'),
1729
+ // `wrappedPspec instanceof GObject.ParamSpec` — a wrapped GParamSpec (from a
1730
+ // notify handler / a GParamSpec-typed value) carries [HANDLE], recognised via
1731
+ // the native tag. `instanceof` honours a `[Symbol.hasInstance]` on the RHS even
1732
+ // though ParamSpec is a factory object rather than a constructor.
1733
+ [Symbol.hasInstance](instance) {
1734
+ if (instance === null || typeof instance !== 'object') return false;
1735
+ const handle = instance[HANDLE];
1736
+ return handle !== undefined && native.isParamSpecHandle(handle);
1737
+ },
1738
+ });
1739
+
1740
+ // Walk up the JS class's prototype chain to the nearest node-gi base and describe
1741
+ // the parent the native engine subclasses from. Two shapes (nearest ancestor wins):
1742
+ // - a REGISTERED ancestor (registerClass'd, present in registeredClasses) →
1743
+ // `{ parentGTypeHandle }` (its runtime GType handle, from #667). The registered
1744
+ // type has no introspection entry, so it must be subclassed from its GType
1745
+ // directly (native.registerClassFromGType) — this is what unlocks multi-level
1746
+ // registered-of-registered subclassing (G2).
1747
+ // - an INTROSPECTED ancestor (a makeClass wrapper carrying a dotted `$gtypeName`
1748
+ // like "Adw.Bin") → `{ parentNamespace, parentType }` (resolved by name).
1749
+ // A registered class carries BOTH a dotless `$gtypeName` AND a registeredClasses
1750
+ // entry, so the registry check MUST come first.
1751
+ function findParentGType(klass) {
1752
+ for (let c = Object.getPrototypeOf(klass); typeof c === 'function'; c = Object.getPrototypeOf(c)) {
1753
+ const reg = registeredClasses.get(c);
1754
+ if (reg !== undefined) {
1755
+ return { parentGTypeHandle: reg.typeHandle };
1756
+ }
1757
+ const gt = c.$gtypeName;
1758
+ if (typeof gt === 'string') {
1759
+ const dot = gt.indexOf('.');
1760
+ if (dot > 0) {
1761
+ return { parentNamespace: gt.slice(0, dot), parentType: gt.slice(dot + 1) };
1762
+ }
1763
+ // A dotless `$gtypeName` with no registeredClasses entry should never occur
1764
+ // (only registered types are dotless, and they are in the registry); be
1765
+ // explicit rather than mis-resolving the namespace split.
1766
+ throw new TypeError(
1767
+ "GObject.registerClass: parent '" + gt + "' has no resolvable GType",
1768
+ );
1769
+ }
1770
+ }
1771
+ return undefined;
1772
+ }
1773
+
1774
+ // Map a GObject.ParamSpec.* descriptor (from meta.Properties) to a native
1775
+ // PropertySpec. The property name defaults to the Properties key.
1776
+ function paramSpecToNative(desc, key) {
1777
+ if (desc === null || typeof desc !== 'object') {
1778
+ throw new TypeError(
1779
+ "GObject.registerClass: Properties['" + key + "'] must be a GObject.ParamSpec descriptor",
1780
+ );
1781
+ }
1782
+ const spec = { name: typeof desc.name === 'string' && desc.name.length > 0 ? desc.name : key };
1783
+ spec.type = desc.type;
1784
+ if (typeof desc.flags === 'number') spec.flags = desc.flags;
1785
+ if (desc.default !== undefined) spec.default = desc.default;
1786
+ if (typeof desc.minimum === 'number') spec.minimum = desc.minimum;
1787
+ if (typeof desc.maximum === 'number') spec.maximum = desc.maximum;
1788
+ // object/boxed ParamSpecs carry a value GType handle (passed straight to the
1789
+ // engine's BuildParamSpec → g_param_spec_object / g_param_spec_boxed).
1790
+ if (desc.gtype !== undefined) spec.gtype = desc.gtype;
1791
+ return spec;
1792
+ }
1793
+
1794
+ // Normalise a signal param/return type to the native type-name vocabulary: a
1795
+ // string passes through; a ParamSpec descriptor contributes its `.type`.
1796
+ function normalizeSignalType(t) {
1797
+ if (typeof t === 'string') return t;
1798
+ if (t !== null && typeof t === 'object' && typeof t.type === 'string') return t.type;
1799
+ return undefined;
1800
+ }
1801
+
1802
+ // Map a meta.Signals entry (`{ param_types?, return_type?, flags? }`, GJS keys,
1803
+ // camelCase also accepted) to a native SignalSpec; the signal name is the key.
1804
+ function signalSpecToNative(spec, key) {
1805
+ const s = spec !== null && typeof spec === 'object' ? spec : {};
1806
+ const out = { name: key };
1807
+ const params = s.param_types !== undefined ? s.param_types : s.paramTypes;
1808
+ if (Array.isArray(params)) {
1809
+ out.paramTypes = params.map(normalizeSignalType).filter((t) => t !== undefined);
1810
+ }
1811
+ const ret = normalizeSignalType(s.return_type !== undefined ? s.return_type : s.returnType);
1812
+ if (ret !== undefined) out.returnType = ret;
1813
+ if (typeof s.flags === 'number') out.flags = s.flags;
1814
+ return out;
1815
+ }
1816
+
1817
+ // Collect this class's OWN newly-declared `vfunc_<name>` methods into the native
1818
+ // vfuncs map (key = name without the `vfunc_` prefix). The native engine invokes
1819
+ // each override with `this` = the raw instance handle; re-wrap it so the user's
1820
+ // vfunc sees a usable class instance (own methods + property routing).
1821
+ // `super.vfunc_<name>(...)` inside the override resolves to a chain-up thunk on the
1822
+ // introspected base class's prototype (vfuncChainProto → native.callParentVfunc):
1823
+ // applying the user fn with a custom `this` keeps `super` bound to its lexical home
1824
+ // object (klass.prototype), so chain-up works through the .apply boundary.
1825
+ //
1826
+ // Multi-level boundary (G2): the walk STOPS at the first REGISTERED ancestor's
1827
+ // prototype. A registered ancestor already installed its own vfunc_* trampolines
1828
+ // into ITS GType's vtable, which this class inherits via the g_type_register_static
1829
+ // class-struct memcpy. Re-collecting them would install a DUPLICATE trampoline on
1830
+ // the leaf's GType whose captured parent IS the ancestor's own trampoline, so the
1831
+ // ancestor's `super.vfunc_*()` (running as the leaf's override) would re-enter
1832
+ // itself forever (stack overflow). Prototypes of UNREGISTERED mixin classes between
1833
+ // the leaf and the nearest registered ancestor ARE still collected — they have no
1834
+ // GType of their own, so their vfuncs only reach the vtable through the leaf.
1835
+ function collectVfuncs(klass) {
1836
+ const vfuncs = {};
1837
+ const seen = new Set();
1838
+ for (let p = klass.prototype; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
1839
+ const owner = Object.getOwnPropertyDescriptor(p, 'constructor')?.value;
1840
+ if (owner !== undefined && owner !== klass && registeredClasses.has(owner)) break;
1841
+ for (const key of Object.getOwnPropertyNames(p)) {
1842
+ if (!key.startsWith('vfunc_') || seen.has(key)) continue;
1843
+ const desc = Object.getOwnPropertyDescriptor(p, key);
1844
+ if (desc === undefined || typeof desc.value !== 'function') continue;
1845
+ seen.add(key);
1846
+ const userFn = desc.value;
1847
+ vfuncs[key.slice('vfunc_'.length)] = function (...args) {
1848
+ return userFn.apply(wrapInstance(this, klass.prototype), args);
1849
+ };
1850
+ }
1851
+ }
1852
+ return vfuncs;
1853
+ }
1854
+
1855
+ /**
1856
+ * GObject.registerClass — register a GObject subclass declared as a JS class.
1857
+ * Supports `registerClass(class)` and `registerClass(meta, class)` where meta is
1858
+ * `{ GTypeName?, Properties?, Signals?, ... }` (GJS allows both). Registers the
1859
+ * GType for the GIVEN class IN PLACE and returns that SAME class (GJS-faithful), so
1860
+ * `static { registerClass(…, X) }` (return discarded) leaves `X` itself bound to
1861
+ * the registered GType. `new X(props)` constructs the registered GType (routed via
1862
+ * the introspected base ctor on `new.target`), runs the user ctor body against the
1863
+ * canonical wrapper, and exposes the user class's own prototype methods + the
1864
+ * GObject property/signal surface. See the block comment above for the G3 contract.
1865
+ * @param {Function|Record<string, unknown>} metaOrClass
1866
+ * @param {Function} [maybeClass]
1867
+ * @returns {Function} the same `klass` (now registered)
1868
+ */
1869
+ function registerClass(metaOrClass, maybeClass) {
1870
+ let meta;
1871
+ let klass;
1872
+ if (typeof metaOrClass === 'function') {
1873
+ klass = metaOrClass;
1874
+ meta = {};
1875
+ } else {
1876
+ meta = metaOrClass !== null && typeof metaOrClass === 'object' ? metaOrClass : {};
1877
+ klass = maybeClass;
1878
+ }
1879
+ if (typeof klass !== 'function') {
1880
+ throw new TypeError('GObject.registerClass: expected a class to register');
1881
+ }
1882
+
1883
+ const parent = findParentGType(klass);
1884
+ if (parent === undefined) {
1885
+ throw new TypeError(
1886
+ 'GObject.registerClass: ' +
1887
+ (klass.name || 'the class') +
1888
+ ' must extend a node-gi GObject class (e.g. GObject.Object)',
1889
+ );
1890
+ }
1891
+
1892
+ const properties = [];
1893
+ if (meta.Properties !== null && typeof meta.Properties === 'object') {
1894
+ for (const key of Object.keys(meta.Properties)) {
1895
+ properties.push(paramSpecToNative(meta.Properties[key], key));
1896
+ }
1897
+ }
1898
+
1899
+ const signals = [];
1900
+ if (meta.Signals !== null && typeof meta.Signals === 'object') {
1901
+ for (const key of Object.keys(meta.Signals)) {
1902
+ signals.push(signalSpecToNative(meta.Signals[key], key));
1903
+ }
1904
+ }
1905
+
1906
+ const vfuncs = collectVfuncs(klass);
1907
+
1908
+ const gtypeName =
1909
+ typeof meta.GTypeName === 'string' && meta.GTypeName.length > 0 ? meta.GTypeName : klass.name;
1910
+ if (!gtypeName) {
1911
+ throw new TypeError('GObject.registerClass: a GTypeName is required for an anonymous class');
1912
+ }
1913
+
1914
+ // Gtk.Widget composite template (GJS-shaped meta: Template / Children /
1915
+ // InternalChildren / CssName). Template is a Uint8Array/Buffer of inline UI-XML,
1916
+ // a "resource:///…" path string, or an inline UI-XML string — passed through to
1917
+ // the engine, which installs it in class_init (set_template[_from_resource] +
1918
+ // bind_template_child_full) and runs gtk_widget_init_template at construction.
1919
+ const options = { properties, signals, vfuncs };
1920
+ if (meta.Template !== undefined && meta.Template !== null) options.template = meta.Template;
1921
+ if (typeof meta.CssName === 'string' && meta.CssName.length > 0) options.cssName = meta.CssName;
1922
+ const children = Array.isArray(meta.Children) ? meta.Children.filter((c) => typeof c === 'string') : [];
1923
+ const internalChildren = Array.isArray(meta.InternalChildren)
1924
+ ? meta.InternalChildren.filter((c) => typeof c === 'string')
1925
+ : [];
1926
+ if (children.length > 0) options.children = children;
1927
+ if (internalChildren.length > 0) options.internalChildren = internalChildren;
1928
+
1929
+ // Branch on the parent shape (findParentGType): a REGISTERED parent is subclassed
1930
+ // from its runtime GType handle (it has no introspection entry); an INTROSPECTED
1931
+ // parent is resolved by namespace+type. Construction (the leaf's registered GType
1932
+ // via the makeClass new.target routing) is already multi-level-correct either way.
1933
+ const typeHandle =
1934
+ parent.parentGTypeHandle !== undefined
1935
+ ? native.registerClassFromGType(gtypeName, parent.parentGTypeHandle, options)
1936
+ : native.registerClass(gtypeName, parent.parentNamespace, parent.parentType, options);
1937
+
1938
+ // G3: register the GType for the GIVEN class IN PLACE (no throwaway Subclass) and
1939
+ // return that same class. `$gtypeName`/`$gtype` are defined as OWN properties:
1940
+ // `klass` inherits a getter-only `$gtype` accessor from the introspected base ctor
1941
+ // (via the makeClass Proxy), so a plain `klass.$gtype = …` assignment would throw
1942
+ // in strict mode — defineProperty installs an own data property that shadows it.
1943
+ Object.defineProperty(klass, '$gtypeName', {
1944
+ value: gtypeName,
1945
+ configurable: true,
1946
+ writable: true,
1947
+ });
1948
+ // native.registerClass returns the registered GType as a tag-distinct node-gi
1949
+ // GType handle (G4) — surface it as `$gtype` (GObject.type_ensure(X.$gtype),
1950
+ // ParamSpec.object(..., X) all read it). It is the SAME handle constructType
1951
+ // consumes, so registration and `$gtype` share one carrier.
1952
+ Object.defineProperty(klass, '$gtype', {
1953
+ value: typeHandle,
1954
+ configurable: true,
1955
+ enumerable: false,
1956
+ writable: false,
1957
+ });
1958
+ // Record the registration so the introspected base ctor (makeClass) routes
1959
+ // `new X(args)` (where new.target === klass) to constructType(typeHandle, …) +
1960
+ // the canonical wrapper. Template children move here from the old Subclass.
1961
+ registeredClasses.set(klass, {
1962
+ typeHandle,
1963
+ children: children.length > 0 ? children : undefined,
1964
+ internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
1965
+ });
1966
+ return klass;
1967
+ }
1968
+
1969
+ // Merge convenience flag names UNDER an introspected enum: the introspected
1970
+ // members WIN (so every real bit — incl. LAX_VALIDATION / EXPLICIT_NOTIFY /
1971
+ // DETAILED / NO_RECURSE — keeps its true value), and the convenience names only
1972
+ // fill a gap if a platform's typelib happens not to introspect a combined value
1973
+ // (e.g. READWRITE). Returns the convenience table verbatim if introspection
1974
+ // yielded no usable enum.
1975
+ function mergeFlags(introspected, convenience) {
1976
+ if (introspected === null || typeof introspected !== 'object') return convenience;
1977
+ return Object.freeze({ ...convenience, ...introspected });
1978
+ }
1979
+
1980
+ // GObject.AccumulatorType — gjs's fake enum for signal accumulators (GObject.js,
1981
+ // "keep in sync with gi/object.c"). Not introspected; a pure convenience table.
1982
+ const AccumulatorType = Object.freeze({ NONE: 0, FIRST_WINS: 1, TRUE_HANDLED: 2 });
1983
+
1984
+ // GObject.signal_handlers_{block,unblock,disconnect}_by_func(instance, fn) — gjs's
1985
+ // GObject.js by-function handler ops. node-gi connects via PRIVATE closures, so the
1986
+ // function is resolved to its recorded handler ids (signalHandlerIds), then each is
1987
+ // blocked/unblocked via the introspected single-handler g_signal_handler_{block,
1988
+ // unblock} (verified to marshal a node-gi GObject IN-arg) or disconnected via
1989
+ // native.disconnectSignal. Returns the count of handlers matched, exactly as gjs's
1990
+ // g_signal_handlers_*_matched-based helpers do.
1991
+ function signalHandlersBlockByFunc(instance, fn) {
1992
+ const handle = unwrapArg(instance);
1993
+ const ids = handlerIdsForFunc(handle, fn);
1994
+ for (const id of ids) native.callFunction('GObject', 'signal_handler_block', [handle, id]);
1995
+ return ids.length;
1996
+ }
1997
+ function signalHandlersUnblockByFunc(instance, fn) {
1998
+ const handle = unwrapArg(instance);
1999
+ const ids = handlerIdsForFunc(handle, fn);
2000
+ for (const id of ids) native.callFunction('GObject', 'signal_handler_unblock', [handle, id]);
2001
+ return ids.length;
2002
+ }
2003
+ function signalHandlersDisconnectByFunc(instance, fn) {
2004
+ const handle = unwrapArg(instance);
2005
+ const ids = handlerIdsForFunc(handle, fn);
2006
+ for (const id of ids) {
2007
+ native.disconnectSignal(handle, id);
2008
+ forgetSignalHandlerId(handle, id);
2009
+ }
2010
+ return ids.length;
2011
+ }
2012
+ // Matches gjs's non-introspectable guard (GObject.js) — there is no func→data map.
2013
+ function signalHandlersDisconnectByData() {
2014
+ throw new Error(
2015
+ 'GObject.signal_handlers_disconnect_by_data() is not introspectable. Use ' +
2016
+ 'GObject.signal_handlers_disconnect_by_func() instead.',
2017
+ );
2018
+ }
2019
+
2020
+ // GObject.signal_connect / signal_connect_after / signal_emit_by_name — gjs's
2021
+ // GObject.js shims that call the instance's OWN connect/connect_after/emit (a
2022
+ // workaround for classes that shadow those names with their own methods). The
2023
+ // instance is the L1 wrapper, whose .connect records the id in the by-func registry.
2024
+ function signalConnect(object, name, handler) {
2025
+ return object.connect(name, handler);
2026
+ }
2027
+ function signalConnectAfter(object, name, handler) {
2028
+ return object.connect_after(name, handler);
2029
+ }
2030
+ function signalEmitByName(object, ...nameAndArgs) {
2031
+ return object.emit(...nameAndArgs);
2032
+ }
2033
+
2034
+ // Overlay the GJS `GObject` runtime statics on top of the introspected GObject
2035
+ // namespace proxy — ADDITIVELY. `registerClass` + `ParamSpec` + the signal-by-func
2036
+ // helpers + `Value` + `AccumulatorType` are genuinely new (or shadow a broken
2037
+ // introspected member — `Value`, whose struct has no `new()`). `ParamFlags` /
2038
+ // `SignalFlags` are FULL introspected bitfields: returning a hardcoded 5-member
2039
+ // table would make every other member read `undefined` (→ coerces to 0 in `FLAGS |
2040
+ // GObject.ParamFlags.X`, silently dropping the bit), so they resolve to the
2041
+ // introspected enum (convenience names merged underneath as a fallback).
2042
+ // `GObject.Object` etc. keep resolving from introspection (with .new added in
2043
+ // makeClass). Merged enums are cached so identity is stable.
2044
+ const OVERLAY_NAMES = new Set([
2045
+ 'registerClass',
2046
+ 'ParamSpec',
2047
+ 'ParamFlags',
2048
+ 'SignalFlags',
2049
+ 'Value',
2050
+ 'AccumulatorType',
2051
+ 'signal_handlers_block_by_func',
2052
+ 'signal_handlers_unblock_by_func',
2053
+ 'signal_handlers_disconnect_by_func',
2054
+ 'signal_handlers_disconnect_by_data',
2055
+ 'signal_connect',
2056
+ 'signal_connect_after',
2057
+ 'signal_emit_by_name',
2058
+ ]);
2059
+
2060
+ // The fundamental GObject.TYPE_* constants. GJS defines these in its GObject
2061
+ // override by looking each name up in libgobject AT RUNTIME (not hardcoding the
2062
+ // fundamental ints): refs/gjs/modules/core/overrides/GObject.js `_init` —
2063
+ // `GObject.TYPE_STRING = GObject.type_from_name('gchararray')`, and the
2064
+ // `_makeDummyClass(obj, name, upperName, gtypeName, …)` helper for the numeric/
2065
+ // char/gtype family. We resolve them the SAME way through the introspected
2066
+ // `GObject.type_from_name`, so each constant is the real, process-correct GType —
2067
+ // an OPAQUE GType handle exactly like GJS (`typeof GObject.TYPE_INT === 'object'`,
2068
+ // NOT the number 24; `GObject.type_from_name('gint') === GObject.TYPE_INT`), never
2069
+ // a hardcoded fundamental value. Map: constant name → the registered GType name.
2070
+ // (TYPE_JSOBJECT is intentionally omitted — 'JSObject' is a GJS-internal boxed type
2071
+ // that libgobject does not register on a plain node-gi host, so it has no correct
2072
+ // value here.)
2073
+ const GTYPE_CONSTANT_NAMES = {
2074
+ TYPE_NONE: 'void',
2075
+ TYPE_INTERFACE: 'GInterface',
2076
+ TYPE_CHAR: 'gchar',
2077
+ TYPE_UCHAR: 'guchar',
2078
+ TYPE_BOOLEAN: 'gboolean',
2079
+ TYPE_INT: 'gint',
2080
+ TYPE_UINT: 'guint',
2081
+ TYPE_LONG: 'glong',
2082
+ TYPE_ULONG: 'gulong',
2083
+ TYPE_INT64: 'gint64',
2084
+ TYPE_UINT64: 'guint64',
2085
+ TYPE_ENUM: 'GEnum',
2086
+ TYPE_FLAGS: 'GFlags',
2087
+ TYPE_FLOAT: 'gfloat',
2088
+ TYPE_DOUBLE: 'gdouble',
2089
+ TYPE_STRING: 'gchararray',
2090
+ TYPE_POINTER: 'gpointer',
2091
+ TYPE_BOXED: 'GBoxed',
2092
+ TYPE_PARAM: 'GParam',
2093
+ TYPE_OBJECT: 'GObject',
2094
+ TYPE_VARIANT: 'GVariant',
2095
+ TYPE_GTYPE: 'GType',
2096
+ TYPE_UNICHAR: 'gint',
2097
+ };
2098
+
2099
+ function isGObjectOverlayName(prop) {
2100
+ return (
2101
+ typeof prop === 'string' &&
2102
+ (OVERLAY_NAMES.has(prop) || Object.prototype.hasOwnProperty.call(GTYPE_CONSTANT_NAMES, prop))
2103
+ );
2104
+ }
2105
+
2106
+ function decorateGObjectNamespace(baseNs) {
2107
+ const cache = new Map();
2108
+ const resolve = (prop) => {
2109
+ if (cache.has(prop)) return cache.get(prop);
2110
+ let value;
2111
+ if (prop === 'registerClass') value = registerClass;
2112
+ else if (prop === 'ParamSpec') value = ParamSpec;
2113
+ else if (prop === 'ParamFlags') value = mergeFlags(baseNs.ParamFlags, ParamFlags);
2114
+ else if (prop === 'SignalFlags') value = mergeFlags(baseNs.SignalFlags, SignalFlags);
2115
+ else if (prop === 'Value') value = valueClass;
2116
+ else if (prop === 'AccumulatorType') value = AccumulatorType;
2117
+ else if (prop === 'signal_handlers_block_by_func') value = signalHandlersBlockByFunc;
2118
+ else if (prop === 'signal_handlers_unblock_by_func') value = signalHandlersUnblockByFunc;
2119
+ else if (prop === 'signal_handlers_disconnect_by_func') value = signalHandlersDisconnectByFunc;
2120
+ else if (prop === 'signal_handlers_disconnect_by_data') value = signalHandlersDisconnectByData;
2121
+ else if (prop === 'signal_connect') value = signalConnect;
2122
+ else if (prop === 'signal_connect_after') value = signalConnectAfter;
2123
+ else if (prop === 'signal_emit_by_name') value = signalEmitByName;
2124
+ else if (Object.prototype.hasOwnProperty.call(GTYPE_CONSTANT_NAMES, prop)) {
2125
+ // Resolve the real GType from libgobject via the introspected type_from_name
2126
+ // (0 / unregistered → null, matching object.cc's GType marshalling).
2127
+ value = baseNs.type_from_name(GTYPE_CONSTANT_NAMES[prop]);
2128
+ }
2129
+ cache.set(prop, value);
2130
+ return value;
2131
+ };
2132
+ return new Proxy(baseNs, {
2133
+ get(t, prop) {
2134
+ return isGObjectOverlayName(prop) ? resolve(prop) : t[prop];
2135
+ },
2136
+ has(t, prop) {
2137
+ return isGObjectOverlayName(prop) || prop in t;
2138
+ },
2139
+ });
2140
+ }
2141
+
178
2142
  function createNamespace(namespace) {
179
2143
  const cache = new Map();
180
2144
  return new Proxy(Object.create(null), {
@@ -228,16 +2192,42 @@ let loopAttached = false;
228
2192
  */
229
2193
  export function requireGi(namespace, version) {
230
2194
  native.requireNamespace(namespace, version);
231
- // Attach the libuv-in-GLib bridge once, so any later blocking GLib loop
232
- // (GLib.MainLoop.run / Gio.Application.run) keeps Node's event loop alive.
2195
+ // Attach the libuvGLib integration once. On Node this arms BOTH directions:
2196
+ // the uv-in-GLib bridge (a blocking GLib.MainLoop.run / Gio.Application.run
2197
+ // keeps Node's event loop alive) AND the uv-driven auto-pump (pending GLib
2198
+ // sources — Gio async completions, GLib timeouts/idles, DBus — dispatch from
2199
+ // Node's own loop, so async gi:// code works with NO explicit mainloop). This
2200
+ // is Node-only: Bun/Deno have no usable libuv (Deno exports no libuv symbols;
2201
+ // Bun panics on uv_backend_fd). There, GLib async is co-pumped from the runtime
2202
+ // loop via the portable GLib-iteration pump instead (see startMainContextPump /
2203
+ // runAsync).
233
2204
  if (!loopAttached) {
234
- native.startMainLoop();
2205
+ if (native.isNodeRuntime) {
2206
+ native.startMainLoop();
2207
+ // Bootstrap kick for the auto-pump: with ONLY unref'd handles active,
2208
+ // uv_run exits without running the pump's prepare/check phase at all, so
2209
+ // an otherwise-empty loop would never arm GLib's timer/fd wake-ups (a
2210
+ // pending top-level await on a GLib timeout would exit-13 unsettled).
2211
+ // 'beforeExit' fires exactly when the loop runs empty: drain what is
2212
+ // ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
2213
+ // uv timer revives the loop; otherwise the process exits normally.
2214
+ process.on('beforeExit', () => native.pumpKick());
2215
+ }
235
2216
  loopAttached = true;
236
2217
  }
237
2218
  const key = version ? `${namespace}@${version}` : namespace;
238
2219
  let ns = namespaceCache.get(key);
239
2220
  if (ns === undefined) {
240
2221
  ns = createNamespace(namespace);
2222
+ // The GObject namespace also carries the GJS runtime statics (registerClass +
2223
+ // ParamSpec/ParamFlags/SignalFlags) layered over its introspected members.
2224
+ if (namespace === 'GObject') ns = decorateGObjectNamespace(ns);
2225
+ // The GLib namespace carries the GJS-shaped GLib.Variant ergonomics
2226
+ // (new GLib.Variant(sig, value) + deepUnpack/unpack/recursiveUnpack) and the
2227
+ // GLib.Error class.
2228
+ else if (namespace === 'GLib') ns = decorateGLibNamespace(ns);
2229
+ // The Gio namespace carries Gio._promisify (async → Promise).
2230
+ else if (namespace === 'Gio') ns = decorateGioNamespace(ns);
241
2231
  namespaceCache.set(key, ns);
242
2232
  }
243
2233
  return ns;
@@ -250,7 +2240,12 @@ export function requireGi(namespace, version) {
250
2240
  * @returns {unknown}
251
2241
  */
252
2242
  export function unwrap(value) {
253
- return unwrapArg(value);
2243
+ // NOT unwrapArg: the public unwrap only extracts handles — it must never
2244
+ // replace a function with the engine-facing callback shim.
2245
+ if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
2246
+ return value[HANDLE];
2247
+ }
2248
+ return value;
254
2249
  }
255
2250
 
256
2251
  export default requireGi;