@gtkx/runtime 1.0.0 → 1.1.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 (109) 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/default-application.d.ts +4 -0
  11. package/dist/default-application.d.ts.map +1 -0
  12. package/dist/default-application.js +17 -0
  13. package/dist/default-application.js.map +1 -0
  14. package/dist/descriptors.d.ts +25 -2
  15. package/dist/descriptors.d.ts.map +1 -1
  16. package/dist/descriptors.js +31 -9
  17. package/dist/descriptors.js.map +1 -1
  18. package/dist/exit-hook.js +13 -0
  19. package/dist/exit-hook.js.map +1 -1
  20. package/dist/fn.d.ts +5 -0
  21. package/dist/fn.d.ts.map +1 -1
  22. package/dist/fn.js +11 -13
  23. package/dist/fn.js.map +1 -1
  24. package/dist/folded-lengths.d.ts +16 -0
  25. package/dist/folded-lengths.d.ts.map +1 -0
  26. package/dist/folded-lengths.js +38 -0
  27. package/dist/folded-lengths.js.map +1 -0
  28. package/dist/internal.d.ts +1 -0
  29. package/dist/internal.d.ts.map +1 -1
  30. package/dist/internal.js +1 -0
  31. package/dist/internal.js.map +1 -1
  32. package/dist/library.d.ts +2 -1
  33. package/dist/library.d.ts.map +1 -1
  34. package/dist/library.js +2 -1
  35. package/dist/library.js.map +1 -1
  36. package/dist/lifecycle.d.ts +16 -1
  37. package/dist/lifecycle.d.ts.map +1 -1
  38. package/dist/lifecycle.js +44 -14
  39. package/dist/lifecycle.js.map +1 -1
  40. package/dist/listeners.d.ts.map +1 -1
  41. package/dist/listeners.js +16 -4
  42. package/dist/listeners.js.map +1 -1
  43. package/dist/native-value.js +2 -2
  44. package/dist/native-value.js.map +1 -1
  45. package/dist/object.d.ts +7 -2
  46. package/dist/object.d.ts.map +1 -1
  47. package/dist/object.js +7 -2
  48. package/dist/object.js.map +1 -1
  49. package/dist/param-spec.d.ts.map +1 -1
  50. package/dist/param-spec.js +18 -20
  51. package/dist/param-spec.js.map +1 -1
  52. package/dist/properties.d.ts +2 -1
  53. package/dist/properties.d.ts.map +1 -1
  54. package/dist/properties.js +13 -9
  55. package/dist/properties.js.map +1 -1
  56. package/dist/register-class.d.ts +30 -0
  57. package/dist/register-class.d.ts.map +1 -1
  58. package/dist/register-class.js +21 -7
  59. package/dist/register-class.js.map +1 -1
  60. package/dist/registry.d.ts +12 -3
  61. package/dist/registry.d.ts.map +1 -1
  62. package/dist/registry.js +24 -15
  63. package/dist/registry.js.map +1 -1
  64. package/dist/signal.d.ts +2 -2
  65. package/dist/signal.d.ts.map +1 -1
  66. package/dist/signal.js +22 -18
  67. package/dist/signal.js.map +1 -1
  68. package/dist/t.d.ts +2 -0
  69. package/dist/t.d.ts.map +1 -1
  70. package/dist/t.js +1 -0
  71. package/dist/t.js.map +1 -1
  72. package/dist/tuple.d.ts +6 -3
  73. package/dist/tuple.d.ts.map +1 -1
  74. package/dist/tuple.js +9 -7
  75. package/dist/tuple.js.map +1 -1
  76. package/dist/value.d.ts +3 -1
  77. package/dist/value.d.ts.map +1 -1
  78. package/dist/value.js +64 -18
  79. package/dist/value.js.map +1 -1
  80. package/dist/vfunc-call.d.ts.map +1 -1
  81. package/dist/vfunc-call.js +5 -1
  82. package/dist/vfunc-call.js.map +1 -1
  83. package/dist/vfunc.d.ts.map +1 -1
  84. package/dist/vfunc.js +9 -3
  85. package/dist/vfunc.js.map +1 -1
  86. package/package.json +14 -4
  87. package/src/application-class.ts +12 -2
  88. package/src/callback.ts +70 -66
  89. package/src/default-application.ts +21 -0
  90. package/src/descriptors.ts +69 -14
  91. package/src/exit-hook.ts +18 -0
  92. package/src/fn.ts +23 -13
  93. package/src/folded-lengths.ts +64 -0
  94. package/src/internal.ts +1 -0
  95. package/src/library.ts +2 -1
  96. package/src/lifecycle.ts +60 -14
  97. package/src/listeners.ts +22 -5
  98. package/src/native-value.ts +2 -2
  99. package/src/object.ts +7 -2
  100. package/src/param-spec.ts +21 -22
  101. package/src/properties.ts +23 -9
  102. package/src/register-class.ts +63 -8
  103. package/src/registry.ts +40 -18
  104. package/src/signal.ts +32 -28
  105. package/src/t.ts +3 -0
  106. package/src/tuple.ts +18 -12
  107. package/src/value.ts +94 -19
  108. package/src/vfunc-call.ts +5 -1
  109. package/src/vfunc.ts +10 -3
