@gjsify/node-gi 0.63.0 → 0.64.1

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
@@ -48,11 +48,21 @@ function camelToSnake(name) {
48
48
  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
49
49
  }
50
50
 
51
+ // Memoized: the wrapper's get/set/has traps convert on EVERY property access, the
52
+ // answer depends on nothing but the name, and the set of names a program uses is
53
+ // bounded by its source text — three regex passes per access buy nothing.
54
+ const kebabNames = new Map();
55
+
51
56
  function toKebab(name) {
52
- return name
53
- .replace(/([A-Z])/g, '-$1')
54
- .replace(/_/g, '-')
55
- .toLowerCase();
57
+ let kebab = kebabNames.get(name);
58
+ if (kebab === undefined) {
59
+ kebab = name
60
+ .replace(/([A-Z])/g, '-$1')
61
+ .replace(/_/g, '-')
62
+ .toLowerCase();
63
+ kebabNames.set(name, kebab);
64
+ }
65
+ return kebab;
56
66
  }
57
67
 
58
68
  // Repeated passes of the same callback must marshal to the SAME native-facing function:
@@ -118,82 +128,249 @@ function unwrapProps(props) {
118
128
  return out;
119
129
  }
120
130
 
121
- // An enum object's GError-domain descriptor ({ name, quark }), so GLib.Error.matches
122
- // can resolve an error-enum (Gio.IOErrorEnum) to the domain it represents. Attached by
123
- // makeEnum when the enum is registered as a GError domain.
124
- // A registry symbol, so a host-side stand-in for an enum the typelib lacks (Gtk.DialogError on
125
- // Android) can declare itself an error domain without importing this module.
126
- const ERROR_DOMAIN = Symbol.for('nodeGiErrorDomain');
127
-
128
- // GLib.Error, L1: the engine throws one of these on a failed sync GI invoke (via the
129
- // builder registered below) and `requireGi('GLib').Error` is this constructor, so a
130
- // caught error is `instanceof GLib.Error` exactly as under GJS.
131
+ // ---- GLib.Error (L1) ----
132
+ //
133
+ // ONE object for every GError, as on GJS: a boxed handle over a real `GError*` whose
134
+ // CLASS is the error DOMAIN's class (`Gio.IOErrorEnum`) when the quark names a
135
+ // registered domain, and `GLib.Error` otherwise. There is no JS `Error` subclass
136
+ // behind it — `Error.isError()` is true while `instanceof Error` is FALSE, a
137
+ // combination only `Reflect.construct(Error, [], Ctor)` reaches (GJS gets it from
138
+ // `JSCLASS_IS_DOMJSCLASS` on ErrorBase::klass).
131
139
  //
