@gtkx/runtime 1.0.0 → 1.2.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.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/dist/application-class.d.ts +5 -0
  3. package/dist/application-class.d.ts.map +1 -1
  4. package/dist/application-class.js +11 -1
  5. package/dist/application-class.js.map +1 -1
  6. package/dist/callback.d.ts +2 -2
  7. package/dist/callback.d.ts.map +1 -1
  8. package/dist/callback.js +51 -54
  9. package/dist/callback.js.map +1 -1
  10. package/dist/closure.js +1 -1
  11. package/dist/closure.js.map +1 -1
  12. package/dist/default-application.d.ts +4 -0
  13. package/dist/default-application.d.ts.map +1 -0
  14. package/dist/default-application.js +17 -0
  15. package/dist/default-application.js.map +1 -0
  16. package/dist/descriptors.d.ts +34 -9
  17. package/dist/descriptors.d.ts.map +1 -1
  18. package/dist/descriptors.js +42 -17
  19. package/dist/descriptors.js.map +1 -1
  20. package/dist/exit-hook.js +13 -0
  21. package/dist/exit-hook.js.map +1 -1
  22. package/dist/fn.d.ts +5 -0
  23. package/dist/fn.d.ts.map +1 -1
  24. package/dist/fn.js +11 -13
  25. package/dist/fn.js.map +1 -1
  26. package/dist/folded-lengths.d.ts +16 -0
  27. package/dist/folded-lengths.d.ts.map +1 -0
  28. package/dist/folded-lengths.js +38 -0
  29. package/dist/folded-lengths.js.map +1 -0
  30. package/dist/internal.d.ts +1 -0
  31. package/dist/internal.d.ts.map +1 -1
  32. package/dist/internal.js +1 -0
  33. package/dist/internal.js.map +1 -1
  34. package/dist/library.d.ts +2 -1
  35. package/dist/library.d.ts.map +1 -1
  36. package/dist/library.js +2 -1
  37. package/dist/library.js.map +1 -1
  38. package/dist/lifecycle.d.ts +16 -1
  39. package/dist/lifecycle.d.ts.map +1 -1
  40. package/dist/lifecycle.js +44 -14
  41. package/dist/lifecycle.js.map +1 -1
  42. package/dist/listeners.d.ts.map +1 -1
  43. package/dist/listeners.js +16 -4
  44. package/dist/listeners.js.map +1 -1
  45. package/dist/native-value.js +2 -2
  46. package/dist/native-value.js.map +1 -1
  47. package/dist/object.d.ts +7 -2
  48. package/dist/object.d.ts.map +1 -1
  49. package/dist/object.js +7 -2
  50. package/dist/object.js.map +1 -1
  51. package/dist/param-spec.d.ts.map +1 -1
  52. package/dist/param-spec.js +18 -20
  53. package/dist/param-spec.js.map +1 -1
  54. package/dist/properties.d.ts +2 -1
  55. package/dist/properties.d.ts.map +1 -1
  56. package/dist/properties.js +13 -9
  57. package/dist/properties.js.map +1 -1
  58. package/dist/register-class.d.ts +33 -3
  59. package/dist/register-class.d.ts.map +1 -1
  60. package/dist/register-class.js +21 -7
  61. package/dist/register-class.js.map +1 -1
  62. package/dist/registry.d.ts +14 -4
  63. package/dist/registry.d.ts.map +1 -1
  64. package/dist/registry.js +24 -15
  65. package/dist/registry.js.map +1 -1
  66. package/dist/signal.d.ts +2 -2
  67. package/dist/signal.d.ts.map +1 -1
  68. package/dist/signal.js +22 -18
  69. package/dist/signal.js.map +1 -1
  70. package/dist/t.d.ts +3 -1
  71. package/dist/t.d.ts.map +1 -1
  72. package/dist/t.js +1 -0
  73. package/dist/t.js.map +1 -1
  74. package/dist/tuple.d.ts +6 -3
  75. package/dist/tuple.d.ts.map +1 -1
  76. package/dist/tuple.js +9 -7
  77. package/dist/tuple.js.map +1 -1
  78. package/dist/value.d.ts +3 -1
  79. package/dist/value.d.ts.map +1 -1
  80. package/dist/value.js +64 -18
  81. package/dist/value.js.map +1 -1
  82. package/dist/vfunc-call.d.ts.map +1 -1
  83. package/dist/vfunc-call.js +5 -1
  84. package/dist/vfunc-call.js.map +1 -1
  85. package/dist/vfunc.d.ts.map +1 -1
  86. package/dist/vfunc.js +9 -3
  87. package/dist/vfunc.js.map +1 -1
  88. package/package.json +14 -4
  89. package/src/application-class.ts +12 -2
  90. package/src/callback.ts +70 -66
  91. package/src/closure.ts +1 -1
  92. package/src/default-application.ts +21 -0
  93. package/src/descriptors.ts +98 -29
  94. package/src/exit-hook.ts +18 -0
  95. package/src/fn.ts +23 -13
  96. package/src/folded-lengths.ts +64 -0
  97. package/src/internal.ts +1 -0
  98. package/src/library.ts +2 -1
  99. package/src/lifecycle.ts +60 -14
  100. package/src/listeners.ts +22 -5
  101. package/src/native-value.ts +2 -2
  102. package/src/object.ts +7 -2
  103. package/src/param-spec.ts +21 -22
  104. package/src/properties.ts +23 -9
  105. package/src/register-class.ts +66 -11
  106. package/src/registry.ts +43 -20
  107. package/src/signal.ts +32 -28
  108. package/src/t.ts +4 -1
  109. package/src/tuple.ts +18 -12
  110. package/src/value.ts +94 -19
  111. package/src/vfunc-call.ts +5 -1
  112. package/src/vfunc.ts +10 -3
