@gjsify/node-gi 0.64.0 → 0.64.2

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
@@ -7,6 +7,7 @@
7
7
  // Reference: gjs/modules/esm/gi.js for the require shape; node-gtk (romgrk, MIT) for the
8
8
  // binding lineage. Hand-authored JS — the package ships no build step.
9
9
  import * as native from './index.js';
10
+ import { ensureNative, onNativeLoaded } from './index.js';
10
11
  // Timers, `process` and the windowing env record come from the host seam (host.node.js
11
12
  // explains why they are the runtime's own, not the globals; host.nativescript.js is the
12
13
  // NativeScript Android variant).
@@ -48,11 +49,21 @@ function camelToSnake(name) {
48
49
  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
49
50
  }
50
51
 
52
+ // Memoized: the wrapper's get/set/has traps convert on EVERY property access, the
53
+ // answer depends on nothing but the name, and the set of names a program uses is
54
+ // bounded by its source text — three regex passes per access buy nothing.
55
+ const kebabNames = new Map();
56
+
51
57
  function toKebab(name) {
52
- return name
53
- .replace(/([A-Z])/g, '-$1')
54
- .replace(/_/g, '-')
55
- .toLowerCase();
58
+ let kebab = kebabNames.get(name);
59
+ if (kebab === undefined) {
60
+ kebab = name
61
+ .replace(/([A-Z])/g, '-$1')
62
+ .replace(/_/g, '-')
63
+ .toLowerCase();
64
+ kebabNames.set(name, kebab);
65
+ }
66
+ return kebab;
56
67
  }
57
68
 
58
69
  // Repeated passes of the same callback must marshal to the SAME native-facing function:
@@ -221,26 +232,10 @@ function wrapGError(handle, withPosition = false, hideAbove = wrapGError) {
221
232
  // domain must already name something, so `new GLib.Error()` is refused here rather
222
233
  // than producing a GError no `matches()` can ever hit.
223
234
  function newErrorLiteral(domain, code, message) {
224
- const quark = toDomainQuark(domain);
225
- if (native.callFunction('GLib', 'quark_to_string', [quark]) === null) {
235
+ if (native.callFunction('GLib', 'quark_to_string', [domain]) === null) {
226
236
  throw new TypeError(`Error.new_literal: ${domain} is not a valid domain`);
227
237
  }
228
- return native.callStaticMethod('GLib', 'Error', 'new_literal', [quark, code, message]);
229
- }
230
-
231
- // GJS's uint32 conversion for a `domain` argument (gi/js-value-inl.h,
232
- // js_value_to_c_checked): `undefined` and a non-finite NUMBER become 0 silently,
233
- // anything else goes through ToNumber and is REFUSED when it misses uint32 range.
234
- // node-gi's own NodeGiToUint32 has no range check, so the node-gi-only spelling
235
- // `err.matches('g-io-error-quark', code)` converted to NaN→0 and quietly matched
236
- // nothing; GJS cannot run that line at all, so neither can this.
237
- function toDomainQuark(domain) {
238
- if (domain === undefined || (typeof domain === 'number' && !Number.isFinite(domain))) return 0;
239
- const quark = Number(domain);
240
- if (Number.isNaN(quark) || quark < 0 || quark > 0xffffffff) {
241
- throw new Error('Argument domain: value is out of range for uint32');
242
- }
243
- return quark >>> 0;
238
+ return native.callStaticMethod('GLib', 'Error', 'new_literal', [domain, code, message]);
244
239
  }
245
240
 
246
241
  // `GLib.Error`: a plain FUNCTION, not a `class … extends Error` — GJS's
@@ -303,7 +298,7 @@ Object.defineProperties(GLibError.prototype, {
303
298
  `method GLib.Error.matches: At least 2 arguments required, but only ${arguments.length} passed`,
304
299
  );
305
300
  }
306
- return native.callBoxedMethod(this[HANDLE], 'matches', [toDomainQuark(domain), code]);
301
+ return native.callBoxedMethod(this[HANDLE], 'matches', [domain, code]);
307
302
  }),
308
303
  });
309
304
  defineErrorFields(GLibError.prototype, 'GLib.Error', undefined);
