@gtkx/runtime 0.0.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +171 -3
  3. package/dist/arg.d.ts +13 -0
  4. package/dist/arg.d.ts.map +1 -0
  5. package/dist/arg.js +6 -0
  6. package/dist/arg.js.map +1 -0
  7. package/dist/bind.d.ts +5 -0
  8. package/dist/bind.d.ts.map +1 -0
  9. package/dist/bind.js +19 -0
  10. package/dist/bind.js.map +1 -0
  11. package/dist/callback.d.ts +13 -0
  12. package/dist/callback.d.ts.map +1 -0
  13. package/dist/callback.js +86 -0
  14. package/dist/callback.js.map +1 -0
  15. package/dist/descriptors.d.ts +158 -0
  16. package/dist/descriptors.d.ts.map +1 -0
  17. package/dist/descriptors.js +135 -0
  18. package/dist/descriptors.js.map +1 -0
  19. package/dist/error.d.ts +24 -0
  20. package/dist/error.d.ts.map +1 -0
  21. package/dist/error.js +33 -0
  22. package/dist/error.js.map +1 -0
  23. package/dist/exit-hook.d.ts +2 -0
  24. package/dist/exit-hook.d.ts.map +1 -0
  25. package/dist/exit-hook.js +3 -0
  26. package/dist/exit-hook.js.map +1 -0
  27. package/dist/fn.d.ts +10 -0
  28. package/dist/fn.d.ts.map +1 -0
  29. package/dist/fn.js +90 -0
  30. package/dist/fn.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +16 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/internal.d.ts +5 -0
  36. package/dist/internal.d.ts.map +1 -0
  37. package/dist/internal.js +5 -0
  38. package/dist/internal.js.map +1 -0
  39. package/dist/library.d.ts +8 -0
  40. package/dist/library.d.ts.map +1 -0
  41. package/dist/library.js +14 -0
  42. package/dist/library.js.map +1 -0
  43. package/dist/lifecycle.d.ts +42 -0
  44. package/dist/lifecycle.d.ts.map +1 -0
  45. package/dist/lifecycle.js +70 -0
  46. package/dist/lifecycle.js.map +1 -0
  47. package/dist/listeners.d.ts +36 -0
  48. package/dist/listeners.d.ts.map +1 -0
  49. package/dist/listeners.js +85 -0
  50. package/dist/listeners.js.map +1 -0
  51. package/dist/mixin.d.ts +13 -0
  52. package/dist/mixin.d.ts.map +1 -0
  53. package/dist/mixin.js +49 -0
  54. package/dist/mixin.js.map +1 -0
  55. package/dist/native-value.d.ts +13 -0
  56. package/dist/native-value.d.ts.map +1 -0
  57. package/dist/native-value.js +105 -0
  58. package/dist/native-value.js.map +1 -0
  59. package/dist/object.d.ts +32 -0
  60. package/dist/object.d.ts.map +1 -0
  61. package/dist/object.js +66 -0
  62. package/dist/object.js.map +1 -0
  63. package/dist/promisify.d.ts +21 -0
  64. package/dist/promisify.d.ts.map +1 -0
  65. package/dist/promisify.js +39 -0
  66. package/dist/promisify.js.map +1 -0
  67. package/dist/properties.d.ts +14 -0
  68. package/dist/properties.d.ts.map +1 -0
  69. package/dist/properties.js +79 -0
  70. package/dist/properties.js.map +1 -0
  71. package/dist/register-class.d.ts +25 -0
  72. package/dist/register-class.d.ts.map +1 -0
  73. package/dist/register-class.js +178 -0
  74. package/dist/register-class.js.map +1 -0
  75. package/dist/registry.d.ts +69 -0
  76. package/dist/registry.d.ts.map +1 -0
  77. package/dist/registry.js +186 -0
  78. package/dist/registry.js.map +1 -0
  79. package/dist/signal.d.ts +35 -0
  80. package/dist/signal.d.ts.map +1 -0
  81. package/dist/signal.js +95 -0
  82. package/dist/signal.js.map +1 -0
  83. package/dist/t.d.ts +50 -0
  84. package/dist/t.d.ts.map +1 -0
  85. package/dist/t.js +49 -0
  86. package/dist/t.js.map +1 -0
  87. package/dist/tuple.d.ts +7 -0
  88. package/dist/tuple.d.ts.map +1 -0
  89. package/dist/tuple.js +26 -0
  90. package/dist/tuple.js.map +1 -0
  91. package/dist/type.d.ts +88 -0
  92. package/dist/type.d.ts.map +1 -0
  93. package/dist/type.js +194 -0
  94. package/dist/type.js.map +1 -0
  95. package/dist/value.d.ts +29 -0
  96. package/dist/value.d.ts.map +1 -0
  97. package/dist/value.js +292 -0
  98. package/dist/value.js.map +1 -0
  99. package/package.json +54 -13
  100. package/src/arg.ts +15 -0
  101. package/src/bind.ts +31 -0
  102. package/src/callback.ts +149 -0
  103. package/src/descriptors.ts +350 -0
  104. package/src/error.ts +58 -0
  105. package/src/exit-hook.ts +3 -0
  106. package/src/fn.ts +139 -0
  107. package/src/index.ts +59 -0
  108. package/src/internal.ts +12 -0
  109. package/src/library.ts +17 -0
  110. package/src/lifecycle.ts +101 -0
  111. package/src/listeners.ts +112 -0
  112. package/src/mixin.ts +73 -0
  113. package/src/native-value.ts +135 -0
  114. package/src/object.ts +95 -0
  115. package/src/promisify.ts +67 -0
  116. package/src/properties.ts +115 -0
  117. package/src/register-class.ts +305 -0
  118. package/src/registry.ts +311 -0
  119. package/src/signal.ts +192 -0
  120. package/src/t.ts +90 -0
  121. package/src/tuple.ts +37 -0
  122. package/src/type.ts +290 -0
  123. package/src/value.ts +430 -0