@@ -0,0 +1,21 @@
1
+ import { bind } from "./bind.js";
2
+ import { objectT, voidT } from "./descriptors.js";
3
+ import { GIO_LIB } from "./library.js";
4
+ import { getHandle } from "./registry.js";
5
+
6
+ const readDefaultApplication = bind(GIO_LIB, "g_application_get_default", [], objectT("borrowed"));
7
+ const writeDefaultApplication = bind(GIO_LIB, "g_application_set_default", [objectT("borrowed")], voidT);
8
+
9
+ const claimDefaultApplication = (application: object): void => {
10
+ writeDefaultApplication(getHandle(application));
11
+ };
12
+
13
+ const releaseDefaultApplication = (application: object): void => {
14
+ if (readDefaultApplication() !== application) {
15
+ return;
16
+ }
17
+
18
+ writeDefaultApplication(null);
19
+ };
20
+
21
+ export { claimDefaultApplication, releaseDefaultApplication };
@@ -89,7 +89,10 @@ type BoxedOptions = {
89
89
  size?: number;
90
90
  };
91
91
 
92
- /** How the callee takes a callback's closure, and how long that closure has to stay alive. */
92
+ /**
93
+ * What the bindings do with a callback's return value, how the callee takes its closure, and how
94
+ * long that closure has to stay alive.
95
+ */
93
96
  type CallbackOptions = {
94
97
  /** The callee also takes a destroy notify, which frees the closure once it is done with it. */
95
98
  hasDestroy?: boolean;
@@ -107,12 +110,22 @@ type CallbackOptions = {
107
110
  type ArrayOptions = {
108
111
  /** Stride in bytes between elements stored inline in the array. */
109
112
  elementSize?: number | undefined;
113
+ /** Position of the argument whose buffer a cursor array points into. */
114
+ baseParamIndex?: number | undefined;
110
115
  /** Position of the argument carrying the element count, for a length-bounded array. */
111
116
  sizeParamIndex?: number | undefined;
112
117
  /** Element count of a fixed-length array. */
113
118
  fixedSize?: number | undefined;
114
119
  };
115
120
 
121
+ /** Where a cursor array's base buffer and total length come from. */
122
+ type CursorBounds = {
123
+ /** Position of the argument holding the buffer the cursor points into. */
124
+ baseParamIndex: number;
125
+ /** Position of the argument carrying that buffer's element count. */
126
+ sizeParamIndex: number;
127
+ };
128
+
116
129
  /** How a ref-counted fundamental value is named, wrapped and stored. */
117
130
  type FundamentalOptions = {
118
131
  /** Whether a decoded value is owned by the caller; defaults to `"borrowed"`. */
@@ -125,6 +138,12 @@ type FundamentalOptions = {
125
138
  isInline?: boolean;
126
139
  };
127
140
 
141
+ type FundamentalLifecycle = {
142
+ sharedLibrary: string;
143
+ refFnName: string;
144
+ unrefFnName: string;
145
+ };
146
+
128
147
  /** How a plain C struct is stored and wrapped. */
129
148
  type StructOptions = {
130
149
  /** The caller owns the storage the callee fills, so a decoded value is borrowed instead of copied. */
@@ -171,6 +190,7 @@ const voidT: VoidDescriptor = { kind: "void" };
171
190
  const unicharT: UnicharDescriptor = { kind: "unichar" };
172
191
  /** Descriptor for an opaque `gpointer`, taken from a typed array's memory or a numeric address. */
173
192
  const bufferT: BufferDescriptor = { kind: "buffer" };
193
+ const fundamentalLifecycles: Map<string, FundamentalLifecycle> = new Map();
174
194
 
175
195
  /**
176
196
  * Builds a descriptor for a C string, whose optional length sizes the caller-allocated buffer
@@ -280,6 +300,15 @@ const structT = (ownership: Ownership = "borrowed", options: StructOptions = {})
280
300
  return result;
281
301
  };
282
302
 
303
+ const recordFundamentalLifecycle = (typeName: string, lifecycle: FundamentalLifecycle): void => {
304
+ if (!fundamentalLifecycles.has(typeName)) {
305
+ fundamentalLifecycles.set(typeName, lifecycle);
306
+ }
307
+ };
308
+
309
+ const fundamentalLifecycleFor = (typeName: string): FundamentalLifecycle | undefined =>
310
+ fundamentalLifecycles.get(typeName);
311
+
283
312
  /** Builds a descriptor for a fundamental type whose lifetime is managed by named ref and unref functions. */
284
313
  const fundamentalT = (
285
314
  sharedLibrary: string,
@@ -292,6 +321,7 @@ const fundamentalT = (
292
321
 
293
322
  if (options.typeName !== undefined) {
294
323
  result.typeName = options.typeName;
324
+ recordFundamentalLifecycle(options.typeName, { sharedLibrary, refFnName, unrefFnName });
295
325
  }
296
326
 
297
327
  if (options.wrapperClass !== undefined) {
@@ -318,6 +348,10 @@ const arrayT = (
318
348
  result.elementSize = options.elementSize;
319
349
  }
320
350
 
351
+ if (options?.baseParamIndex !== undefined) {
352
+ result.baseParamIndex = options.baseParamIndex;
353
+ }
354
+
321
355
  if (options?.sizeParamIndex !== undefined) {
322
356
  result.sizeParamIndex = options.sizeParamIndex;
323
357
  }
@@ -361,6 +395,17 @@ const sizedArrayT = (
361
395
  elementSize?: number,
362
396
  ): ArrayDescriptor => arrayT(itemDescriptor, "sized", ownership, { sizeParamIndex, elementSize });
363
397
 
398
+ /**
399
+ * Builds a descriptor for an out pointer into the buffer another argument supplied, decoded as the
400
+ * elements from where it points to the end of that buffer.
401
+ */
402
+ const cursorArrayT = (
403
+ itemDescriptor: Descriptor,
404
+ bounds: CursorBounds,
405
+ ownership: Ownership = "borrowed",
406
+ elementSize?: number,
407
+ ): ArrayDescriptor => arrayT(itemDescriptor, "cursor", ownership, { ...bounds, elementSize });
408
+
364
409
  /** Builds a descriptor for a C array of a fixed length. */
365
410
  const fixedArrayT = (
366
411
  itemDescriptor: Descriptor,
@@ -369,33 +414,41 @@ const fixedArrayT = (
369
414
  elementSize?: number,
370
415
  ): ArrayDescriptor => arrayT(itemDescriptor, "fixed", ownership, { fixedSize, elementSize });
371
416
 
372
- /** Builds a descriptor for a function pointer, marshalling a JavaScript function into a native closure. */
373
- const callbackT = (
374
- argDescriptors: Descriptor[],
375
- returnDescriptor: Descriptor,
376
- options?: CallbackOptions,
377
- ): CallbackDescriptor => {
378
- const result: CallbackDescriptor = { kind: "callback", argDescriptors, returnDescriptor };
379
-
380
- if (options?.hasDestroy !== undefined) {
417
+ const applyClosureOptions = (result: CallbackDescriptor, options: CallbackOptions): void => {
418
+ if (options.hasDestroy !== undefined) {
381
419
  result.hasDestroy = options.hasDestroy;
382
420
  }
383
421
 
384
- if (options?.destroyKind !== undefined) {
422
+ if (options.destroyKind !== undefined) {
385
423
  result.destroyKind = options.destroyKind;
386
424
  }
387
425
 
388
- if (options?.hasUserData !== undefined) {
426
+ if (options.hasUserData !== undefined) {
389
427
  result.hasUserData = options.hasUserData;
390
428
  }
391
429
 
392
- if (options?.userDataIndex !== undefined) {
430
+ if (options.userDataIndex !== undefined) {
393
431
  result.userDataIndex = options.userDataIndex;
394
432
  }
395
433
 
396
- if (options?.scope !== undefined) {
434
+ if (options.scope !== undefined) {
397
435
  result.scope = options.scope;
398
436
  }
437
+ };
438
+
439
+ /** Builds a descriptor for a function pointer, marshalling a JavaScript function into a native closure. */
440
+ const callbackT = (
441
+ argDescriptors: Descriptor[],
442
+ returnDescriptor: Descriptor,
443
+ options?: CallbackOptions,
444
+ ): CallbackDescriptor => {
445
+ const result: CallbackDescriptor = { kind: "callback", argDescriptors, returnDescriptor };
446
+
447
+ if (options === undefined) {
448
+ return result;
449
+ }
450
+
451
+ applyClosureOptions(result, options);
399
452
 
400
453
  return result;
401
454
  };
@@ -426,6 +479,7 @@ export {
426
479
  flagsT,
427
480
  boxedT,
428
481
  structT,
482
+ fundamentalLifecycleFor,
429
483
  fundamentalT,
430
484
  arrayT,
431
485
  listT,
@@ -435,6 +489,7 @@ export {
435
489
  byteArrayT,
436
490
  sizedArrayT,
437
491
  fixedArrayT,
492
+ cursorArrayT,
438
493
  callbackT,
439
494
  type BoxedDescriptor,
440
495
  type StructDescriptor,
package/src/exit-hook.ts CHANGED
@@ -1,3 +1,21 @@
1
1
  import { quit } from "./lifecycle.js";
2
2
 
3
+ const TERMINATION_SIGNALS: NodeJS.Signals[] = ["SIGHUP", "SIGINT", "SIGTERM"];
4
+
5
+ const isSoleHandler = (signal: NodeJS.Signals): boolean => process.listenerCount(signal) === 1;
6
+
7
+ const quitOnSignal = (signal: NodeJS.Signals): void => {
8
+ if (!isSoleHandler(signal)) {
9
+ return;
10
+ }
11
+
12
+ quit();
13
+ process.removeAllListeners(signal);
14
+ process.kill(process.pid, signal);
15
+ };
16
+
3
17
  process.on("exit", quit);
18
+
19
+ for (const signal of TERMINATION_SIGNALS) {
20
+ process.on(signal, quitOnSignal);
21
+ }
package/src/fn.ts CHANGED
@@ -8,7 +8,7 @@ import { checkError } from "./error.js";
8
8
  import { LIB } from "./library.js";
9
9
  import { fromNative } from "./native-value.js";
10
10
  import { getHandle } from "./registry.js";
11
- import { packTupleResult } from "./tuple.js";
11
+ import { hasSurfacedPrimary, packTupleResult } from "./tuple.js";
12
12
 
13
13
  /** The signature a native function is bound against. */
14
14
  type FnSpec = {
@@ -16,6 +16,11 @@ type FnSpec = {
16
16
  args: Arg[];
17
17
  /** Descriptor for the C return value, packed first when output arguments are also returned. */
18
18
  returns: Descriptor;
19
+ /**
20
+ * The callee returns a value the bindings do not surface, so the call is made against the
21
+ * declared return type and the value is dropped instead of being packed into the result.
22
+ */
23
+ isReturnSkipped?: boolean;
19
24
  /** The function takes a trailing `GError**`, whose contents are thrown as an error on return. */
20
25
  canThrow?: boolean;
21
26
  /**
@@ -135,15 +140,19 @@ const resizeInputs = (inputs: unknown[], argCount: number): unknown[] => {
135
140
  const directCallable = (
136
141
  descriptor: ExternalObject<CallDescriptor>,
137
142
  returnDescriptor: Descriptor,
143
+ hasPrimary: boolean,
138
144
  argCount: number,
139
145
  ): ((...inputs: unknown[]) => unknown) => {
140
- if (returnDescriptor.kind === "void") {
141
- return (...inputs) => {
142
- call(descriptor, resizeInputs(inputs, argCount));
143
- };
146
+ const marshal = (inputs: unknown[]): unknown =>
147
+ fromNative(returnDescriptor, call(descriptor, resizeInputs(inputs, argCount)));
148
+
149
+ if (hasPrimary) {
150
+ return (...inputs) => marshal(inputs);
144
151
  }
145
152
 
146
- return (...inputs) => fromNative(returnDescriptor, call(descriptor, resizeInputs(inputs, argCount)));
153
+ return (...inputs) => {
154
+ marshal(inputs);
155
+ };
147
156
  };
148
157
 
149
158
  function fromNativeCallable(
@@ -152,20 +161,21 @@ function fromNativeCallable(
152
161
  getRefSeeds?: () => RefSeeds | undefined,
153
162
  ): (...inputs: unknown[]) => unknown {
154
163
  const { args, returns: returnDescriptor, canThrow = false } = spec;
155
- const hasPrimary = returnDescriptor.kind !== "void";
164
+ const hasPrimary = hasSurfacedPrimary(returnDescriptor, spec.isReturnSkipped);
156
165
  const plans = buildArgSpecs(args);
157
166
  const outPlans = plans.filter((plan) => plan.isOutParam);
158
167
  const arePassThrough = plans.every((plan, index) => isPassThroughPlan(plan, index));
159
168
 
160
169
  if (!canThrow && arePassThrough) {
161
- return directCallable(descriptor, returnDescriptor, plans.length);
170
+ return directCallable(descriptor, returnDescriptor, hasPrimary, plans.length);
162
171
  }
163
172
 
164
- const shape = (inputs: unknown[], nativeValues: unknown[], nativeResult: unknown): unknown => {
165
- const primary = hasPrimary ? fromNative(returnDescriptor, nativeResult) : undefined;
166
-
167
- return packTupleResult(readOutParams(outPlans, inputs, nativeValues), primary, hasPrimary);
168
- };
173
+ const shape = (inputs: unknown[], nativeValues: unknown[], nativeResult: unknown): unknown =>
174
+ packTupleResult(
175
+ readOutParams(outPlans, inputs, nativeValues),
176
+ fromNative(returnDescriptor, nativeResult),
177
+ hasPrimary,
178
+ );
169
179
 
170
180
  if (canThrow) {
171
181
  return (...inputs) => {
@@ -0,0 +1,64 @@
1
+ import type { Descriptor } from "@gtkx/native";
2
+
3
+ type LengthSource = { kind: "return" } | { kind: "outArg"; argIndex: number };
4
+ type LengthSources = Map<number, LengthSource[]>;
5
+
6
+ type FoldedLengthSpec = {
7
+ argDescriptors: Descriptor[];
8
+ returnDescriptor: Descriptor;
9
+ userDataIndex?: number | undefined;
10
+ };
11
+
12
+ const sizedArrayLengthIndex = (descriptor: Descriptor): number | undefined =>
13
+ descriptor.kind === "array" && descriptor.arrayKind === "sized" ? descriptor.sizeParamIndex : undefined;
14
+
15
+ const outArgLengthIndex = (descriptor: Descriptor | undefined): number | undefined =>
16
+ descriptor?.kind === "ref" ? sizedArrayLengthIndex(descriptor.innerDescriptor) : undefined;
17
+
18
+ const effectiveArgIndex = (argIndex: number | undefined, userDataIndex: number | undefined): number | undefined => {
19
+ if (argIndex === undefined || userDataIndex === undefined || argIndex < userDataIndex) {
20
+ return argIndex;
21
+ }
22
+
23
+ return argIndex === userDataIndex ? undefined : argIndex - 1;
24
+ };
25
+
26
+ const addLengthSource = (sources: LengthSources, index: number | undefined, source: LengthSource): void => {
27
+ if (index === undefined) {
28
+ return;
29
+ }
30
+
31
+ const existing = sources.get(index);
32
+
33
+ if (existing === undefined) {
34
+ sources.set(index, [source]);
35
+
36
+ return;
37
+ }
38
+
39
+ existing.push(source);
40
+ };
41
+
42
+ const addOutArgLengthSources = (sources: LengthSources, spec: FoldedLengthSpec): void => {
43
+ const { userDataIndex } = spec;
44
+
45
+ for (const [declaredIndex, descriptor] of spec.argDescriptors.entries()) {
46
+ const argIndex = effectiveArgIndex(declaredIndex, userDataIndex);
47
+
48
+ if (argIndex !== undefined) {
49
+ const lengthIndex = effectiveArgIndex(outArgLengthIndex(descriptor), userDataIndex);
50
+ addLengthSource(sources, lengthIndex, { kind: "outArg", argIndex });
51
+ }
52
+ }
53
+ };
54
+
55
+ const foldedLengthSources = (spec: FoldedLengthSpec): LengthSources => {
56
+ const sources: LengthSources = new Map();
57
+ const returnLengthIndex = effectiveArgIndex(sizedArrayLengthIndex(spec.returnDescriptor), spec.userDataIndex);
58
+ addLengthSource(sources, returnLengthIndex, { kind: "return" });
59
+ addOutArgLengthSources(sources, spec);
60
+
61
+ return sources;
62
+ };
63
+
64
+ export { foldedLengthSources, type LengthSource, type LengthSources };
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.