@@ -366,17 +361,19 @@ function makeErrorDomainClass(namespace, typeName, members, quark) {
366
361
  // Called by the engine on a failed GI invoke, with ownership of the GError* already
367
362
  // transferred to the boxed handle. Named, so the position properties report the
368
363
  // application frame rather than this one.
369
- native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
370
- // Same cross-version pairing: an addon older than this JS calls the builder with
371
- // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
372
- // there is no second `errorClass` argument to give it. Rebuilding the GError from
373
- // the fields keeps the ONE shape on that pairing too — it costs only the original
374
- // GError's address, which nothing can read.
375
- if (typeof handle === 'string') {
376
- return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
377
- }
378
- return wrapGError(handle, true, buildThrownGError);
379
- });
364
+ onNativeLoaded(() =>
365
+ native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
366
+ // Same cross-version pairing: an addon older than this JS calls the builder with
367
+ // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
368
+ // there is no second `errorClass` argument to give it. Rebuilding the GError from
369
+ // the fields keeps the ONE shape on that pairing too — it costs only the original
370
+ // GError's address, which nothing can read.
371
+ if (typeof handle === 'string') {
372
+ return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
373
+ }
374
+ return wrapGError(handle, true, buildThrownGError);
375
+ }),
376
+ );
380
377
 
381
378
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
382
379
  // record WHICH introspected class a registration belongs to — matched against the
@@ -396,6 +393,13 @@ const promisifiedWrappers = new WeakSet();
396
393
  const nativeThunks = new WeakSet();
397
394
 
398
395
  function wrapReturn(value) {
396
+ // EVERY engine value comes through here — each return, each OUT tuple element and
397
+ // each signal argument (wrapSignalCallback) — and the five tag probes below are
398
+ // five N-API crossings. A handle is always an OBJECT (a tagged napi External), so a
399
+ // primitive can answer for itself: a `notify::label` handler's `(emitter, pspec)`
400
+ // then costs two probes for the emitter instead of ten for the pair, and the string
401
+ // argument of an ordinary `emit('changed', 'x')` costs none.
402
+ if (value === null || typeof value !== 'object') return value;
399
403
  if (native.isGObjectHandle(value)) return wrapInstance(value);
400
404
  // A GLib.Variant is ALSO a boxed handle by tag, so it must be checked first to get
401
405
  // the Variant ergonomics rather than a plain method-routing proxy.
@@ -442,7 +446,7 @@ function resolveTemplateCallback(handle, handlerName) {
442
446
  return desc.value.apply(proxy, args.map(wrapReturn));
443
447
  };
444
448
  }
445
- native.setTemplateCallbackResolver(resolveTemplateCallback);
449
+ onNativeLoaded(() => native.setTemplateCallbackResolver(resolveTemplateCallback));
446
450
 
447
451
  // Run a registered class's JS constructor for a GObject the ENGINE instantiated from C
448
452
  // — typically a GtkBuilder composite-template InternalChild, which has no JS-`new` to
@@ -465,7 +469,7 @@ function runCtorForCObject(handle, gtypeName) {
465
469
  adoptHandle = prev;
466
470
  }
467
471
  }
468
- native.setConstructCallback(runCtorForCObject);
472
+ onNativeLoaded(() => native.setConstructCallback(runCtorForCObject));
469
473
 
470
474
  // A non-GObject GObject-fundamental (GskRenderNode, GdkEvent) as an OPAQUE,
471
475
  // round-trippable handle: the native tagged External already carries the pointer and