package/src/internal.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { checkError, createErrorDomain } from "./error.js";
2
+ export { type ApplicationInstance, getApplicationInstance } from "./lifecycle.js";
2
3
  export { registerClassType, resolveWrapperClass, wrapHandle } from "./registry.js";
3
4
  export { hasSignalListener } from "./signal.js";
4
5
  export { resolveType } from "./type.js";
package/src/library.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type BoxedDescriptor, boxedT, type FundamentalDescriptor, fundamentalT } from "./descriptors.js";
2
2
 
3
3
  const LIB = "libgobject-2.0.so.0,libglib-2.0.so.0";
4
+ const GIO_LIB = "libgio-2.0.so.0";
4
5
  const VALUE_SIZE = 24;
5
6
  const CLOSURE_SIZE = 32;
6
7
  const VALUE_T: BoxedDescriptor = boxedT("GValue", { sharedLibrary: LIB, getTypeFnName: "g_value_get_type" });
@@ -15,4 +16,4 @@ const VARIANT_T: FundamentalDescriptor = fundamentalT(LIB, "g_variant_ref", "g_v
15
16
  typeName: "GVariant",
16
17
  });
17
18
 
18
- export { CLOSURE_SIZE, LIB, VALUE_SIZE, VALUE_T, PARAM_T, VARIANT_T };
19
+ export { CLOSURE_SIZE, GIO_LIB, LIB, VALUE_SIZE, VALUE_T, PARAM_T, VARIANT_T };
package/src/lifecycle.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { keepAlive, quit as nativeQuit } from "@gtkx/native";
2
2
  import { isDerivedApplication, type LocalCommandLineApplication, shutDownThroughRun } from "./application-class.js";
3
+ import { claimDefaultApplication, releaseDefaultApplication } from "./default-application.js";
3
4
 
4
5
  /**
5
6
  * The GIO and GTK application surface {@link runApplication} and {@link quitApplication} drive, so any
@@ -30,6 +31,8 @@ type ApplicationLike = {
30
31
  emit(signal: "shutdown"): unknown;
31
32
  };
32
33
 
34
+ type ApplicationInstance = "primary" | "remote" | "shutDown" | "unregistered";
35
+
33
36
  /** What {@link runApplication} reports about the process it just started. */