@@ -0,0 +1,101 @@
1
+ import { keepAlive, quit as nativeQuit } from "@gtkx/native";
2
+ import { blockMatchedSignalHandlers } from "./signal.js";
3
+
4
+ /**
5
+ * Minimal structural interface of a GTK4/GIO application needed to register, run,
6
+ * and shut it down, plus the signals its lifecycle helpers connect to.
7
+ */
8
+ type ApplicationLike = {
9
+ getIsRegistered(): boolean;
10
+ register(cancellable: null): boolean;
11
+ activate(): void;
12
+ quit(): void;
13
+ run(argv: string[]): number;
14
+ getWindows?(): object[];
15
+ removeWindow?(window: object): void;
16
+ on(signal: "activate" | "shutdown", handler: () => void): unknown;
17
+ };
18
+
19
+ const shutdownCallbacks: (() => void)[] = [];
20
+ /**
21
+ * Runs every registered exit callback and shuts down the native runtime. Safe to
22
+ * call more than once; only the first call takes effect.
23
+ */
24
+ const quit: () => void = createQuit();
25
+
26
+ function createQuit(): () => void {
27
+ let hasQuit = false;
28
+
29
+ return () => {
30
+ if (hasQuit) {
31
+ return;
32
+ }
33
+
34
+ hasQuit = true;
35
+
36
+ for (const callback of shutdownCallbacks) {
37
+ callback();
38
+ }
39
+
40
+ nativeQuit();
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Registers a callback to run once when the process quits, before the native
46
+ * runtime is torn down.
47
+ *
48
+ * @param callback Invoked during shutdown.
49
+ */
50
+ const onExit = (callback: () => void): void => {
51
+ shutdownCallbacks.push(callback);
52
+ };
53
+
54
+ /**
55
+ * Registers the application if needed and activates it, keeping the runtime alive
56
+ * while it is active and releasing it on shutdown.
57
+ *
58
+ * @param application The application to register and activate.
59
+ */
60
+ const runApplication = (application: ApplicationLike): void => {
61
+ application.on("activate", () => {
62
+ keepAlive(true);
63
+ });
64
+
65
+ application.on("shutdown", () => {
66
+ keepAlive(false);
67
+ });
68
+
69
+ if (!application.getIsRegistered()) {
70
+ application.register(null);
71
+ }
72
+
73
+ application.activate();
74
+ };
75
+
76
+ /**
77
+ * Closes all of the application's windows and drives its main loop until it quits,
78
+ * so pending shutdown work can complete before the process exits.
79
+ *
80
+ * @param application The application to shut down.
81
+ */
82
+ const quitApplication = (application: ApplicationLike): void => {
83
+ if (!application.getIsRegistered()) {
84
+ return;
85
+ }
86
+
87
+ const windows = application.getWindows?.() ?? [];
88
+
89
+ for (const window of windows) {
90
+ application.removeWindow?.(window);
91
+ }
92
+
93
+ application.on("shutdown", () => {
94
+ application.quit();
95
+ });
96
+
97
+ blockMatchedSignalHandlers(application, "activate");
98
+ application.run([]);
99
+ };
100
+
101
+ export { onExit, quit, runApplication, quitApplication, type ApplicationLike };
@@ -0,0 +1,112 @@
1
+ import { isSignalHandlerConnected, type SignalHandler } from "./signal.js";
2
+
3
+ type SignalConnectable = {
4
+ connect(signal: string, handler: SignalHandler, isAfter?: boolean): number;
5
+ disconnect(handlerId: number): void;
6
+ };
7
+
8
+ const listenerTable: WeakMap<object, Map<string, Map<SignalHandler, number>>> = new WeakMap();
9
+
10
+ const findListenerHandlerId = (instance: object, signal: string, handler: SignalHandler): number | undefined =>
11
+ listenerTable.get(instance)?.get(signal)?.get(handler);
12
+
13
+ const trackListener = (instance: object, signal: string, handler: SignalHandler, handlerId: number): void => {
14
+ let bySignal = listenerTable.get(instance);
15
+
16
+ if (!bySignal) {
17
+ bySignal = new Map();
18
+ listenerTable.set(instance, bySignal);
19
+ }
20
+
21
+ let byHandler = bySignal.get(signal);
22
+
23
+ if (!byHandler) {
24
+ byHandler = new Map();
25
+ bySignal.set(signal, byHandler);
26
+ }
27
+
28
+ byHandler.set(handler, handlerId);
29
+ };
30
+
31
+ const untrackHandlerId = (instance: object, signal: string, handlerId: number): void => {
32
+ const bySignal = listenerTable.get(instance);
33
+ const byHandler = bySignal?.get(signal);
34
+
35
+ if (byHandler === undefined) {
36
+ return;
37
+ }
38
+
39
+ for (const [handler, id] of byHandler) {
40
+ if (id === handlerId) {
41
+ byHandler.delete(handler);
42
+ }
43
+ }
44
+
45
+ if (byHandler.size === 0) {
46
+ bySignal?.delete(signal);
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Connects a handler to a signal and tracks it so it can later be removed with
52
+ * {@link offSignal}.
53
+ *
54
+ * @param instance The object emitting the signal.
55
+ * @param signal The signal name to connect to.
56
+ * @param handler The callback invoked on each emission.
57
+ * @param isAfter When true, run the handler after the default handler.
58
+ */
59
+ function onSignal(instance: SignalConnectable, signal: string, handler: SignalHandler, isAfter?: boolean): void {
60
+ const handlerId = instance.connect(signal, handler, isAfter);
61
+ trackListener(instance, signal, handler, handlerId);
62
+ }
63
+
64
+ /**
65
+ * Connects a handler that runs at most once, disconnecting itself after the first
66
+ * emission.
67
+ *
68
+ * @param instance The object emitting the signal.
69
+ * @param signal The signal name to connect to.
70
+ * @param handler The callback invoked on the first emission.
71
+ * @param isAfter When true, run the handler after the default handler.
72
+ */
73
+ function onceSignal(instance: SignalConnectable, signal: string, handler: SignalHandler, isAfter?: boolean): void {
74
+ let handlerId = 0;
75
+
76
+ const wrapped: SignalHandler = (...args) => {
77
+ untrackHandlerId(instance, signal, handlerId);
78
+ instance.disconnect(handlerId);
79
+
80
+ return handler(...args);
81
+ };
82
+
83
+ handlerId = instance.connect(signal, wrapped, isAfter);
84
+ trackListener(instance, signal, wrapped, handlerId);
85
+ trackListener(instance, signal, handler, handlerId);
86
+ }
87
+
88
+ /**
89
+ * Disconnects a handler previously connected with {@link onSignal} or
90
+ * {@link onceSignal}. Does nothing if the handler is not currently connected.
91
+ *
92
+ * @param instance The object the handler was connected to.
93
+ * @param signal The signal name the handler was connected to.
94
+ * @param handler The handler to disconnect.
95
+ */
96
+ function offSignal(instance: SignalConnectable, signal: string, handler: SignalHandler): void {
97
+ const handlerId = findListenerHandlerId(instance, signal, handler);
98
+
99
+ if (handlerId === undefined) {
100
+ return;
101
+ }
102
+
103
+ untrackHandlerId(instance, signal, handlerId);
104
+
105
+ if (!isSignalHandlerConnected(instance, handlerId)) {
106
+ return;
107
+ }
108
+
109
+ instance.disconnect(handlerId);
110
+ }
111
+
112
+ export { onSignal, onceSignal, offSignal };
package/src/mixin.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { AnyClass } from "@gtkx/utils";
2
+
3
+ type MixinReceiver = {
4
+ connect(signal: string, handler: (...args: unknown[]) => unknown, isAfter?: boolean): number;
5
+ emit(signal: string, ...args: unknown[]): unknown;
6
+ };
7
+
8
+ /**
9
+ * A factory that, given a base class, returns a subclass adding extra members to
10
+ * be merged onto a target prototype.
11
+ */
12
+ type Mixin = (base: AnyClass<MixinReceiver>) => AnyClass;
13
+
14
+ function isDefinedInClassChain(prototype: object, key: string): boolean {
15
+ let current: object | null = prototype;
16
+
17
+ while (current !== null && current !== Object.prototype) {
18
+ if (Object.hasOwn(current, key)) {
19
+ return true;
20
+ }
21
+
22
+ current = Reflect.getPrototypeOf(current);
23
+ }
24
+
25
+ return false;
26
+ }
27
+
28
+ function copyLayerMember(target: AnyClass, layer: object, key: string): void {
29
+ if (key === "constructor") {
30
+ return;
31
+ }
32
+
33
+ if (isDefinedInClassChain(target.prototype, key)) {
34
+ return;
35
+ }
36
+
37
+ const descriptor = Object.getOwnPropertyDescriptor(layer, key);
38
+
39
+ if (descriptor !== undefined) {
40
+ Object.defineProperty(target.prototype, key, descriptor);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Copies each mixin's prototype members onto the target class prototype, skipping
46
+ * any member already defined anywhere in the target's class chain.
47
+ *
48
+ * @param target The class whose prototype receives the mixin members.
49
+ * @param mixins The mixins to apply, in order.
50
+ */
51
+ function copyLayerMembers(target: AnyClass, layer: object): void {
52
+ for (const key of Object.getOwnPropertyNames(layer)) {
53
+ copyLayerMember(target, layer, key);
54
+ }
55
+ }
56
+
57
+ function installMixins(target: AnyClass, mixins: Mixin[]): void {
58
+ const empty: AnyClass<MixinReceiver> = class {
59
+ connect(): number {
60
+ return 0;
61
+ }
62
+
63
+ emit(): unknown {
64
+ return undefined;
65
+ }
66
+ };
67
+
68
+ for (const mixin of mixins) {
69
+ copyLayerMembers(target, mixin(empty).prototype);
70
+ }
71
+ }
72
+
73
+ export { installMixins, type MixinReceiver, type Mixin };
@@ -0,0 +1,135 @@
1
+ import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
2
+ import type { ArrayDescriptor, FundamentalDescriptor, HashTableDescriptor, StructDescriptor } from "./descriptors.js";
3
+ import { getWrapperClass, tryGetHandle, wrapHandle } from "./registry.js";
4
+ import { resolveDescriptorType } from "./type.js";
5
+
6
+ type MarshalledKind = "object" | "struct" | "boxed" | "fundamental" | "array" | "hashtable";
7
+ type MarshalledDescriptor = Extract<Descriptor, { kind: MarshalledKind }>;
8
+
9
+ const MARSHALLED_KINDS: Set<Descriptor["kind"]> = new Set<MarshalledKind>([
10
+ "object",
11
+ "struct",
12
+ "boxed",
13
+ "fundamental",
14
+ "array",
15
+ "hashtable",
16
+ ]);
17
+
18
+ function isMarshalledDescriptor(descriptor: Descriptor): descriptor is MarshalledDescriptor {
19
+ return MARSHALLED_KINDS.has(descriptor.kind);
20
+ }
21
+
22
+ function mapCollection(
23
+ descriptor: ArrayDescriptor,
24
+ value: unknown,
25
+ convert: (itemDescriptor: Descriptor, item: unknown) => unknown,
26
+ ): unknown[] {
27
+ return (value as unknown[]).map((item) => convert(descriptor.itemDescriptor, item));
28
+ }
29
+
30
+ function collectionFromNative(descriptor: ArrayDescriptor, value: unknown): unknown {
31
+ return value === null ? null : mapCollection(descriptor, value, fromNative);
32
+ }
33
+
34
+ function boxedFromNative(descriptor: Descriptor, value: unknown): unknown {
35
+ return value == null
36
+ ? null
37
+ : wrapHandle(value as ExternalObject<Handle>, getWrapperClass(resolveDescriptorType(descriptor)));
38
+ }
39
+
40
+ function fundamentalFromNative(descriptor: FundamentalDescriptor, value: unknown): unknown {
41
+ return value == null
42
+ ? null
43
+ : wrapHandle(
44
+ value as ExternalObject<Handle>,
45
+ descriptor.wrapperClass ?? getWrapperClass(resolveDescriptorType(descriptor)),
46
+ );
47
+ }
48
+
49
+ function hashTableFromNative(descriptor: HashTableDescriptor, value: unknown): unknown {
50
+ if (value === null) {
51
+ return null;
52
+ }
53
+
54
+ const entries = value as [unknown, unknown][];
55
+
56
+ return new Map(
57
+ entries.map(([key, val]): [unknown, unknown] => [
58
+ fromNative(descriptor.keyDescriptor, key),
59
+ fromNative(descriptor.valueDescriptor, val),
60
+ ]),
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Converts a raw value returned from native code into its JavaScript form,
66
+ * wrapping object, struct, boxed, and fundamental handles and recursively
67
+ * converting arrays and hash tables according to the descriptor.
68
+ *
69
+ * @param descriptor Describes the native type of the value.
70
+ * @param value The raw native value to convert.
71
+ */
72
+ function fromNative(descriptor: Descriptor, value: unknown): unknown {
73
+ if (!isMarshalledDescriptor(descriptor)) {
74
+ return value;
75
+ }
76
+
77
+ switch (descriptor.kind) {
78
+ case "object": {
79
+ return wrapHandle(value as ExternalObject<Handle> | null);
80
+ }
81
+ case "struct": {
82
+ return wrapHandle(value as ExternalObject<Handle> | null, (descriptor as StructDescriptor).wrapperClass);
83
+ }
84
+ case "boxed": {
85
+ return boxedFromNative(descriptor, value);
86
+ }
87
+ case "fundamental": {
88
+ return fundamentalFromNative(descriptor, value);
89
+ }
90
+ case "array": {
91
+ return collectionFromNative(descriptor, value);
92
+ }
93
+ case "hashtable": {
94
+ return hashTableFromNative(descriptor, value);
95
+ }
96
+ }
97
+ }
98
+
99
+ function collectionToNative(descriptor: ArrayDescriptor, value: unknown): unknown {
100
+ return value == null ? null : mapCollection(descriptor, value, toNative);
101
+ }
102
+
103
+ function hashTableToNative(descriptor: HashTableDescriptor, value: unknown): unknown {
104
+ if (value == null) {
105
+ return null;
106
+ }
107
+
108
+ return [...(value as Map<unknown, unknown>)].map(([key, val]): [unknown, unknown] => [
109
+ toNative(descriptor.keyDescriptor, key),
110
+ toNative(descriptor.valueDescriptor, val),
111
+ ]);
112
+ }
113
+
114
+ function toNative(descriptor: Descriptor, value: unknown): unknown {
115
+ if (!isMarshalledDescriptor(descriptor)) {
116
+ return value;
117
+ }
118
+
119
+ switch (descriptor.kind) {
120
+ case "object":
121
+ case "struct":
122
+ case "boxed":
123
+ case "fundamental": {
124
+ return tryGetHandle(value as object | null | undefined) ?? null;
125
+ }
126
+ case "array": {
127
+ return collectionToNative(descriptor, value);
128
+ }
129
+ case "hashtable": {
130
+ return hashTableToNative(descriptor, value);
131
+ }
132
+ }
133
+ }
134
+
135
+ export { fromNative, toNative };
package/src/object.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
2
+ import { bind } from "./bind.js";
3
+ import { biguint64T, objectT, sizedArrayT, stringT, uint32T, voidT } from "./descriptors.js";
4
+ import { LIB, VALUE_SIZE, VALUE_T } from "./library.js";
5
+ import { getHandle } from "./registry.js";
6
+ import { fromValue, newValueForDescriptor, toValue } from "./value.js";
7
+
8
+ const gObjectNewWithProperties = bind(
9
+ LIB,
10
+ "g_object_new_with_properties",
11
+ [
12
+ biguint64T,
13
+ uint32T,
14
+ sizedArrayT(stringT("borrowed"), 1, "borrowed"),
15
+ sizedArrayT(VALUE_T, 1, "borrowed", VALUE_SIZE),
16
+ ],
17
+ objectT("full"),
18
+ );
19
+
20
+ const gObjectGetProperty = bind(
21
+ LIB,
22
+ "g_object_get_property",
23
+ [objectT("borrowed"), stringT("borrowed"), VALUE_T],
24
+ voidT,
25
+ );
26
+
27
+ const gObjectSetProperty = bind(
28
+ LIB,
29
+ "g_object_set_property",
30
+ [objectT("borrowed"), stringT("borrowed"), VALUE_T],
31
+ voidT,
32
+ );
33
+
34
+ /**
35
+ * Constructs a new GObject of the given type, setting the supplied construct
36
+ * properties. Entries whose value is `undefined` or not a `[descriptor, value]`
37
+ * pair are skipped.
38
+ *
39
+ * @param gtype The GType of the object to construct.
40
+ * @param props Property names mapped to `[descriptor, value]` pairs.
41
+ * @returns The handle of the newly created object.
42
+ */
43
+ function newObjectWithProperties(gtype: bigint, props: Record<string, unknown>): ExternalObject<Handle> {
44
+ const names: string[] = [];
45
+ const values: ExternalObject<Handle>[] = [];
46
+
47
+ for (const name in props) {
48
+ const entry: unknown = props[name];
49
+
50
+ if (!Array.isArray(entry)) {
51
+ continue;
52
+ }
53
+
54
+ const [descriptor, value] = entry as [Descriptor, unknown];
55
+
56
+ if (value === undefined) {
57
+ continue;
58
+ }
59
+
60
+ names.push(name);
61
+ values.push(toValue(descriptor, value));
62
+ }
63
+
64
+ return gObjectNewWithProperties(gtype, names.length, names, values) as ExternalObject<Handle>;
65
+ }
66
+
67
+ /**
68
+ * Reads a GObject property and converts it to its JavaScript value using the
69
+ * descriptor.
70
+ *
71
+ * @param obj The object to read from.
72
+ * @param propertyName The property name.
73
+ * @param descriptor Describes the property's type.
74
+ */
75
+ function getObjectProperty(obj: object, propertyName: string, descriptor: Descriptor): unknown {
76
+ const value = newValueForDescriptor(descriptor);
77
+ gObjectGetProperty(getHandle(obj), propertyName, value);
78
+
79
+ return fromValue(value);
80
+ }
81
+
82
+ /**
83
+ * Writes a JavaScript value to a GObject property, converting it to native form
84
+ * using the descriptor.
85
+ *
86
+ * @param obj The object to write to.
87
+ * @param propertyName The property name.
88
+ * @param descriptor Describes the property's type.
89
+ * @param jsValue The value to set.
90
+ */
91
+ function setObjectProperty(obj: object, propertyName: string, descriptor: Descriptor, jsValue: unknown): void {
92
+ gObjectSetProperty(getHandle(obj), propertyName, toValue(descriptor, jsValue));
93
+ }
94
+
95
+ export { newObjectWithProperties, getObjectProperty, setObjectProperty };
@@ -0,0 +1,67 @@
1
+ import { tryGetHandle } from "./registry.js";
2
+
3
+ /**
4
+ * Extracts the finished value from the async result a GIO-style operation completes with.
5
+ *
6
+ * @template R - The async result type passed to the completion callback.
7
+ * @template T - The finished value type.
8
+ */
9
+ type FinishResult<R extends object, T> = (result: R) => T;
10
+
11
+ type Settlement<R extends object, T> = {
12
+ finish: FinishResult<R, T>;
13
+ creationStack: Error | undefined;
14
+ resolve: (value: T) => void;
15
+ reject: (reason: Error) => void;
16
+ };
17
+
18
+ const attachCreationStack = (error: unknown, creationStack: Error | undefined): void => {
19
+ if (creationStack === undefined || !(error instanceof Error)) {
20
+ return;
21
+ }
22
+
23
+ if (error.cause !== undefined || !Object.isExtensible(error)) {
24
+ return;
25
+ }
26
+
27
+ error.cause = creationStack;
28
+ };
29
+
30
+ const settle = <R extends object, T>(settlement: Settlement<R, T>, asyncResult: object): void => {
31
+ const { finish, creationStack, resolve, reject } = settlement;
32
+
33
+ try {
34
+ resolve(finish(asyncResult as R));
35
+ } catch (error) {
36
+ attachCreationStack(error, creationStack);
37
+ reject(error instanceof Error ? error : new Error(String(error)));
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Wraps a GIO-style asynchronous function that takes a completion callback into a
43
+ * promise, invoking the finish function to extract the result. Outside production,
44
+ * the call site's stack is captured and attached as the rejection error's cause.
45
+ *
46
+ * @param asyncFn The async function, called with the leading arguments, the cancellable, and a completion callback.
47
+ * @param finish Extracts the result from the async result passed to the completion callback.
48
+ * @param cancellable A cancellable object, or null/undefined for none.
49
+ * @param leading Arguments passed to `asyncFn` before the cancellable and callback.
50
+ * @returns A promise resolving to the finished result, or rejecting if `finish` throws.
51
+ */
52
+ const promisify = <R extends object, T>(
53
+ asyncFn: (...args: unknown[]) => void,
54
+ finish: FinishResult<R, T>,
55
+ cancellable: object | null | undefined,
56
+ ...leading: unknown[]
57
+ ): Promise<T> =>
58
+ new Promise<T>((resolve, reject) => {
59
+ const creationStack =
60
+ process.env.NODE_ENV === "production" ? undefined : new Error("gtkx async operation started here");
61
+
62
+ asyncFn(...leading, tryGetHandle(cancellable), (_source: object | null, asyncResult: object) => {
63
+ settle({ finish, creationStack, resolve, reject }, asyncResult);
64
+ });
65
+ });
66
+
67
+ export { promisify, type FinishResult };
@@ -0,0 +1,115 @@
1
+ import type { ExternalObject, Handle, RegisterClassProperty } from "@gtkx/native";
2
+ import type { AnyClass } from "@gtkx/utils";
3
+ import { camelCase } from "@gtkx/utils";
4
+ import { bind } from "./bind.js";
5
+ import { LIB, PARAM_T, VALUE_T } from "./library.js";
6
+ import { getHandle } from "./registry.js";
7
+ import { fromValue, intoValue } from "./value.js";
8
+
9
+ type PropertyAccessor = {
10
+ name: string;
11
+ storage: symbol;
12
+ pspec: object;
13
+ };
14
+
15
+ type PropertySpec = object;
16
+ type NotifyingObject = { notify?: (propertyName: string) => void };
17
+
18
+ const FIRST_PROPERTY_ID = 1;
19
+ const paramSpecDefaultValue = bind(LIB, "g_param_spec_get_default_value", [PARAM_T], VALUE_T);
20
+
21
+ const underscoreCase = (name: string): string => name.replaceAll("-", "_");
22
+ const accessorNames = (name: string): string[] => [...new Set([name, underscoreCase(name), camelCase(name)])];
23
+
24
+ function readStored(instance: Record<symbol, unknown>, accessor: PropertyAccessor): unknown {
25
+ if (!Object.hasOwn(instance, accessor.storage)) {
26
+ const defaults = paramSpecDefaultValue(getHandle(accessor.pspec)) as ExternalObject<Handle>;
27
+ instance[accessor.storage] = fromValue(defaults);
28
+ }
29
+
30
+ return instance[accessor.storage];
31
+ }
32
+
33
+ function defineAccessor(prototype: object, accessor: PropertyAccessor, alias: string): void {
34
+ if (Object.getOwnPropertyDescriptor(prototype, alias) !== undefined) {
35
+ return;
36
+ }
37
+
38
+ Object.defineProperty(prototype, alias, {
39
+ configurable: true,
40
+ enumerable: true,
41
+ get(this: Record<symbol, unknown>) {
42
+ return readStored(this, accessor);
43
+ },
44
+ set(this: Record<symbol, unknown>, value: unknown) {
45
+ if (readStored(this, accessor) === value) {
46
+ return;
47
+ }
48
+
49
+ this[accessor.storage] = value;
50
+ (this as NotifyingObject).notify?.(accessor.name);
51
+ },
52
+ });
53
+ }
54
+
55
+ function installAccessors(klass: AnyClass, accessor: PropertyAccessor): void {
56
+ const prototype = (klass as { prototype: object }).prototype;
57
+
58
+ for (const alias of accessorNames(accessor.name)) {
59
+ defineAccessor(prototype, accessor, alias);
60
+ }
61
+ }
62
+
63
+ function resolveAccessor(accessors: PropertyAccessor[], propertyId: number): PropertyAccessor {
64
+ const accessor = accessors[propertyId - FIRST_PROPERTY_ID];
65
+
66
+ if (accessor === undefined) {
67
+ throw new RangeError(`registerClass: no property registered for id ${String(propertyId)}`);
68
+ }
69
+
70
+ return accessor;
71
+ }
72
+
73
+ function makeGetProperty(accessors: PropertyAccessor[]) {
74
+ return function getProperty(this: object, propertyId: number, value: object): void {
75
+ const accessor = resolveAccessor(accessors, propertyId);
76
+ const current = (this as Record<string, unknown>)[camelCase(accessor.name)];
77
+ intoValue(getHandle(value), current);
78
+ };
79
+ }
80
+
81
+ function makeSetProperty(accessors: PropertyAccessor[]) {
82
+ return function setProperty(this: object, propertyId: number, value: object): void {
83
+ const accessor = resolveAccessor(accessors, propertyId);
84
+ const jsValue = fromValue(getHandle(value));
85
+ (this as Record<string, unknown>)[camelCase(accessor.name)] = jsValue;
86
+ };
87
+ }
88
+
89
+ function buildAccessors(klass: AnyClass, properties: Record<string, PropertySpec>): PropertyAccessor[] {
90
+ const accessors: PropertyAccessor[] = [];
91
+
92
+ for (const [name, pspec] of Object.entries(properties)) {
93
+ const accessor: PropertyAccessor = { name, storage: Symbol(`gtkx:property:${name}`), pspec };
94
+ installAccessors(klass, accessor);
95
+ accessors.push(accessor);
96
+ }
97
+
98
+ return accessors;
99
+ }
100
+
101
+ function toNativeProperties(properties: Record<string, PropertySpec>): RegisterClassProperty[] {
102
+ return Object.values(properties).map((pspec, index) => ({
103
+ id: index + FIRST_PROPERTY_ID,
104
+ pspec: getHandle(pspec),
105
+ }));
106
+ }
107
+
108
+ export {
109
+ buildAccessors,
110
+ makeGetProperty,
111
+ makeSetProperty,
112
+ toNativeProperties,
113
+ type PropertyAccessor,
114
+ type PropertySpec,
115
+ };