@@ -543,36 +547,51 @@ function wrapBoxed(handle) {
543
547
  // GParamSpec-typed value surface as. Carries [HANDLE] so it round-trips back into the
544
548
  // engine and `instanceof GObject.ParamSpec` recognises it. value_type/owner_type are
545
549
  // native GType handles, like gjs's GType objects.
550
+ // The accessors live HERE, on one shared prototype, and read the handle off `this`.
551
+ // Built per instance they were eleven fresh closures plus a twelve-slot object on
552
+ // EVERY notify emission — the engine hands the pspec to the handler whether or not
553
+ // the handler looks at it, so that allocation landed on every property write of the
554
+ // real-GObject engine. On gjs these same members live on GObject.ParamSpec.prototype,
555
+ // so prototype-held is also the closer shape.
556
+ const paramSpecProto = {
557
+ get name() {
558
+ return native.paramSpecProp(this[HANDLE], 'name');
559
+ },
560
+ get nick() {
561
+ return native.paramSpecProp(this[HANDLE], 'nick');
562
+ },
563
+ get blurb() {
564
+ return native.paramSpecProp(this[HANDLE], 'blurb');
565
+ },
566
+ get flags() {
567
+ return native.paramSpecProp(this[HANDLE], 'flags');
568
+ },
569
+ get value_type() {
570
+ return native.paramSpecProp(this[HANDLE], 'valueType');
571
+ },
572
+ get owner_type() {
573
+ return native.paramSpecProp(this[HANDLE], 'ownerType');
574
+ },
575
+ get default_value() {
576
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
577
+ },
578
+ get_name() {
579
+ return native.paramSpecProp(this[HANDLE], 'name');
580
+ },
581
+ get_nick() {
582
+ return native.paramSpecProp(this[HANDLE], 'nick');
583
+ },
584
+ get_blurb() {
585
+ return native.paramSpecProp(this[HANDLE], 'blurb');
586
+ },
587
+ get_default_value() {
588
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
589
+ },
590
+ };
591
+
546
592
  function wrapParamSpec(handle) {
547
- const prop = (which) => native.paramSpecProp(handle, which);
548
- const pspec = {
549
- [HANDLE]: handle,
550
- get name() {
551
- return prop('name');
552
- },
553
- get nick() {
554
- return prop('nick');
555
- },
556
- get blurb() {
557
- return prop('blurb');
558
- },
559
- get flags() {
560
- return prop('flags');
561
- },
562
- get value_type() {
563
- return prop('valueType');
564
- },
565
- get owner_type() {
566
- return prop('ownerType');
567
- },
568
- get default_value() {
569
- return wrapReturn(prop('defaultValue'));
570
- },
571
- get_name: () => prop('name'),
572
- get_nick: () => prop('nick'),
573
- get_blurb: () => prop('blurb'),
574
- get_default_value: () => wrapReturn(prop('defaultValue')),
575
- };
593
+ const pspec = Object.create(paramSpecProto);
594
+ pspec[HANDLE] = handle;
576
595
  return pspec;
577
596
  }
578
597
 
@@ -911,13 +930,50 @@ function decorateGioNamespace(baseNs) {
911
930
  });
912
931
  }
913
932
 
933
+ // The internals of the PROXY links on an introspected prototype chain: the Proxy
934
+ // itself -> its target plus the materializer its getOwnPropertyDescriptor trap runs
935
+ // (`null` for a Proxy that traps no descriptor read). Registered by
936
+ // makeClassPrototype and vfuncChainProto; read only by findProtoDescriptor.
937
+ //
938
+ // Why: a descriptor read THROUGH a Proxy costs ~110 ns of V8 invariant machinery on
939
+ // top of the handler, and an introspected chain is mostly Proxies. Measured on the
940
+ // bench's depth-3 subject, a full MISS walk took 328 ns — 154 ns on the
941
+ // makeClassPrototype link, 65 ns on vfuncChainProto, 12 ns on the one plain link.
942
+ // With the target in hand the same answer costs 27 ns, because the trap body is
943
+ // exactly `materialize(prop); return Reflect.getOwnPropertyDescriptor(target, prop)`
944
+ // and running it directly skips only the machinery, never a step of the computation.
945
+ //
946
+ // This is NOT a cache of the ANSWER: nothing is remembered between calls. The target
947
+ // is the object every mutation of the prototype lands on — none of the handlers trap
948
+ // `set`, `defineProperty`, `deleteProperty` or `setPrototypeOf`, so
949
+ // `Cls.prototype.m = f`, `Object.defineProperty(…)`, `delete` and a spliced chain
950
+ // write through to it — and the descriptor still comes from a fresh read of it. That
951
+ // has to hold, because GJS pins nothing: on gjs 1.88.1 every one of those mutations
952
+ // is observable on an already-wrapped instance (measured), so an answer cache would
953
+ // need an invalidation signal JS does not offer. test/proto-descriptor-liveness.test.mjs
954
+ // is the proof obligation.
955
+ const protoInternals = new WeakMap();
956
+
914
957
  // Walk a JS prototype chain (excluding Object.prototype) for an own property
915
958
  // descriptor of `prop` — used to surface a registerClass subclass's own methods