34
37
  type RunApplicationResult = {
35
38
  /** Whether this process owns the application ID and may build a user interface. */
@@ -40,6 +43,7 @@ type RunApplicationResult = {
40
43
 
41
44
  const shutdownCallbacks: (() => void)[] = [];
42
45
  const startedApplications: WeakSet<object> = new WeakSet();
46
+ const registeredApplications: WeakSet<object> = new WeakSet();
43
47
  const shutDownApplications: WeakSet<object> = new WeakSet();
44
48
  /**
45
49
  * Runs every registered exit callback and shuts down the native runtime. Safe to
@@ -101,6 +105,14 @@ const restartApplication = (application: ApplicationLike): number => {
101
105
  return 0;
102
106
  };
103
107
 
108
+ const getApplicationInstance = (application: ApplicationLike): ApplicationInstance => {
109
+ if (!application.getIsRegistered()) {
110
+ return registeredApplications.has(application) ? "shutDown" : "unregistered";
111
+ }
112
+
113
+ return application.getIsRemote?.() === true ? "remote" : "primary";
114
+ };
115
+
104
116
  /**
105
117
  * Hands `argv` to GLib's own command line handling, which parses the application's options, prints
106
118
  * `--help`, runs `handle-local-options`, registers the application, and either activates it or
@@ -119,6 +131,13 @@ const restartApplication = (application: ApplicationLike): number => {
119
131
  * user interface may be built here, because a remote application has no `GtkApplicationImpl` and
120
132
  * attaching a window to it crashes.
121
133
  *
134
+ * The application also claims the process-wide default `Gio.Application.getDefault()` returns, before
135
+ * its command line is parsed, so an `activate` handler and anything the host reads afterwards see the
136
+ * application being started rather than an earlier one. That holds whether the start ends registered,
137
+ * remote or unregistered: a refused command line still has to be reportable through the default, which
138
+ * is how `gtkx dev` learns which application its entry mounted. {@link quitApplication} gives the
139
+ * default back up, since GLib drops it only at finalize.
140
+ *
122
141
  * @param application The application to start.
123
142
  * @param argv The command line, whose first entry names the program as `--help` should print it.
124
143
  * @returns Whether this process may build a user interface, and the status to exit with.
@@ -132,11 +151,19 @@ const runApplication = (application: ApplicationLike, argv: string[]): RunApplic
132
151
  );
133
152
  }
134
153
 
154
+ claimDefaultApplication(application);
155
+
135
156
  const exitStatus = startedApplications.has(application)
136
157
  ? restartApplication(application)
137
158
  : startApplication(application, argv);
138
159
 
139
- const isPrimary = application.getIsRegistered() && application.getIsRemote?.() !== true;
160
+ const instance = getApplicationInstance(application);
161
+
162
+ if (instance !== "unregistered") {
163
+ registeredApplications.add(application);
164
+ }
165
+
166
+ const isPrimary = instance === "primary";
140
167
 
141
168
  if (isPrimary) {
142
169
  keepAlive(true);
@@ -145,18 +172,7 @@ const runApplication = (application: ApplicationLike, argv: string[]): RunApplic
145
172
  return { isPrimary, exitStatus };
146
173
  };
147
174
 
148
- /**
149
- * Detaches every window from the application and runs GLib's own shutdown, which emits `shutdown`,
150
- * destroys the application implementation and releases the D-Bus registration. Does nothing for an
151
- * application that is not registered, so a repeated call is a no-op.
152
- *
153
- * GLib's own shutdown is reachable once per application: reaching it marks the application as
154
- * quitting, which GLib never undoes. An application that has already quit falls back to emitting
155
- * `shutdown`, which releases the runtime and leaves the registration for GLib to drop at finalize.
156
- *
157
- * @param application The application to shut down.
158
- */
159
- const quitApplication = (application: ApplicationLike): void => {
175
+ const tearDownApplication = (application: ApplicationLike): void => {
160
176
  if (!application.getIsRegistered() || shutDownApplications.has(application)) {
161
177
  return;
162
178
  }
@@ -175,4 +191,34 @@ const quitApplication = (application: ApplicationLike): void => {
175
191
  }
176
192
  };
177
193
 
178
- export { onExit, quit, runApplication, quitApplication, type ApplicationLike, type RunApplicationResult };
194
+ /**
195
+ * Detaches every window from the application and runs GLib's own shutdown, which emits `shutdown`,
196
+ * destroys the application implementation and releases the D-Bus registration. Does nothing for an
197
+ * application that is not registered, so a repeated call is a no-op.
198
+ *
199
+ * GLib's own shutdown is reachable once per application: reaching it marks the application as
200
+ * quitting, which GLib never undoes. An application that has already quit falls back to emitting
201
+ * `shutdown`, which releases the runtime and leaves the registration for GLib to drop at finalize.
202
+ *
203
+ * The process-wide default {@link runApplication} claimed is given up whichever of those paths the
204
+ * application took, including the ones that have no teardown left to do, so
205
+ * `Gio.Application.getDefault()` never hands back an application that has been torn down or that
206
+ * never registered in the first place. GLib clears that default only at finalize, which a
207
+ * garbage-collected wrapper reaches arbitrarily late or never.
208
+ *
209
+ * @param application The application to shut down.
210
+ */
211
+ const quitApplication = (application: ApplicationLike): void => {
212
+ tearDownApplication(application);
213
+ releaseDefaultApplication(application);
214
+ };
215
+
216
+ export {
217
+ getApplicationInstance,
218
+ onExit,
219
+ quit,
220
+ runApplication,
221
+ quitApplication,
222
+ type ApplicationInstance,
223
+ type RunApplicationResult,
224
+ };
package/src/listeners.ts CHANGED
@@ -11,10 +11,10 @@ type SignalConnectable = {
11
11
  disconnect(handlerId: number): void;
12
12
  };
13
13
 
14
- const listenerTable: WeakMap<object, Map<string, Map<SignalHandler, number>>> = new WeakMap();
14
+ const listenerTable: WeakMap<object, Map<string, Map<SignalHandler, number[]>>> = new WeakMap();
15
15
 
16
16
  const findListenerHandlerId = (instance: object, signal: string, handler: SignalHandler): number | undefined =>
17
- listenerTable.get(instance)?.get(signal)?.get(handler);
17
+ listenerTable.get(instance)?.get(signal)?.get(handler)?.at(-1);
18
18
 
19
19
  const trackListener = (instance: object, signal: string, handler: SignalHandler, handlerId: number): void => {
20
20
  let bySignal = listenerTable.get(instance);
@@ -31,7 +31,24 @@ const trackListener = (instance: object, signal: string, handler: SignalHandler,
31
31
  bySignal.set(signal, byHandler);
32
32
  }
33
33
 
34
- byHandler.set(handler, handlerId);
34
+ let handlerIds = byHandler.get(handler);
35
+
36
+ if (!handlerIds) {
37
+ handlerIds = [];
38
+ byHandler.set(handler, handlerIds);
39
+ }
40
+
41
+ handlerIds.push(handlerId);
42
+ };
43
+
44
+ const removeTrackedHandlerId = (handlerIds: number[], handlerId: number): number => {
45
+ const index = handlerIds.lastIndexOf(handlerId);
46
+
47
+ if (index !== -1) {
48
+ handlerIds.splice(index, 1);
49
+ }
50
+
51
+ return handlerIds.length;
35
52
  };
36
53
 
37
54
  const untrackHandlerId = (instance: object, signal: string, handlerId: number): void => {
@@ -42,8 +59,8 @@ const untrackHandlerId = (instance: object, signal: string, handlerId: number):
42
59
  return;
43
60
  }
44
61
 
45
- for (const [handler, id] of byHandler) {
46
- if (id === handlerId) {
62
+ for (const [handler, handlerIds] of byHandler) {
63
+ if (removeTrackedHandlerId(handlerIds, handlerId) === 0) {
47
64
  byHandler.delete(handler);
48
65
  }
49
66
  }
@@ -1,6 +1,6 @@
1
1
  import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
2
2
  import type { ArrayDescriptor, FundamentalDescriptor, HashTableDescriptor, StructDescriptor } from "./descriptors.js";
3
- import { getHandle, getWrapperClass, wrapHandle, wrapObject } from "./registry.js";
3
+ import { getHandle, getWrapperClass, wrapCallScopedObject, wrapHandle, wrapObject } from "./registry.js";
4
4
  import { resolveDescriptorType } from "./type.js";
5
5
 
6
6
  type MarshalledKind = "object" | "struct" | "boxed" | "fundamental" | "array" | "hashtable";
@@ -76,7 +76,7 @@ function fromNative(descriptor: Descriptor, value: unknown): unknown {
76
76
 
77
77
  switch (descriptor.kind) {
78
78
  case "object": {
79
- return wrapObject(value);
79
+ return descriptor.isCallScoped === true ? wrapCallScopedObject(value) : wrapObject(value);
80
80
  }
81
81
  case "struct": {
82
82
  return wrapHandle(value as ExternalObject<Handle> | null, (descriptor as StructDescriptor).wrapperClass);
package/src/object.ts CHANGED
@@ -121,7 +121,9 @@ function constructPropertyForEntry(
121
121
  * that name, dashed or camelCased, and is skipped when the type installs none.
122
122
  * A value that ParamSpec would refuse throws before GObject sees it: a `TypeError`
123
123
  * for a read-only property and for a value of a type the property cannot hold, and
124
- * a `RangeError` for a value the ParamSpec rejects.
124
+ * a `RangeError` for a value the ParamSpec rejects. A value marshalled through a
125
+ * declared descriptor is converted rather than checked, so a `null` handed to a
126
+ * numeric one of those lands 0 rather than being refused.
125
127
  * Properties whose value is `undefined` are skipped. A type registered with
126
128
  * `registerClass` binds the wrapper before its `constructed` slot runs, so an
127
129
  * override of that slot already sees a usable instance.
@@ -168,7 +170,10 @@ function getObjectProperty(obj: object, propertyName: string, descriptor: Descri
168
170
 
169
171
  /**
170
172
  * Writes a JavaScript value to a GObject property, converting it to native form
171
- * using the descriptor.
173
+ * using the descriptor. The descriptor converts what it is given rather than checking it against
174
+ * the property's `GObject.ParamSpec`, so `null` and `undefined` written to a numeric or enum
175
+ * property land 0, where the same write to a property installed through `registerClass` is
176
+ * refused with a `TypeError`.
172
177
  *
173
178
  * @param obj The object to write to.
174
179
  * @param propertyName The property name.
package/src/param-spec.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  TYPE_ENUM,
14
14
  TYPE_FLAGS,
15
15
  TYPE_FLOAT,
16
+ TYPE_GTYPE,
16
17
  TYPE_INT,
17
18
  TYPE_INT64,
18
19
  TYPE_INTERFACE,
@@ -51,7 +52,6 @@ const UINT32_MAXIMUM = 4_294_967_295;
51
52
  const INT64_MINIMUM = -(2n ** 63n);
52
53
  const INT64_MAXIMUM = 2n ** 63n - 1n;
53
54
  const UINT64_MAXIMUM = 2n ** 64n - 1n;
54
- const FLOAT_MAXIMUM = 3.4028234663852886e38;
55
55
 
56
56
  const WRAPPED_FUNDAMENTALS: Set<bigint> = new Set([
57
57
  TYPE_BOXED,
@@ -61,6 +61,8 @@ const WRAPPED_FUNDAMENTALS: Set<bigint> = new Set([
61
61
  TYPE_VARIANT,
62
62
  ]);
63
63
 
64
+ const isWideUnsignedValue: ValueGuard = wideIntegerGuardFor(0n, UINT64_MAXIMUM);
65
+
64
66
  const SCALAR_GUARDS: Map<bigint, ValueGuard> = new Map([
65
67
  [TYPE_BOOLEAN, isBooleanValue],
66
68
  [TYPE_STRING, isStringValue],
@@ -71,11 +73,11 @@ const SCALAR_GUARDS: Map<bigint, ValueGuard> = new Map([
71
73
  [TYPE_ENUM, integerGuardFor(INT32_MINIMUM, INT32_MAXIMUM)],
72
74
  [TYPE_FLAGS, integerGuardFor(0, UINT32_MAXIMUM)],
73
75
  [TYPE_LONG, wideIntegerGuardFor(INT64_MINIMUM, INT64_MAXIMUM)],
74
- [TYPE_ULONG, wideIntegerGuardFor(0n, UINT64_MAXIMUM)],
76
+ [TYPE_ULONG, isWideUnsignedValue],
75
77
  [TYPE_INT64, wideIntegerGuardFor(INT64_MINIMUM, INT64_MAXIMUM)],
76
- [TYPE_UINT64, wideIntegerGuardFor(0n, UINT64_MAXIMUM)],
77
- [TYPE_FLOAT, isFloatValue],
78
- [TYPE_DOUBLE, isDoubleValue],
78
+ [TYPE_UINT64, isWideUnsignedValue],
79
+ [TYPE_FLOAT, isNumberValue],
80
+ [TYPE_DOUBLE, isNumberValue],
79
81
  [TYPE_POINTER, isNullValue],
80
82
  ]);
81
83
 
@@ -115,12 +117,8 @@ function isStrvValue(value: unknown): boolean {
115
117
  return value == null || (Array.isArray(value) && value.every((item) => typeof item === "string"));
116
118
  }
117
119
 
118
- function isDoubleValue(value: unknown): boolean {
119
- return value == null || (typeof value === "number" && Number.isFinite(value));
120
- }
121
-
122
- function isFloatValue(value: unknown): boolean {
123
- return isDoubleValue(value) && (value == null || Math.abs(value as number) <= FLOAT_MAXIMUM);
120
+ function isNumberValue(value: unknown): boolean {
121
+ return typeof value === "number";
124
122
  }
125
123
 
126
124
  function isAnyValue(): boolean {
@@ -131,12 +129,8 @@ function isNullValue(value: unknown): boolean {
131
129
  return value == null;
132
130
  }
133
131
 
134
- function isIntegerWithin(value: unknown, minimum: number, maximum: number): boolean {
135
- return Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
136
- }
137
-
138
132
  function integerGuardFor(minimum: number, maximum: number): ValueGuard {
139
- return (value) => value == null || isIntegerWithin(value, minimum, maximum);
133
+ return (value) => Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
140
134
  }
141
135
 
142
136
  function toWideInteger(value: unknown): bigint | undefined {
@@ -149,10 +143,6 @@ function toWideInteger(value: unknown): bigint | undefined {
149
143
 
150
144
  function wideIntegerGuardFor(minimum: bigint, maximum: bigint): ValueGuard {
151
145
  return (value) => {
152
- if (value == null) {
153
- return true;
154
- }
155
-
156
146
  const wide = toWideInteger(value);
157
147
 
158
148
  return wide !== undefined && wide >= minimum && wide <= maximum;
@@ -173,11 +163,20 @@ function wrappedGuardFor(valueType: bigint): ValueGuard {
173
163
  return (value) => value == null || typeIsA(resolveGtype(value), valueType);
174
164
  }
175
165
 
166
+ function exactGuardFor(valueType: bigint): ValueGuard | undefined {
167
+ if (valueType === TYPE_GTYPE) {
168
+ return isWideUnsignedValue;
169
+ }
170
+
171
+ return valueType === getStrvType() ? isStrvValue : undefined;
172
+ }
173
+
176
174
  function valueGuardFor(valueType: bigint): ValueGuard {
177
175
  assertParamLayout();
176
+ const exact = exactGuardFor(valueType);
178
177
 
179
- if (valueType === getStrvType()) {
180
- return isStrvValue;
178
+ if (exact !== undefined) {
179
+ return exact;
181
180
  }
182
181
 
183
182
  const fundamental = typeFundamental(valueType);
package/src/properties.ts CHANGED
@@ -16,7 +16,14 @@ import {
16
16
  } from "./param-spec.js";
17
17
  import { getHandle, getInterfaceProperties, instanceClassName, type InterfaceProperty } from "./registry.js";
18
18
  import { typeName } from "./type.js";
19
- import { fromValue, intoValue, newValueForType, type ValueWriter, valueWriterFor } from "./value.js";
19
+ import {
20
+ fromValue,
21
+ newValueForType,
22
+ type ValueNarrower,
23
+ valueNarrowerFor,
24
+ type ValueWriter,
25
+ valueWriterFor,
26
+ } from "./value.js";
20
27
 
21
28
  type PropertyCheck = {
22
29
  name: string;
@@ -25,6 +32,7 @@ type PropertyCheck = {
25
32
  flags: number;
26
33
  valueType: bigint;
27
34
  canHoldValue: ValueGuard;
35
+ narrowValue: ValueNarrower;
28
36
  write?: ValueWriter;
29
37
  scratch?: ExternalObject<Handle>;
30
38
  };
@@ -80,6 +88,8 @@ const typeLabel = (type: bigint): string => typeName(type) ?? String(type);
80
88
  const defaultValueFor = (handle: ExternalObject<Handle>): ExternalObject<Handle> =>
81
89
  paramSpecDefaultValue(handle) as ExternalObject<Handle>;
82
90
 
91
+ const heldValue = (value: unknown): unknown => value ?? null;
92
+
83
93
  const holdsReason = (check: PropertyCheck): string =>
84
94
  `the property holds values of type '${typeLabel(check.valueType)}'`;
85
95
 
@@ -93,6 +103,7 @@ function checkFor(handle: ExternalObject<Handle>, name: string): PropertyCheck {
93
103
  flags: getParamFlags(handle),
94
104
  valueType,
95
105
  canHoldValue: valueGuardFor(valueType),
106
+ narrowValue: valueNarrowerFor(valueType),
96
107
  };
97
108
  }
98
109
 
@@ -168,6 +179,11 @@ function assertWritable(instance: object, check: PropertyCheck, value: unknown):
168
179
  throw new TypeError(propertyMessage(instance, check, refusalTail(check, value, READ_ONLY_REASON)));
169
180
  }
170
181
 
182
+ function writeHeld(check: PropertyCheck, gValue: ExternalObject<Handle>, value: unknown): void {
183
+ check.write ??= valueWriterFor(check.valueType);
184
+ check.write(gValue, check.narrowValue(value));
185
+ }
186
+
171
187
  function fillCheckedValue(
172
188
  instance: object,
173
189
  check: PropertyCheck,
@@ -175,8 +191,7 @@ function fillCheckedValue(
175
191
  value: unknown,
176
192
  ): void {
177
193
  assertValueFits(instance, check, value);
178
- check.write ??= valueWriterFor(check.valueType);
179
- check.write(gValue, value);
194
+ writeHeld(check, gValue, value);
180
195
  assertValueValidates(instance, check, value, gValue);
181
196
  }
182
197
 
@@ -224,11 +239,11 @@ function readStored(instance: Record<symbol, unknown>, accessor: PropertyAccesso
224
239
  }
225
240
 
226
241
  function storeValue(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
227
- instance[accessor.storage] = value;
242
+ instance[accessor.storage] = heldValue(value);
228
243
  }
229
244
 
230
245
  function writeStored(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
231
- if (readStored(instance, accessor) === value) {
246
+ if (readStored(instance, accessor) === heldValue(value)) {
232
247
  return;
233
248
  }
234
249
 
@@ -239,13 +254,12 @@ function writeStored(instance: Record<symbol, unknown>, accessor: PropertyAccess
239
254
  function writeProperty(instance: object, accessor: PropertyAccessor, value: unknown): void {
240
255
  const stored = instance as Record<symbol, unknown>;
241
256
 
242
- if (readStored(stored, accessor) === value) {
257
+ if (readStored(stored, accessor) === heldValue(value)) {
243
258
  return;
244
259
  }
245
260
 
246
261
  assertValueAccepted(instance, accessor, value);
247
- storeValue(stored, accessor, value);
248
- (instance as NotifyingObject).notify?.(accessor.propertyName);
262
+ writeStored(stored, accessor, accessor.narrowValue(value));
249
263
  }
250
264
 
251
265
  function storedGetter(accessor: PropertyAccessor): (this: object) => unknown {
@@ -431,7 +445,7 @@ function makeGetProperty(dispatch: PropertyDispatch) {
431
445
  const accessor = resolveAccessor(dispatch, propertyId, pspec);
432
446
  const current = readCurrent(this, accessor);
433
447
  assertValueServes(this, accessor, current);
434
- intoValue(getHandle(value), current);
448
+ writeHeld(accessor, getHandle(value), current);
435
449
  };
436
450
  }
437
451
 
@@ -61,7 +61,7 @@ type Camelized<TName extends string> = TName extends `${infer THead}-${infer TTa
61
61
 
62
62
  /**
63
63
  * The names a registered class carries in its property map: every key of
64
- * `RegisterClassOptions.properties` in camelCase, whichever of the three spellings it was written in.
64
+ * `RegisterClassOptions.properties` in camelCase, whichever spelling it was written in.
65
65
  * A `properties` object given a type of its own rather than left to inference has `string` for its key
66
66
  * type and names nothing, because a name only known as `string` addresses no member in particular.
67
67
  */
@@ -129,13 +129,43 @@ type RegisterClassOptions<TInstance extends object, TProperties extends Record<s
129
129
  * A write the ParamSpec would refuse throws rather than reaching GObject, which reports such a
130
130
  * write as a GLib critical and drops it: a `TypeError` for a read-only or construct-only
131
131
  * property and for a value of a type the property cannot hold, and a `RangeError` for a value
132
- * the ParamSpec rejects. The same two checks run over a value handed to the constructor, where
132
+ * the ParamSpec rejects. The same checks run over a value handed to the constructor, where
133
133
  * a construct-only property is the one that is writable. An accepted write emits one `notify`,
134
134
  * which a `freeze_notify` batch collects; a write of the value the property already holds is
135
135
  * dropped and emits none.
136
136
  *
137
+ * `null` and `undefined` both mean NULL, and mean it only where the ParamSpec's own type holds
138
+ * NULL, which is a string, string-array, boxed, object, interface, param, variant or pointer
139
+ * property. Such a property holds the `null` either spelling wrote, so the member, the type's
140
+ * `get_property` slot and `g_object_get_property` serve the same thing, and writing the other
141
+ * spelling over it emits no `notify`. Every other property, so every integer, floating-point,
142
+ * boolean, enum, flags and GType one, refuses both with the same `TypeError` it refuses a
143
+ * string with, and keeps the value it already holds. It refuses them for what it holds rather
144
+ * than for its range, whatever that range is: the type is checked before the range, so a
145
+ * `gint` whose range excludes 0 answers a nullish with that same `TypeError` and never with
146
+ * the `RangeError` that names the value GObject would put in its place. The one place
147
+ * `undefined` means something else is the constructor, which reads it as the property not
148
+ * being given at all and leaves it at the ParamSpec's default, so a property is never handed a
149
+ * value it cannot serve back.
150
+ *
151
+ * A floating-point property takes every JavaScript number, `NaN` and both infinities
152
+ * included, and its ParamSpec alone rules on which of them the range admits: a `gdouble`
153
+ * bounded by `-Infinity` and `Infinity` holds either infinity, and a magnitude a bounded
154
+ * one excludes, like any `NaN`, comes back as the `RangeError` that names what GObject
155
+ * would put in its place rather than as the `TypeError` a type the property cannot hold
156
+ * earns. A `gfloat` property holds what GObject narrows the double to, so it serves `0.1`
157
+ * back as `0.10000000149011612` and a finite magnitude no `gfloat` reaches as an infinity,
158
+ * which the range then rules on in turn. That narrowing belongs to the property alone: the
159
+ * same magnitude written to a `gfloat` through a generated binding, a signal argument or a
160
+ * closure return is refused outright rather than narrowed.
161
+ *
162
+ * A generated property of a wrapped type answers a nullish differently, and the two halves of
163
+ * the API disagree here: that property marshals what it is written through its descriptor
164
+ * rather than through the checks above, so `new Gtk.Label({ widthRequest: null })` and a later
165
+ * write of `null` to that member both land 0, where a `gint` installed here refuses both.
166
+ *
137
167
  * A class that defines the camelCase member itself owns the property: its own accessor decides
138
- * what a write means, the other two spellings forward to it, and the type's property slots read
168
+ * what a write means, the other spellings forward to it, and the type's property slots read
139
169
  * and write it rather than the generated storage.
140
170
  */
141
171
  properties?: TProperties;
@@ -166,7 +196,11 @@ type PropertyVfuncSpec = {
166
196
  makeDispatch: (dispatch: PropertyDispatch) => VfuncFn;
167
197
  };
168
198
 
199
+ type ArgPatch = { isCallerAllocated: true } | { isCallScoped: true };
200
+
201
+ const INSTANCE_ARG_INDEX = 0;
169
202
  const VALUE_ARG_INDEX = 2;
203
+ const TEARDOWN_VFUNC_NAMES: Set<string> = new Set(["dispose", "finalize"]);
170
204
 
171
205
  const PROPERTY_VFUNC_SPECS: PropertyVfuncSpec[] = [
172
206
  { methodName: GET_PROPERTY_VFUNC, isValueOut: true, makeDispatch: makeGetProperty },
@@ -345,10 +379,13 @@ function buildDiscoveredVfunc(
345
379
  return undefined;
346
380
  }
347
381
 
382
+ const argDescriptors = slotArgDescriptors(descriptor);
383
+
348
384
  return {
349
385
  ...descriptor,
350
386
  methodName,
351
- fn: wrapVfunc(fn, descriptor.argDescriptors, descriptor.returnDescriptor),
387
+ argDescriptors,
388
+ fn: wrapVfunc(fn, argDescriptors, descriptor),
352
389
  };
353
390
  }
354
391
 
@@ -381,9 +418,13 @@ function discoverClassVfuncs(klass: AnyClass, methods: MethodTable): DiscoveredV
381
418
  function wrapVfunc(
382
419
  fn: VfuncFn,
383
420
  argDescriptors: NativeRegisterClassVfunc["argDescriptors"],
384
- returnDescriptor: NativeRegisterClassVfunc["returnDescriptor"],
421
+ descriptor: VfuncDescriptor,
385
422
  ): VfuncFn {
386
- return wrapCallback(fn as (...args: unknown[]) => unknown, { argDescriptors, returnDescriptor }, "this");
423
+ return wrapCallback(
424
+ fn as (...args: unknown[]) => unknown,
425
+ { argDescriptors, returnDescriptor: descriptor.returnDescriptor },
426
+ "vfunc",
427
+ );
387
428
  }
388
429
 
389
430
  function discoverInterfaceBindings(
@@ -443,9 +484,23 @@ function propertyVfuncs(source: PropertyVfuncSource): DiscoveredVfunc[] {
443
484
  );
444
485
  }
445
486
 
446
- function markValueCallerAllocated(argDescriptors: Descriptor[]): Descriptor[] {
447
- return argDescriptors.map((arg, index) =>
448
- index === VALUE_ARG_INDEX ? { ...arg, isCallerAllocated: true } : arg);
487
+ function markArg(argDescriptors: Descriptor[], index: number, patch: ArgPatch): Descriptor[] {
488
+ return argDescriptors.map((arg, at) => (at === index ? { ...arg, ...patch } : arg));
489
+ }
490
+
491
+ function isTeardownSlot(descriptor: VfuncDescriptor): boolean {
492
+ return (
493
+ TEARDOWN_VFUNC_NAMES.has(descriptor.vfuncName) &&
494
+ descriptor.argDescriptors[INSTANCE_ARG_INDEX]?.kind === "object"
495
+ );
496
+ }
497
+
498
+ function slotArgDescriptors(descriptor: VfuncDescriptor): Descriptor[] {
499
+ if (!isTeardownSlot(descriptor)) {
500
+ return descriptor.argDescriptors;
501
+ }
502
+
503
+ return markArg(descriptor.argDescriptors, INSTANCE_ARG_INDEX, { isCallScoped: true });
449
504
  }
450
505
 
451
506
  function buildPropertyVfunc(
@@ -461,14 +516,14 @@ function buildPropertyVfunc(
461
516
  }
462
517
 
463
518
  const argDescriptors = isValueOut
464
- ? markValueCallerAllocated(descriptor.argDescriptors)
519
+ ? markArg(descriptor.argDescriptors, VALUE_ARG_INDEX, { isCallerAllocated: true })
465
520
  : descriptor.argDescriptors;
466
521
 
467
522
  return {
468
523
  ...descriptor,
469
524
  methodName,
470
525
  argDescriptors,
471
- fn: wrapVfunc(fn, argDescriptors, descriptor.returnDescriptor),
526
+ fn: wrapVfunc(fn, argDescriptors, descriptor),
472
527
  };
473
528
  }
474
529