132
- // Deliberate divergence from GJS: `.domain` is the quark NAME string (GJS's is the
133
- // numeric GQuark), with the numeric quark on `.domainQuark`. `matches()` accepts the
134
- // domain as an error-enum object, a name string or a numeric quark.
135
-
136
- // Resolve a `matches` domain argument to a { name?, quark? } descriptor.
137
- function errorDomainOf(domain) {
138
- if (domain === null || domain === undefined) return null;
139
- if (typeof domain === 'object' && domain[ERROR_DOMAIN] !== undefined) return domain[ERROR_DOMAIN];
140
- if (typeof domain === 'string') return { name: domain };
141
- if (typeof domain === 'number') return { quark: domain };
142
- return null;
143
- }
144
-
145
- class GLibError extends Error {
146
- // `new GLib.Error(domain, code, message)`, where `domain` may be an error-enum
147
- // object, a quark name string, or a numeric quark.
148
- constructor(domain, code, message) {
149
- super(typeof message === 'string' ? message : '');
150
- this.name = 'GLib.Error';
151
- const d = errorDomainOf(domain);
152
- this.domain = d !== null && d.name !== undefined ? d.name : typeof domain === 'string' ? domain : undefined;
153
- this.domainQuark =
154
- d !== null && d.quark !== undefined ? d.quark : typeof domain === 'number' ? domain : undefined;
155
- this.code = code;
140
+ // Reference: refs/gjs/gi/gerror.cpp (ErrorBase/ErrorInstance/ErrorPrototype) and its
141
+ // `gjs_define_error_properties`, plus the GLib.js override's `Error.new_literal`
142
+ // guard and `Error.prototype.matches`. The old divergence — an `Error` subclass with
143
+ // `.domain` the quark NAME and the number on a node-gi-only `.domainQuark`, beside a
144
+ // SECOND plain-boxed shape for a marshalled one — is gone with no compat shim;
145
+ // test/gerror-identity.test.mjs is the contract.
146
+
147
+ // quark → the class makeErrorDomainClass built for it. A cached `null` means "asked
148
+ // the repository, this quark has no domain class".
149
+ const errorClassByQuark = new Map();
150
+
151
+ // GJS defines the error methods enumerable and non-configurable, which is observable:
152
+ // a formatter listing a prototype's methods sees them, and nothing can redefine them.
153
+ const errorMethod = (value) => ({ value, enumerable: true, writable: true, configurable: false });
154
+
155
+ // The class for a GError's domain quark, or null when no typelib registers one (a
156
+ // private quark from `GLib.quark_from_string`), in which case the error is a plain
157
+ // `GLib.Error`.
158
+ function errorClassForQuark(quark) {
159
+ const cached = errorClassByQuark.get(quark);
160
+ if (cached !== undefined) return cached;
161
+ // Nothing has touched the enum yet, so the map cannot know it: ask the repository
162
+ // which { namespace, name } registered the domain and READ that member — makeEnum
163
+ // registers the class it builds on the way out. Without this an error whose domain
164
+ // enum the application never named would fall back to the base class, which is
165
+ // exactly the `catch (e) { if (e instanceof Gio.IOErrorEnum) }` case.
166
+ // Guarded for the cross-version pairing the `native.setPropertySetCallback` guard
167
+ // below describes: an addon older than this JS does not export it. Degrading means
168
+ // falling back to what the map alone knows — a domain whose enum `makeEnum` has
169
+ // already realized is in it, one nothing has touched yet lands on the base class.
170
+ // Nothing is cached in that case, so a later `makeEnum` still registers.
171
+ if (typeof native.errorDomainInfo !== 'function') return null;
172
+ const info = native.errorDomainInfo(quark);
173
+ errorClassByQuark.set(quark, null); // also breaks the cycle while the namespace loads
174
+ if (info === null) return null;
175
+ // Namespace MEMBERS are lazy (createNamespace resolves on first read), so the
176
+ // namespace object alone builds nothing — the member read is what runs makeEnum.
177
+ // oxlint-disable-next-line no-unused-expressions -- the READ is the side effect: it runs makeEnum, which registers the class
178
+ requireGi(info.namespace)[info.name];
179
+ return errorClassByQuark.get(quark);
180
+ }
181
+
182
+ // The first parseable `file:line:column` in a V8 stack. The FRAMES stay V8's — only
183
+ // their position is lifted out, and the frame it lands on is node-gi's own dispatch
184
+ // site for a marshalled error (see docs/node-gi-gjs-surface.md).
185
+ function topFrame(stack) {
186
+ for (const line of String(stack).split('\n')) {
187
+ const match = /\(?([^()\s]+):(\d+):(\d+)\)?$/.exec(line.trim());
188
+ if (match !== null) {
189
+ return { fileName: match[1], lineNumber: Number(match[2]), columnNumber: Number(match[3]) };
190
+ }
156
191
  }
157
-
158
- // g_error_new_literal, which GJS exposes as a static on GLib.Error because it is in
159
- // `GLib-2.0.gir` — and which is the only spelling `@girs/glib-2.0` TYPES, its
160
- // `constructor(properties?)` signature rejecting the three positional arguments the
161
- // constructor above actually takes. So a cross-runtime source that has to build a GError
162
- // (a test asserting on an error a host cannot produce, say a CoreText font map's
163
- // G_IO_ERROR_NOT_SUPPORTED) type-checks only against this, and without it the gjs leg
164
- // passes and the node leg dies with `Error.new_literal is not a function`. Same arguments,
165
- // same result — the constructor is the implementation.
166
- static new_literal(domain, code, message) {
167
- return new GLibError(domain, code, message);
192
+ return { fileName: undefined, lineNumber: undefined, columnNumber: undefined };
193
+ }
194
+
195
+ // GJS's `gjs_define_error_properties`: four own ENUMERABLE value properties saying
196
+ // where the error came from. Enumerable is the load-bearing half — V8's own `stack`
197
+ // is not, so a `{...err}` spread or a structured log carried the frames on GJS and
198
+ // nothing here.
199
+ function definePositionProperties(error, hideAbove) {
200
+ Error.captureStackTrace(error, hideAbove);
201
+ const stack = error.stack;
202
+ const frame = topFrame(stack);
203
+ for (const [name, value] of [
204
+ ['stack', stack],
205
+ ['fileName', frame.fileName],
206
+ ['lineNumber', frame.lineNumber],
207
+ ['columnNumber', frame.columnNumber],
208
+ ]) {
209
+ Object.defineProperty(error, name, { value, enumerable: true, writable: true, configurable: true });
168
210
  }
211
+ }
212
+
213
+ // Wrap a `GError*` handle as the error object. `withPosition` is GJS's asymmetry: the
214
+ // position properties are defined when the error was THROWN or CONSTRUCTED, and
215
+ // otherwise only for an UNKNOWN domain — because GJS then has no domain metadata to
216
+ // build a class from and reaches the GError through its generic struct wrapper, which
217
+ // defines them (gi/struct.cpp). `hideAbove` is the frame the position should be
218
+ // measured at, so `new GLib.Error(…)` reports the caller and not this file.
219
+ function wrapGError(handle, withPosition = false, hideAbove = wrapGError) {
220
+ const cls = errorClassForQuark(native.getBoxedField(handle, 'domain'));
221
+ const error = Reflect.construct(Error, [], cls ?? GLibError);
222
+ error[HANDLE] = handle;
223
+ if (withPosition || cls === null) definePositionProperties(error, hideAbove);
224
+ // `Reflect.construct(Error, …)` always leaves V8's own `stack` behind, and GJS has
225
+ // none on this path at all.
226
+ else delete error.stack;
227
+ return error;
228
+ }
169
229
 
170
- // g_error_matches: true when the error's domain AND code both match.
171
- matches(domain, code) {
172
- const d = errorDomainOf(domain);
173
- if (d === null) return false;
174
- const domainMatch =
175
- (d.name !== undefined && d.name === this.domain) || (d.quark !== undefined && d.quark === this.domainQuark);
176
- return domainMatch && code === this.code;
230
+ // g_error_new_literal, behind the quark guard GJS's GLib.js puts in front of it: the
231
+ // domain must already name something, so `new GLib.Error()` is refused here rather
232
+ // than producing a GError no `matches()` can ever hit.
233
+ function newErrorLiteral(domain, code, message) {
234
+ if (native.callFunction('GLib', 'quark_to_string', [domain]) === null) {
235
+ throw new TypeError(`Error.new_literal: ${domain} is not a valid domain`);
177
236
  }
237
+ return native.callStaticMethod('GLib', 'Error', 'new_literal', [domain, code, message]);
238
+ }
178
239
 
179
- toString() {
180
- return `GLib.Error ${this.domain}: ${this.message}`;
240
+ // `GLib.Error`: a plain FUNCTION, not a `class … extends Error` — GJS's
241
+ // GLib.Error.prototype has Object.prototype on its chain, not Error.prototype.
242
+ function GLibError(domain, code, message) {
243
+ // The spelling `@girs/glib-2.0`'s `constructor(properties?)` signature invites,
244
+ // and which GJS runs into its generic boxed-struct constructor with: a GError
245
+ // field is not writable. Replicated verbatim so the failure reads the same.
246
+ if (arguments.length === 1 && typeof domain === 'object' && domain !== null) {
247
+ throw new Error('Writing field GLib.Error.message is not supported');
181
248
  }
249
+ return wrapGError(newErrorLiteral(domain, code, message), true, GLibError);
182
250
  }
251
+ Object.defineProperty(GLibError, 'name', { value: 'GLib_Error', configurable: true });
252
+ defineLazyGType(GLibError, 'GLib', 'Error');
183
253
 
184
- // Called by the engine on a failed GI invoke, with the GError's authoritative fields.
185
- function buildGError(domainName, domainQuark, code, message) {
186
- const error = new GLibError(domainName, code, message);
187
- error.domain = domainName;
188
- error.domainQuark = domainQuark;
189
- return error;
254
+ // The static GJS exposes because `g_error_new_literal` is in GLib-2.0.gir — and the
255
+ // only spelling `@girs/glib-2.0` TYPES, so a cross-runtime source that has to build a
256
+ // GError type-checks against this rather than the three positional constructor args.
257
+ GLibError.new_literal = function new_literal(domain, code, message) {
258
+ return wrapGError(newErrorLiteral(domain, code, message));
259
+ };
260
+
261
+ // `domain`/`code`/`message` read straight off the `GError*` through a PROTOTYPE
262
+ // accessor, as GJS does: getter-only (a consumer "normalising" an error by assigning
263
+ // `.domain` is a TypeError, not a silent mutation) and own to each prototype, so the
264
+ // refusal below can name the right one.
265
+ function defineErrorFields(proto, displayName, protoQuark) {
266
+ for (const field of ['domain', 'code', 'message']) {
267
+ Object.defineProperty(proto, field, {
268
+ enumerable: true,
269
+ configurable: false,
270
+ get() {
271
+ const handle = this[HANDLE];
272
+ if (handle !== undefined) return native.getBoxedField(handle, field);
273
+ // A PROTOTYPE has no GError* behind it, and refusing is what keeps a
274
+ // prototype walk (util.inspect, a formatter collecting accessors) from
275
+ // reading a wild pointer. GJS still answers a DOMAIN prototype's
276
+ // `domain` — that is where `matches(Gio.IOErrorEnum, …)` reads the
277
+ // quark from — and refuses the other two.
278
+ if (field === 'domain' && protoQuark !== undefined) return protoQuark;
279
+ throw new Error(`Can't get a field on ${displayName}.prototype; only on instances`);
280
+ },
281
+ });
282
+ }
190
283
  }
191
- // The CLASS is registered with the builder, because the engine needs it the other
192
- // way round too: a `GError`-typed IN argument recognises a caught (or `new`-ed)
193
- // GLib.Error by `instanceof` and rebuilds a real GError from its fields, so an
194
- // application can hand back the error it is holding (`GLib.propagate_error`,
195
- // `Gst.Message.new_error`). One call, so builder and class cannot drift apart.
196
- native.setErrorBuilder(buildGError, GLibError);
284
+
285
+ Object.defineProperties(GLibError.prototype, {
286
+ copy: errorMethod(function copy() {
287
+ return wrapGError(native.callBoxedMethod(this[HANDLE], 'copy', []));
288
+ }),
289
+ // g_error_free, for parity. It carries the same double-free footgun as on GJS: the
290
+ // handle still owns the GError* and frees it again on GC.
291
+ free: errorMethod(function free() {
292
+ return native.callBoxedMethod(this[HANDLE], 'free', []);
293
+ }),
294
+ matches: errorMethod(function matches(domain, code) {
295
+ if (arguments.length < 2) {
296
+ throw new TypeError(
297
+ `method GLib.Error.matches: At least 2 arguments required, but only ${arguments.length} passed`,
298
+ );
299
+ }
300
+ return native.callBoxedMethod(this[HANDLE], 'matches', [domain, code]);
301
+ }),
302
+ });
303
+ defineErrorFields(GLibError.prototype, 'GLib.Error', undefined);
304
+ // Defined SEPARATELY here and on each domain prototype, which is why on GJS
305
+ // `err.toString !== GLib.Error.prototype.toString` for a known domain: there is no
306
+ // sentinel branch, the class that knows its name owns the method.
307
+ Object.defineProperty(
308
+ GLibError.prototype,
309
+ 'toString',
310
+ errorMethod(function toString() {
311
+ return `GLib.Error ${native.callFunction('GLib', 'quark_to_string', [this.domain])}: ${this.message}`;
312
+ }),
313
+ );
314
+
315
+ // An error-domain enum (`Gio.IOErrorEnum`) is BOTH the enum and the CLASS of its
316
+ // errors on GJS — `instanceof` discriminates the domain and `new Gio.IOErrorEnum({
317
+ // message, code })` builds one — so it has to be a function with a prototype chained
318
+ // to GLib.Error.prototype. A frozen object, which is what makeEnum hands out
319
+ // otherwise, can be neither.
320
+ function makeErrorDomainClass(namespace, typeName, members, quark) {
321
+ const displayName = `${namespace}.${typeName}`;
322
+ const cls = function (props) {
323
+ // The wording is the contract — a cross-runtime test asserting on a
324
+ // constructor refusal compares the string — and `message` is checked BEFORE
325
+ // `code`, so a call naming neither names `message`.
326
+ if (arguments.length !== 1 || typeof props !== 'object' || props === null) {
327
+ throw new Error('Invalid parameters passed to GError constructor, expected one object');
328
+ }
329
+ if (typeof props.message !== 'string') {
330
+ throw new Error("No property 'message' in GError constructor (or it was not a string)");
331
+ }
332
+ if (!Number.isInteger(props.code) || props.code < -0x80000000 || props.code > 0x7fffffff) {
333
+ throw new Error("No property 'code' in GError constructor (or it was not a 32-bit integer)");
334
+ }
335
+ return wrapGError(newErrorLiteral(quark, props.code, props.message), true, cls);
336
+ };
337
+ Object.defineProperty(cls, 'name', { value: `${namespace}_${typeName}`, configurable: true });
338
+ Object.assign(cls, members);
339
+ // G_TYPE_ERROR, NOT the enum's own GIOErrorEnum: on GJS an error domain is an
340
+ // Error class before it is an enum (ErrorBase::to_gtype), and the enum GType it
341
+ // shadows is unreachable there, so nothing written for GJS can ask for it.
342
+ defineLazyGType(cls, 'GLib', 'Error');
343
+ // A STATIC on GJS (ErrorBase::value_of reads `this.constructor.prototype`'s
344
+ // domain), and what makes both `Number(Gio.IOErrorEnum)` and
345
+ // `err.matches(Gio.IOErrorEnum, code)` answer the quark with no registry lookup.
346
+ cls.valueOf = () => quark;
347
+ Object.setPrototypeOf(cls.prototype, GLibError.prototype);
348
+ defineErrorFields(cls.prototype, displayName, quark);
349
+ Object.defineProperty(
350
+ cls.prototype,
351
+ 'toString',
352
+ errorMethod(function toString() {
353
+ return `${displayName}: ${this.message}`;
354
+ }),
355
+ );
356
+ errorClassByQuark.set(quark, cls);
357
+ return cls;
358
+ }
359
+
360
+ // Called by the engine on a failed GI invoke, with ownership of the GError* already
361
+ // transferred to the boxed handle. Named, so the position properties report the
362
+ // application frame rather than this one.
363
+ native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
364
+ // Same cross-version pairing: an addon older than this JS calls the builder with
365
+ // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
366
+ // there is no second `errorClass` argument to give it. Rebuilding the GError from
367
+ // the fields keeps the ONE shape on that pairing too — it costs only the original
368
+ // GError's address, which nothing can read.
369
+ if (typeof handle === 'string') {
370
+ return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
371
+ }
372
+ return wrapGError(handle, true, buildThrownGError);
373
+ });
197
374
 
198
375
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
199
376
  // record WHICH introspected class a registration belongs to — matched against the
@@ -213,6 +390,13 @@ const promisifiedWrappers = new WeakSet();
213
390
  const nativeThunks = new WeakSet();
214
391
 
215
392
  function wrapReturn(value) {
393
+ // EVERY engine value comes through here — each return, each OUT tuple element and
394
+ // each signal argument (wrapSignalCallback) — and the five tag probes below are
395
+ // five N-API crossings. A handle is always an OBJECT (a tagged napi External), so a
396
+ // primitive can answer for itself: a `notify::label` handler's `(emitter, pspec)`
397
+ // then costs two probes for the emitter instead of ten for the pair, and the string
398
+ // argument of an ordinary `emit('changed', 'x')` costs none.
399
+ if (value === null || typeof value !== 'object') return value;
216
400
  if (native.isGObjectHandle(value)) return wrapInstance(value);
217
401
  // A GLib.Variant is ALSO a boxed handle by tag, so it must be checked first to get
218
402
  // the Variant ergonomics rather than a plain method-routing proxy.
@@ -301,6 +485,12 @@ function wrapFundamental(handle) {
301
485
  // boxedMemberKind checks methods first — 1 = method, 2 = field, 0 = neither,
302
486
  // 3 = undecidable.
303
487
  function wrapBoxed(handle) {
488
+ // A GError gets the full error shape — domain class, Error brand, position
489
+ // properties — wherever it surfaces: an OUT param, a struct field, a container
490
+ // element. Routing HERE rather than in the GError-typed OUT arm in C++ is what
491
+ // makes those three agree; the old code only special-cased the thrown path, so a
492
+ // GError read out of a container was a bare method-routing proxy.
493
+ if (native.boxedTypeName(handle) === 'GError') return wrapGError(handle);
304
494
  const target = { [HANDLE]: handle };
305
495
  const methodDispatch =
306
496
  (prop) =>
@@ -354,36 +544,51 @@ function wrapBoxed(handle) {
354
544
  // GParamSpec-typed value surface as. Carries [HANDLE] so it round-trips back into the
355
545
  // engine and `instanceof GObject.ParamSpec` recognises it. value_type/owner_type are
356
546
  // native GType handles, like gjs's GType objects.
547
+ // The accessors live HERE, on one shared prototype, and read the handle off `this`.
548
+ // Built per instance they were eleven fresh closures plus a twelve-slot object on
549
+ // EVERY notify emission — the engine hands the pspec to the handler whether or not
550
+ // the handler looks at it, so that allocation landed on every property write of the
551
+ // real-GObject engine. On gjs these same members live on GObject.ParamSpec.prototype,
552
+ // so prototype-held is also the closer shape.
553
+ const paramSpecProto = {
554
+ get name() {
555
+ return native.paramSpecProp(this[HANDLE], 'name');
556
+ },
557
+ get nick() {
558
+ return native.paramSpecProp(this[HANDLE], 'nick');
559
+ },
560
+ get blurb() {
561
+ return native.paramSpecProp(this[HANDLE], 'blurb');
562
+ },
563
+ get flags() {
564
+ return native.paramSpecProp(this[HANDLE], 'flags');
565
+ },
566
+ get value_type() {
567
+ return native.paramSpecProp(this[HANDLE], 'valueType');
568
+ },
569
+ get owner_type() {
570
+ return native.paramSpecProp(this[HANDLE], 'ownerType');
571
+ },
572
+ get default_value() {
573
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
574
+ },
575
+ get_name() {
576
+ return native.paramSpecProp(this[HANDLE], 'name');
577
+ },
578
+ get_nick() {
579
+ return native.paramSpecProp(this[HANDLE], 'nick');
580
+ },
581
+ get_blurb() {
582
+ return native.paramSpecProp(this[HANDLE], 'blurb');
583
+ },
584
+ get_default_value() {
585
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
586
+ },
587
+ };
588
+
357
589
  function wrapParamSpec(handle) {
358
- const prop = (which) => native.paramSpecProp(handle, which);
359
- const pspec = {
360
- [HANDLE]: handle,
361
- get name() {
362
- return prop('name');
363
- },
364
- get nick() {
365
- return prop('nick');
366
- },
367
- get blurb() {
368
- return prop('blurb');
369
- },
370
- get flags() {
371
- return prop('flags');
372
- },
373
- get value_type() {
374
- return prop('valueType');
375
- },
376
- get owner_type() {
377
- return prop('ownerType');
378
- },
379
- get default_value() {
380
- return wrapReturn(prop('defaultValue'));
381
- },
382
- get_name: () => prop('name'),
383
- get_nick: () => prop('nick'),
384
- get_blurb: () => prop('blurb'),
385
- get_default_value: () => wrapReturn(prop('defaultValue')),
386
- };
590
+ const pspec = Object.create(paramSpecProto);
591
+ pspec[HANDLE] = handle;
387
592
  return pspec;
388
593
  }
389
594
 
@@ -612,6 +817,13 @@ function makeLogStructured(baseNs) {
612
817
  }
613
818
 
614
819
  function decorateGLibNamespace(baseNs) {
820
+ // GJS's GLib override installs this on the GLOBAL Error.prototype, which is what
821
+ // makes `catch (e) { if (e.matches(domain, code)) … }` safe to write without a
822
+ // type test in front of it. Plain assignment, so the descriptor is GJS's
823
+ // (writable, enumerable, configurable) rather than a hardened one.
824
+ Error.prototype.matches = function matches() {
825
+ return false;
826
+ };
615
827
  // Per-namespace cache of the overlay functions that need the introspected baseNs
616
828
  // (log_structured → log_variant; the *_once helpers → idle_add/timeout_add).
617
829
  const cache = new Map();
@@ -658,9 +870,9 @@ function decorateGLibNamespace(baseNs) {
658
870
  return new Proxy(baseNs, {
659
871
  get(t, prop) {
660
872
  if (prop === 'Variant') return variantClass;
661
- // GLib.Error is the L1 GError subclass (the engine throws instances of it,
662
- // and `new GLib.Error(domain, code, message)` constructs one), shadowing the
663
- // introspected boxed type so `instanceof GLib.Error` + `.matches()` work.
873
+ // GLib.Error is the L1 error base class — every GError reaching JS is an
874
+ // instance of it or of its domain's subclass — shadowing the introspected
875
+ // boxed type, which would hand out a bare method-routing proxy.
664
876
  if (prop === 'Error') return GLibError;
665
877
  if (typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) return overlay(prop);
666
878
  return t[prop];
@@ -867,7 +1079,27 @@ function assignTemplateChildren(instance, handle, reg) {
867
1079
  // resolves the same three spellings at the point of use instead — same reachability,
868
1080
  // no accessor synthesis. Learn6502's SourceView is exactly why this matters: its
869
1081
  // property is `line-numbers` and its accessor is `lineNumbers`.
1082
+ // Resolved per prototype, INCLUDING the "this class declares none" answer (null).
1083
+ // The native store calls runJsPropertySetter on every write of a custom property, so
1084
+ // without this a class with no accessor at all — the common case — paid three
1085
+ // prototype-chain walks and two regex rewrites per assignment to learn nothing. GJS
1086
+ // settles the same question once, at registerClass time (`_checkAccessors` mirrors
1087
+ // the declared spelling onto the other two); a prototype gaining an accessor later is
1088
+ // outside what either runtime honours.
1089
+ const propertySettersByProto = new WeakMap();
1090
+
870
1091
  function findPropertySetter(userProto, propertyName) {
1092
+ let perProto = propertySettersByProto.get(userProto);
1093
+ if (perProto === undefined) propertySettersByProto.set(userProto, (perProto = new Map()));
1094
+ let setter = perProto.get(propertyName);
1095
+ if (setter === undefined) {
1096
+ setter = resolvePropertySetter(userProto, propertyName) ?? null;
1097
+ perProto.set(propertyName, setter);
1098
+ }
1099
+ return setter ?? undefined;
1100
+ }
1101
+
1102
+ function resolvePropertySetter(userProto, propertyName) {
871
1103
  const underscored = propertyName.replace(/-/g, '_');
872
1104
  const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
873
1105
  for (const spelling of [propertyName, underscored, camel]) {
@@ -1352,6 +1584,30 @@ function wrapInstance(handle, userProto) {
1352
1584
  materialised.add(name);
1353
1585
  return had;
1354
1586
  };
1587
+ // The `connect`/`connect_after`/`emit`/`disconnect` thunks of this wrapper, each
1588
+ // built on its first use (see the `get` trap). A null prototype so an unrelated
1589
+ // name — `constructor`, `toString` — cannot be mistaken for a built shortcut.
1590
+ const signalShortcuts = Object.create(null);
1591
+ // The GI-method thunks of this wrapper, built on first use, `null` for a name
1592
+ // that resolves to no introspected method. A thunk is a pure function of
1593
+ // (handle, name) and presence is already memoized per GType (instanceHasMethod),
1594
+ // so the only thing that changes by keeping it is IDENTITY — and a STABLE one is
1595
+ // the GJS shape: there the method lives on the prototype, so `obj.m === obj.m`
1596
+ // (the reason a user prototype's own function is handed back unbound, #1175).
1597
+ // Built fresh, every `obj.m(…)` in a loop allocated a closure and re-asked for
1598
+ // the type name across the N-API boundary. Null prototype: `constructor` and
1599
+ // friends must not read as a cached thunk.
1600
+ const giMethodThunks = Object.create(null);
1601
+ const giMethodThunk = (prop) => {
1602
+ let thunk = giMethodThunks[prop];
1603
+ if (thunk === undefined) {
1604
+ thunk = instanceHasMethod(handle, prop)
1605
+ ? (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)))
1606
+ : null;
1607
+ giMethodThunks[prop] = thunk;
1608
+ }
1609
+ return thunk ?? undefined;
1610
+ };
1355
1611
  const proxy = new Proxy(target, {
1356
1612
  get(t, prop) {
1357
1613
  if (prop === HANDLE) return handle;
@@ -1365,10 +1621,16 @@ function wrapInstance(handle, userProto) {
1365
1621
  // introspectable ancestor (GObject.Object), yet on gjs 1.88.1
1366
1622
  // `socks5.connect` IS g_proxy_connect (Gio.Proxy, 3 IN args) — measured.
1367
1623
  // The same native walk the GI-method fallback below uses sees it.
1368
- if (instanceHasMethod(handle, prop)) {
1369
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1370
- }
1624
+ const method = giMethodThunk(prop);
1625
+ if (method !== undefined) return method;
1371
1626
  }
1627
+ // The four signal shortcuts below close over nothing but `handle`, so they
1628
+ // are per-wrapper constants — yet the switch rebuilt one on EVERY access,
1629
+ // i.e. one throwaway closure per `obj.emit(…)` in a loop. Built once here
1630
+ // and reused; on gjs these live on GObject.Object.prototype and have stable
1631
+ // identity too. Lazy, because most wrappers never reach for one.
1632
+ const shortcut = signalShortcuts[prop];
1633
+ if (shortcut !== undefined) return shortcut;
1372
1634
  switch (prop) {
1373
1635
  case '$typeName':
1374
1636
  // The instance's concrete RUNTIME GType name —
@@ -1385,24 +1647,25 @@ function wrapInstance(handle, userProto) {
1385
1647
  case 'connect':
1386
1648
  // Record the (user fn → id) mapping so signal_handlers_*_by_func can
1387
1649
  // resolve the private-closure handler back to its ids (see the registry).
1388
- return (signal, cb) => {
1650
+ return (signalShortcuts.connect = (signal, cb) => {
1389
1651
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
1390
1652
  recordSignalHandler(handle, cb, id);
1391
1653
  return id;
1392
- };
1654
+ });
1393
1655
  case 'connect_after':
1394
- return (signal, cb) => {
1656
+ return (signalShortcuts.connect_after = (signal, cb) => {
1395
1657
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
1396
1658
  recordSignalHandler(handle, cb, id);
1397
1659
  return id;
1398
- };
1660
+ });
1399
1661
  case 'emit':
1400
- return (signal, ...args) => wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
1662
+ return (signalShortcuts.emit = (signal, ...args) =>
1663
+ wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args))));
1401
1664
  case 'disconnect':
1402
- return (id) => {
1665
+ return (signalShortcuts.disconnect = (id) => {
1403
1666
  forgetSignalHandlerId(handle, id);
1404
1667
  return native.disconnectSignal(handle, id);
1405
- };
1668
+ });
1406
1669
  default:
1407
1670
  break;
1408
1671
  }
@@ -1478,10 +1741,7 @@ function wrapInstance(handle, userProto) {
1478
1741
  // camelCase like Gwebgl's `getString`) and only falls back to the
1479
1742
  // snake_case alias. Converting here would destroy a literal camelCase
1480
1743
  // GI name.
1481
- if (instanceHasMethod(handle, prop)) {
1482
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1483
- }
1484
- return undefined;
1744
+ return giMethodThunk(prop);
1485
1745
  },
1486
1746
  set(t, prop, value) {
1487
1747
  if (typeof prop === 'string') {
@@ -1954,13 +2214,11 @@ function makeEnum(namespace, typeName) {
1954
2214
  for (const key of Object.keys(raw)) {
1955
2215
  out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
1956
2216
  }
1957
- // If this enum is registered as a GError domain (Gio.IOErrorEnum, …), attach
1958
- // its domain descriptor (non-enumerable) so GLib.Error.matches(enum, code) can
1959
- // resolve the enum to its domain. A plain enum gets nothing (getErrorDomain → null).
2217
+ // An enum registered as a GError domain (Gio.IOErrorEnum, …) is the CLASS of its
2218
+ // errors as well, which a frozen object cannot be — it keeps its members on the
2219
+ // function form instead.
1960
2220
  const domain = native.getErrorDomain(namespace, typeName);
1961
- if (domain !== null) {
1962
- Object.defineProperty(out, ERROR_DOMAIN, { value: domain, enumerable: false });
1963
- }
2221
+ if (domain !== null) return makeErrorDomainClass(namespace, typeName, out, domain.quark);
1964
2222
  // `Ns.Enum.$gtype` — gjs answers a registered G_TYPE_ENUM/G_TYPE_FLAGS here, so
1965
2223
  // `GObject.type_name(Gio.BusType.$gtype)` is `GBusType` there and was `null` over
1966
2224
  // the bridge: an enum object carried no `$gtype` at all. EAGER rather than