916
- // /getters/setters on the GObject wrapper.
959
+ // /getters/setters on the GObject wrapper. One full walk runs per property GET, per
960
+ // SET and per `emit`, so the Proxy bypass above is on the hottest path there is.
917
961
  function findProtoDescriptor(proto, prop) {
918
- for (let p = proto; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
919
- const d = Object.getOwnPropertyDescriptor(p, prop);
962
+ for (let p = proto; p !== null && p !== Object.prototype;) {
963
+ const internals = protoInternals.get(p);
964
+ if (internals === undefined) {
965
+ const d = Object.getOwnPropertyDescriptor(p, prop);
966
+ if (d !== undefined) return d;
967
+ p = Object.getPrototypeOf(p);
968
+ continue;
969
+ }
970
+ const { target, materialize } = internals;
971
+ if (materialize !== null) materialize(prop);
972
+ const d = Object.getOwnPropertyDescriptor(target, prop);
920
973
  if (d !== undefined) return d;
974
+ // The target's prototype, not the Proxy's: neither handler traps
975
+ // getPrototypeOf, so these are the same link — one fewer trap to pay.
976
+ p = Object.getPrototypeOf(target);
921
977
  }
922
978
  return undefined;
923
979
  }
@@ -1063,7 +1119,27 @@ function assignTemplateChildren(instance, handle, reg) {
1063
1119
  // resolves the same three spellings at the point of use instead — same reachability,
1064
1120
  // no accessor synthesis. Learn6502's SourceView is exactly why this matters: its
1065
1121
  // property is `line-numbers` and its accessor is `lineNumbers`.
1122
+ // Resolved per prototype, INCLUDING the "this class declares none" answer (null).
1123
+ // The native store calls runJsPropertySetter on every write of a custom property, so
1124
+ // without this a class with no accessor at all — the common case — paid three
1125
+ // prototype-chain walks and two regex rewrites per assignment to learn nothing. GJS
1126
+ // settles the same question once, at registerClass time (`_checkAccessors` mirrors
1127
+ // the declared spelling onto the other two); a prototype gaining an accessor later is
1128
+ // outside what either runtime honours.
1129
+ const propertySettersByProto = new WeakMap();
1130
+
1066
1131
  function findPropertySetter(userProto, propertyName) {
1132
+ let perProto = propertySettersByProto.get(userProto);
1133
+ if (perProto === undefined) propertySettersByProto.set(userProto, (perProto = new Map()));
1134
+ let setter = perProto.get(propertyName);
1135
+ if (setter === undefined) {
1136
+ setter = resolvePropertySetter(userProto, propertyName) ?? null;
1137
+ perProto.set(propertyName, setter);
1138
+ }
1139
+ return setter ?? undefined;
1140
+ }
1141
+
1142
+ function resolvePropertySetter(userProto, propertyName) {
1067
1143
  const underscored = propertyName.replace(/-/g, '_');
1068
1144
  const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
1069
1145
  for (const spelling of [propertyName, underscored, camel]) {
@@ -1095,9 +1171,9 @@ function runJsPropertySetter(handle, propertyName) {
1095
1171
  // function` until the next release republishes the binary. Every native call added
1096
1172
  // here degrades instead; without the callback, a post-construction set simply does
1097
1173
  // not reach the JS setter, which is what 0.50.0 did.
1098
- if (typeof native.setPropertySetCallback === 'function') {
1099
- native.setPropertySetCallback(runJsPropertySetter);
1100
- }
1174
+ onNativeLoaded(() => {
1175
+ if (typeof native.setPropertySetCallback === 'function') native.setPropertySetCallback(runJsPropertySetter);
1176
+ });
1101
1177
 
1102
1178
  // Which properties the construct-time replay must run.
1103
1179
  //
@@ -1306,6 +1382,7 @@ export function startMainContextPump() {
1306
1382
  // GLib's default main context — a JS-timer pump would only iterate the
1307
1383
  // context recursively from within its own dispatch.
1308
1384
  if (hostPumpsItself()) return () => {};
1385
+ ensureNative();
1309
1386
  pumpRefCount++;
1310
1387
  if (pumpTimer === null) {
1311
1388
  if (pendingView === null) pendingView = native.makePumpPendingCount();
@@ -1548,6 +1625,30 @@ function wrapInstance(handle, userProto) {
1548
1625
  materialised.add(name);
1549
1626
  return had;
1550
1627
  };
1628
+ // The `connect`/`connect_after`/`emit`/`disconnect` thunks of this wrapper, each
1629
+ // built on its first use (see the `get` trap). A null prototype so an unrelated
1630
+ // name — `constructor`, `toString` — cannot be mistaken for a built shortcut.
1631
+ const signalShortcuts = Object.create(null);
1632
+ // The GI-method thunks of this wrapper, built on first use, `null` for a name
1633
+ // that resolves to no introspected method. A thunk is a pure function of
1634
+ // (handle, name) and presence is already memoized per GType (instanceHasMethod),
1635
+ // so the only thing that changes by keeping it is IDENTITY — and a STABLE one is
1636
+ // the GJS shape: there the method lives on the prototype, so `obj.m === obj.m`
1637
+ // (the reason a user prototype's own function is handed back unbound, #1175).
1638
+ // Built fresh, every `obj.m(…)` in a loop allocated a closure and re-asked for
1639
+ // the type name across the N-API boundary. Null prototype: `constructor` and
1640
+ // friends must not read as a cached thunk.
1641
+ const giMethodThunks = Object.create(null);
1642
+ const giMethodThunk = (prop) => {
1643
+ let thunk = giMethodThunks[prop];
1644
+ if (thunk === undefined) {
1645
+ thunk = instanceHasMethod(handle, prop)
1646
+ ? (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)))
1647
+ : null;
1648
+ giMethodThunks[prop] = thunk;
1649
+ }
1650
+ return thunk ?? undefined;
1651
+ };
1551
1652
  const proxy = new Proxy(target, {
1552
1653
  get(t, prop) {
1553
1654
  if (prop === HANDLE) return handle;
@@ -1561,10 +1662,16 @@ function wrapInstance(handle, userProto) {
1561
1662
  // introspectable ancestor (GObject.Object), yet on gjs 1.88.1
1562
1663
  // `socks5.connect` IS g_proxy_connect (Gio.Proxy, 3 IN args) — measured.
1563
1664
  // The same native walk the GI-method fallback below uses sees it.
1564
- if (instanceHasMethod(handle, prop)) {
1565
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1566
- }
1665
+ const method = giMethodThunk(prop);
1666
+ if (method !== undefined) return method;
1567
1667
  }
1668
+ // The four signal shortcuts below close over nothing but `handle`, so they
1669
+ // are per-wrapper constants — yet the switch rebuilt one on EVERY access,
1670
+ // i.e. one throwaway closure per `obj.emit(…)` in a loop. Built once here
1671
+ // and reused; on gjs these live on GObject.Object.prototype and have stable
1672
+ // identity too. Lazy, because most wrappers never reach for one.
1673
+ const shortcut = signalShortcuts[prop];
1674
+ if (shortcut !== undefined) return shortcut;
1568
1675
  switch (prop) {
1569
1676
  case '$typeName':
1570
1677
  // The instance's concrete RUNTIME GType name —
@@ -1581,24 +1688,25 @@ function wrapInstance(handle, userProto) {
1581
1688
  case 'connect':
1582
1689
  // Record the (user fn → id) mapping so signal_handlers_*_by_func can
1583
1690
  // resolve the private-closure handler back to its ids (see the registry).
1584
- return (signal, cb) => {
1691
+ return (signalShortcuts.connect = (signal, cb) => {
1585
1692
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
1586
1693
  recordSignalHandler(handle, cb, id);
1587
1694
  return id;
1588
- };
1695
+ });
1589
1696
  case 'connect_after':
1590
- return (signal, cb) => {
1697
+ return (signalShortcuts.connect_after = (signal, cb) => {
1591
1698
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
1592
1699
  recordSignalHandler(handle, cb, id);
1593
1700
  return id;
1594
- };
1701
+ });
1595
1702
  case 'emit':
1596
- return (signal, ...args) => wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
1703
+ return (signalShortcuts.emit = (signal, ...args) =>
1704
+ wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args))));
1597
1705
  case 'disconnect':
1598
- return (id) => {
1706
+ return (signalShortcuts.disconnect = (id) => {
1599
1707
  forgetSignalHandlerId(handle, id);
1600
1708
  return native.disconnectSignal(handle, id);
1601
- };
1709
+ });
1602
1710
  default:
1603
1711
  break;
1604
1712
  }
@@ -1674,10 +1782,7 @@ function wrapInstance(handle, userProto) {
1674
1782
  // camelCase like Gwebgl's `getString`) and only falls back to the
1675
1783
  // snake_case alias. Converting here would destroy a literal camelCase
1676
1784
  // GI name.
1677
- if (instanceHasMethod(handle, prop)) {
1678
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1679
- }
1680
- return undefined;
1785
+ return giMethodThunk(prop);
1681
1786
  },
1682
1787
  set(t, prop, value) {
1683
1788
  if (typeof prop === 'string') {
@@ -1748,7 +1853,8 @@ function wrapInstance(handle, userProto) {
1748
1853
  // the introspected parent's `vfunc_x` is a thunk into the actual C vtable entry.
1749
1854
  // Non-`vfunc_` lookups fall through to Object.prototype (so the base prototype
1750
1855
  // keeps its normal object behaviour).
1751
- const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1856
+ const vfuncChainTarget = Object.create(Object.prototype);
1857
+ const vfuncChainProto = new Proxy(vfuncChainTarget, {
1752
1858
  get(target, prop, receiver) {
1753
1859
  if (typeof prop === 'string' && prop.startsWith('vfunc_')) {
1754
1860
  const vfuncName = prop.slice('vfunc_'.length);
@@ -1763,6 +1869,10 @@ const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1763
1869
  return Reflect.get(target, prop, receiver);
1764
1870
  },
1765
1871
  });
1872
+ // No descriptor trap here: a `vfunc_*` access is synthesized by `get` alone and was
1873
+ // never reported as a descriptor, so the walk's answer for this link always came from
1874
+ // the target. `null` materializer = "read the target, that is the whole trap".
1875
+ protoInternals.set(vfuncChainProto, { target: vfuncChainTarget, materialize: null });
1766
1876
 
1767
1877
  // Surface a lazy `$gtype` getter on an introspected class/struct ctor: the real
1768
1878
  // GObject GType (a node-gi GType handle), resolved on first read via the engine
@@ -1930,7 +2040,7 @@ function makeClassPrototype(namespace, typeName) {
1930
2040
  }
1931
2041
  return arity >= 0 && define(prop, makeGiMethod(where, prop, arity));
1932
2042
  };
1933
- return new Proxy(target, {
2043
+ const proto = new Proxy(target, {
1934
2044
  get(t, prop, receiver) {
1935
2045
  materialize(prop);
1936
2046
  return Reflect.get(t, prop, receiver);
@@ -1943,6 +2053,10 @@ function makeClassPrototype(namespace, typeName) {
1943
2053
  return materialize(prop) || Reflect.has(t, prop);
1944
2054
  },
1945
2055
  });
2056
+ // Let findProtoDescriptor run the descriptor trap's body directly on the target —
2057
+ // same two steps, without V8's per-trap invariant checks (see protoInternals).
2058
+ protoInternals.set(proto, { target, materialize });
2059
+ return proto;
1946
2060
  }
1947
2061
 
1948
2062
  function makeClass(namespace, typeName) {
@@ -2462,6 +2576,8 @@ function paramSpecGTyped(type) {
2462
2576
  const ParamSpec = Object.freeze({
2463
2577
  string: paramSpecPlain('string'),
2464
2578
  boolean: paramSpecPlain('boolean'),
2579
+ char: paramSpecRanged('char'),
2580
+ uchar: paramSpecRanged('uchar'),
2465
2581
  int: paramSpecRanged('int'),
2466
2582
  uint: paramSpecRanged('uint'),
2467
2583
  int64: paramSpecRanged('int64'),
@@ -3126,6 +3242,7 @@ function mirrorWindowingEnvIntoCrt() {
3126
3242
  * @returns {Record<string, unknown>}
3127
3243
  */
3128
3244
  export function requireGi(namespace, version) {
3245
+ ensureNative();
3129
3246
  native.requireNamespace(namespace, version);
3130
3247
  // The one moment the win32 CRT-environment mirror can run: the addon is loaded, so there
3131
3248
  // is a GLib to call, and nothing has built a font map or loaded a fontconfig
@@ -44,6 +44,12 @@
44
44
  /** @type {'nativescript'} */
45
45
  export const RUNTIME = 'nativescript';
46
46
 
47
+ /**
48
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
49
+ * @type {boolean}
50
+ */
51
+ export const LAZY_NATIVE = true;
52
+
47
53
  // Timers from the global scope: NativeScript has no node:timers, and there is no
48
54
  // GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
49
55
  export const setImmediate =
package/host.node.js CHANGED
@@ -55,6 +55,12 @@ export const RUNTIME =
55
55
  ? 'deno'
56
56
  : 'node';
57
57
 
58
+ /**
59
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
60
+ * @type {boolean}
61
+ */
62
+ export const LAZY_NATIVE = false;
63
+
58
64
  // GJS host mode: `GjsifyNapi.init()` installs the loader as
59
65
  // `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
60
66
  // @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib