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