@gtkx/runtime 1.0.0-rc.4 → 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 (156) hide show
  1. package/README.md +5 -5
  2. package/dist/application-class.d.ts +37 -0
  3. package/dist/application-class.d.ts.map +1 -0
  4. package/dist/application-class.js +78 -0
  5. package/dist/application-class.js.map +1 -0
  6. package/dist/arg.d.ts +2 -1
  7. package/dist/arg.d.ts.map +1 -1
  8. package/dist/arg.js +2 -1
  9. package/dist/arg.js.map +1 -1
  10. package/dist/bind.d.ts +11 -0
  11. package/dist/bind.d.ts.map +1 -1
  12. package/dist/bind.js +11 -0
  13. package/dist/bind.js.map +1 -1
  14. package/dist/callback.d.ts +4 -3
  15. package/dist/callback.d.ts.map +1 -1
  16. package/dist/callback.js +124 -29
  17. package/dist/callback.js.map +1 -1
  18. package/dist/closure.d.ts +26 -0
  19. package/dist/closure.d.ts.map +1 -0
  20. package/dist/closure.js +99 -0
  21. package/dist/closure.js.map +1 -0
  22. package/dist/default-application.d.ts +4 -0
  23. package/dist/default-application.d.ts.map +1 -0
  24. package/dist/default-application.js +17 -0
  25. package/dist/default-application.js.map +1 -0
  26. package/dist/descriptors.d.ts +98 -4
  27. package/dist/descriptors.d.ts.map +1 -1
  28. package/dist/descriptors.js +72 -6
  29. package/dist/descriptors.js.map +1 -1
  30. package/dist/error.d.ts +1 -0
  31. package/dist/error.d.ts.map +1 -1
  32. package/dist/error.js.map +1 -1
  33. package/dist/exit-hook.js +13 -0
  34. package/dist/exit-hook.js.map +1 -1
  35. package/dist/fn.d.ts +24 -2
  36. package/dist/fn.d.ts.map +1 -1
  37. package/dist/fn.js +67 -32
  38. package/dist/fn.js.map +1 -1
  39. package/dist/folded-lengths.d.ts +16 -0
  40. package/dist/folded-lengths.d.ts.map +1 -0
  41. package/dist/folded-lengths.js +38 -0
  42. package/dist/folded-lengths.js.map +1 -0
  43. package/dist/index.d.ts +14 -6
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +12 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal.d.ts +2 -0
  48. package/dist/internal.d.ts.map +1 -1
  49. package/dist/internal.js +2 -0
  50. package/dist/internal.js.map +1 -1
  51. package/dist/library.d.ts +3 -1
  52. package/dist/library.d.ts.map +1 -1
  53. package/dist/library.js +3 -1
  54. package/dist/library.js.map +1 -1
  55. package/dist/lifecycle.d.ts +58 -11
  56. package/dist/lifecycle.d.ts.map +1 -1
  57. package/dist/lifecycle.js +95 -22
  58. package/dist/lifecycle.js.map +1 -1
  59. package/dist/listeners.d.ts.map +1 -1
  60. package/dist/listeners.js +19 -5
  61. package/dist/listeners.js.map +1 -1
  62. package/dist/mixin.d.ts +3 -2
  63. package/dist/mixin.d.ts.map +1 -1
  64. package/dist/mixin.js +13 -1
  65. package/dist/mixin.js.map +1 -1
  66. package/dist/native-value.d.ts.map +1 -1
  67. package/dist/native-value.js +4 -3
  68. package/dist/native-value.js.map +1 -1
  69. package/dist/object.d.ts +40 -7
  70. package/dist/object.d.ts.map +1 -1
  71. package/dist/object.js +95 -26
  72. package/dist/object.js.map +1 -1
  73. package/dist/param-spec.d.ts +11 -0
  74. package/dist/param-spec.d.ts.map +1 -0
  75. package/dist/param-spec.js +147 -0
  76. package/dist/param-spec.js.map +1 -0
  77. package/dist/promisify.d.ts.map +1 -1
  78. package/dist/promisify.js +3 -2
  79. package/dist/promisify.js.map +1 -1
  80. package/dist/properties.d.ts +39 -7
  81. package/dist/properties.d.ts.map +1 -1
  82. package/dist/properties.js +310 -43
  83. package/dist/properties.js.map +1 -1
  84. package/dist/register-class.d.ts +147 -12
  85. package/dist/register-class.d.ts.map +1 -1
  86. package/dist/register-class.js +129 -83
  87. package/dist/register-class.js.map +1 -1
  88. package/dist/registry.d.ts +80 -21
  89. package/dist/registry.d.ts.map +1 -1
  90. package/dist/registry.js +86 -33
  91. package/dist/registry.js.map +1 -1
  92. package/dist/signal.d.ts +11 -4
  93. package/dist/signal.d.ts.map +1 -1
  94. package/dist/signal.js +85 -25
  95. package/dist/signal.js.map +1 -1
  96. package/dist/t.d.ts +2 -0
  97. package/dist/t.d.ts.map +1 -1
  98. package/dist/t.js +1 -0
  99. package/dist/t.js.map +1 -1
  100. package/dist/tuple.d.ts +6 -3
  101. package/dist/tuple.d.ts.map +1 -1
  102. package/dist/tuple.js +9 -7
  103. package/dist/tuple.js.map +1 -1
  104. package/dist/type.d.ts +2 -1
  105. package/dist/type.d.ts.map +1 -1
  106. package/dist/type.js +2 -0
  107. package/dist/type.js.map +1 -1
  108. package/dist/value.d.ts +10 -1
  109. package/dist/value.d.ts.map +1 -1
  110. package/dist/value.js +99 -24
  111. package/dist/value.js.map +1 -1
  112. package/dist/vfunc-call.d.ts +37 -0
  113. package/dist/vfunc-call.d.ts.map +1 -0
  114. package/dist/vfunc-call.js +172 -0
  115. package/dist/vfunc-call.js.map +1 -0
  116. package/dist/vfunc-seeds.d.ts +12 -0
  117. package/dist/vfunc-seeds.d.ts.map +1 -0
  118. package/dist/vfunc-seeds.js +19 -0
  119. package/dist/vfunc-seeds.js.map +1 -0
  120. package/dist/vfunc.d.ts +8 -0
  121. package/dist/vfunc.d.ts.map +1 -0
  122. package/dist/vfunc.js +29 -0
  123. package/dist/vfunc.js.map +1 -0
  124. package/package.json +14 -4
  125. package/src/application-class.ts +126 -0
  126. package/src/arg.ts +2 -1
  127. package/src/bind.ts +11 -0
  128. package/src/callback.ts +178 -34
  129. package/src/closure.ts +136 -0
  130. package/src/default-application.ts +21 -0
  131. package/src/descriptors.ts +150 -34
  132. package/src/error.ts +1 -0
  133. package/src/exit-hook.ts +18 -0
  134. package/src/fn.ts +107 -36
  135. package/src/folded-lengths.ts +64 -0
  136. package/src/index.ts +21 -6
  137. package/src/internal.ts +2 -0
  138. package/src/library.ts +3 -1
  139. package/src/lifecycle.ts +138 -25
  140. package/src/listeners.ts +25 -6
  141. package/src/mixin.ts +15 -2
  142. package/src/native-value.ts +5 -3
  143. package/src/object.ts +134 -35
  144. package/src/param-spec.ts +234 -0
  145. package/src/promisify.ts +4 -2
  146. package/src/properties.ts +473 -46
  147. package/src/register-class.ts +386 -114
  148. package/src/registry.ts +169 -49
  149. package/src/signal.ts +116 -42
  150. package/src/t.ts +3 -0
  151. package/src/tuple.ts +18 -12
  152. package/src/type.ts +3 -1
  153. package/src/value.ts +143 -23
  154. package/src/vfunc-call.ts +247 -0
  155. package/src/vfunc-seeds.ts +36 -0
  156. package/src/vfunc.ts +38 -0
package/src/properties.ts CHANGED
@@ -1,102 +1,525 @@
1
1
  import type { ExternalObject, Handle, RegisterClassProperty } from "@gtkx/native";
2
2
  import type { AnyClass } from "@gtkx/utils";
3
- import { camelCase } from "@gtkx/utils";
3
+ import { camelCase, kebabCase } from "@gtkx/utils";
4
4
  import { bind } from "./bind.js";
5
+ import { biguint64T, stringT, structT, voidT } from "./descriptors.js";
5
6
  import { LIB, PARAM_T, VALUE_T } from "./library.js";
6
- import { getHandle } from "./registry.js";
7
- import { fromValue, intoValue } from "./value.js";
7
+ import {
8
+ getParamFlags,
9
+ getParamValueType,
10
+ isParamConstructOnly,
11
+ isParamLaxlyValidated,
12
+ isParamWritable,
13
+ type ValueGuard,
14
+ valueGuardFor,
15
+ wasParamValueModified,
16
+ } from "./param-spec.js";
17
+ import { getHandle, getInterfaceProperties, instanceClassName, type InterfaceProperty } from "./registry.js";
18
+ import { typeName } from "./type.js";
19
+ import {
20
+ fromValue,
21
+ newValueForType,
22
+ type ValueNarrower,
23
+ valueNarrowerFor,
24
+ type ValueWriter,
25
+ valueWriterFor,
26
+ } from "./value.js";
8
27
 
9
- type PropertyAccessor = {
28
+ type PropertyCheck = {
10
29
  name: string;
30
+ propertyName: string;
31
+ handle: ExternalObject<Handle>;
32
+ flags: number;
33
+ valueType: bigint;
34
+ canHoldValue: ValueGuard;
35
+ narrowValue: ValueNarrower;
36
+ write?: ValueWriter;
37
+ scratch?: ExternalObject<Handle>;
38
+ };
39
+
40
+ type PropertyAccessor = PropertyCheck & {
41
+ memberName: string;
11
42
  storage: symbol;
12
- pspec: object;
43
+ hasGeneratedMember: boolean;
44
+ isInterfaceProperty?: boolean;
45
+ delegate?: InterfaceProperty;
13
46
  };
14
47
 
15
48
  /** A `GObject.ParamSpec` wrapper describing one property's name, type, flags and default. */
16
49
  type PropertySpec = object;
50
+
51
+ type PropertyDispatch = {
52
+ accessors: PropertyAccessor[];
53
+ delegates: Map<string, InterfaceProperty>;
54
+ };
55
+
56
+ type PropertyDispatchSource = {
57
+ klass: AnyClass;
58
+ properties: Record<string, PropertySpec>;
59
+ adoptedTypes: bigint[];
60
+ };
61
+
62
+ type ConstructProperty = {
63
+ name: string;
64
+ value: ExternalObject<Handle>;
65
+ };
66
+
17
67
  type NotifyingObject = { notify?: (propertyName: string) => void };
18
68
 
19
69
  const FIRST_PROPERTY_ID = 1;
70
+ const GET_PROPERTY_VFUNC = "vfuncGetProperty";
71
+ const SET_PROPERTY_VFUNC = "vfuncSetProperty";
72
+ const READ_ONLY_REASON = "the property is read-only";
73
+ const CONSTRUCT_ONLY_REASON = "the property can only be set when the object is constructed";
74
+ const CLASS_T = structT("borrowed");
75
+ const interfaceStorages: Map<string, symbol> = new Map();
20
76
  const paramSpecDefaultValue = bind(LIB, "g_param_spec_get_default_value", [PARAM_T], VALUE_T);
77
+ const paramSpecName = bind(LIB, "g_param_spec_get_name", [PARAM_T], stringT("borrowed"));
78
+ const typeClassRef = bind(LIB, "g_type_class_ref", [biguint64T], CLASS_T);
79
+ const typeClassUnref = bind(LIB, "g_type_class_unref", [CLASS_T], voidT);
80
+ const classFindProperty = bind(LIB, "g_object_class_find_property", [CLASS_T, stringT("borrowed")], PARAM_T);
21
81
 
82
+ const getPropertyName = (pspec: PropertySpec): string => paramSpecName(getHandle(pspec)) as string;
22
83
  const underscoreCase = (name: string): string => name.replaceAll("-", "_");
84
+ const canonicalCase = (name: string): string => kebabCase(name).replaceAll("_", "-");
23
85
  const accessorNames = (name: string): string[] => [...new Set([name, underscoreCase(name), camelCase(name)])];
86
+ const typeLabel = (type: bigint): string => typeName(type) ?? String(type);
87
+
88
+ const defaultValueFor = (handle: ExternalObject<Handle>): ExternalObject<Handle> =>
89
+ paramSpecDefaultValue(handle) as ExternalObject<Handle>;
90
+
91
+ const heldValue = (value: unknown): unknown => value ?? null;
92
+
93
+ const holdsReason = (check: PropertyCheck): string =>
94
+ `the property holds values of type '${typeLabel(check.valueType)}'`;
95
+
96
+ function checkFor(handle: ExternalObject<Handle>, name: string): PropertyCheck {
97
+ const valueType = getParamValueType(handle);
98
+
99
+ return {
100
+ name,
101
+ propertyName: paramSpecName(handle) as string,
102
+ handle,
103
+ flags: getParamFlags(handle),
104
+ valueType,
105
+ canHoldValue: valueGuardFor(valueType),
106
+ narrowValue: valueNarrowerFor(valueType),
107
+ };
108
+ }
109
+
110
+ function findPropertySpec(klass: ExternalObject<Handle>, name: string): ExternalObject<Handle> | null {
111
+ const found = classFindProperty(klass, name) as ExternalObject<Handle> | null;
112
+
113
+ return found ?? (classFindProperty(klass, canonicalCase(name)) as ExternalObject<Handle> | null);
114
+ }
115
+
116
+ function describeValue(value: unknown): string {
117
+ if (typeof value === "string") {
118
+ return JSON.stringify(value);
119
+ }
120
+
121
+ if (typeof value === "object" && value !== null) {
122
+ return instanceClassName(value);
123
+ }
124
+
125
+ return String(value);
126
+ }
127
+
128
+ function propertyMessage(instance: object, check: PropertyCheck, tail: string): string {
129
+ return `${instanceClassName(instance)}.${check.name}: ${tail}`;
130
+ }
131
+
132
+ function refusalTail(check: PropertyCheck, value: unknown, reason: string): string {
133
+ return `cannot set property '${check.propertyName}' to ${describeValue(value)}; ${reason}`;
134
+ }
135
+
136
+ function serveTail(check: PropertyCheck, value: unknown): string {
137
+ return `cannot serve property '${check.propertyName}' from ${describeValue(value)}; ${holdsReason(check)}`;
138
+ }
139
+
140
+ function assertValueFits(instance: object, check: PropertyCheck, value: unknown): void {
141
+ if (check.canHoldValue(value)) {
142
+ return;
143
+ }
144
+
145
+ const tail = refusalTail(check, value, holdsReason(check));
146
+ throw new TypeError(propertyMessage(instance, check, tail));
147
+ }
148
+
149
+ function assertValueServes(instance: object, check: PropertyCheck, value: unknown): void {
150
+ if (check.canHoldValue(value)) {
151
+ return;
152
+ }
153
+
154
+ throw new TypeError(propertyMessage(instance, check, serveTail(check, value)));
155
+ }
156
+
157
+ function assertValueValidates(
158
+ instance: object,
159
+ check: PropertyCheck,
160
+ value: unknown,
161
+ gValue: ExternalObject<Handle>,
162
+ ): void {
163
+ if (!wasParamValueModified(check.handle, gValue) || isParamLaxlyValidated(check.flags)) {
164
+ return;
165
+ }
166
+
167
+ const reason =
168
+ `the value is invalid or out of range for type '${typeLabel(check.valueType)}', ` +
169
+ `and GObject would put ${describeValue(fromValue(gValue))} in its place`;
170
+
171
+ throw new RangeError(propertyMessage(instance, check, refusalTail(check, value, reason)));
172
+ }
173
+
174
+ function assertWritable(instance: object, check: PropertyCheck, value: unknown): void {
175
+ if (isParamWritable(check.flags)) {
176
+ return;
177
+ }
178
+
179
+ throw new TypeError(propertyMessage(instance, check, refusalTail(check, value, READ_ONLY_REASON)));
180
+ }
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
+
187
+ function fillCheckedValue(
188
+ instance: object,
189
+ check: PropertyCheck,
190
+ gValue: ExternalObject<Handle>,
191
+ value: unknown,
192
+ ): void {
193
+ assertValueFits(instance, check, value);
194
+ writeHeld(check, gValue, value);
195
+ assertValueValidates(instance, check, value, gValue);
196
+ }
197
+
198
+ function checkedValueFor(instance: object, check: PropertyCheck, value: unknown): ExternalObject<Handle> {
199
+ const gValue = newValueForType(check.valueType);
200
+ fillCheckedValue(instance, check, gValue, value);
201
+
202
+ return gValue;
203
+ }
204
+
205
+ function assertValueAccepted(instance: object, check: PropertyCheck, value: unknown): void {
206
+ check.scratch ??= newValueForType(check.valueType);
207
+ fillCheckedValue(instance, check, check.scratch, value);
208
+ }
209
+
210
+ function constructValueFor(wrapper: object, check: PropertyCheck, value: unknown): ConstructProperty {
211
+ assertWritable(wrapper, check, value);
212
+
213
+ return { name: check.propertyName, value: checkedValueFor(wrapper, check, value) };
214
+ }
215
+
216
+ function constructPropertyFor(
217
+ gtype: bigint,
218
+ name: string,
219
+ value: unknown,
220
+ wrapper: object,
221
+ ): ConstructProperty | undefined {
222
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
223
+
224
+ try {
225
+ const pspec = findPropertySpec(klass, name);
226
+
227
+ return pspec === null ? undefined : constructValueFor(wrapper, checkFor(pspec, name), value);
228
+ } finally {
229
+ typeClassUnref(klass);
230
+ }
231
+ }
24
232
 
25
233
  function readStored(instance: Record<symbol, unknown>, accessor: PropertyAccessor): unknown {
26
234
  if (!Object.hasOwn(instance, accessor.storage)) {
27
- const defaults = paramSpecDefaultValue(getHandle(accessor.pspec)) as ExternalObject<Handle>;
28
- instance[accessor.storage] = fromValue(defaults);
235
+ instance[accessor.storage] = fromValue(defaultValueFor(accessor.handle));
29
236
  }
30
237
 
31
238
  return instance[accessor.storage];
32
239
  }
33
240
 
34
- function defineAccessor(prototype: object, accessor: PropertyAccessor, alias: string): void {
35
- if (Object.getOwnPropertyDescriptor(prototype, alias) !== undefined) {
241
+ function storeValue(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
242
+ instance[accessor.storage] = heldValue(value);
243
+ }
244
+
245
+ function writeStored(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
246
+ if (readStored(instance, accessor) === heldValue(value)) {
36
247
  return;
37
248
  }
38
249
 
39
- Object.defineProperty(prototype, alias, {
40
- configurable: true,
41
- enumerable: true,
42
- get(this: Record<symbol, unknown>) {
43
- return readStored(this, accessor);
44
- },
45
- set(this: Record<symbol, unknown>, value: unknown) {
46
- if (readStored(this, accessor) === value) {
47
- return;
48
- }
250
+ storeValue(instance, accessor, value);
251
+ (instance as NotifyingObject).notify?.(accessor.propertyName);
252
+ }
253
+
254
+ function writeProperty(instance: object, accessor: PropertyAccessor, value: unknown): void {
255
+ const stored = instance as Record<symbol, unknown>;
256
+
257
+ if (readStored(stored, accessor) === heldValue(value)) {
258
+ return;
259
+ }
260
+
261
+ assertValueAccepted(instance, accessor, value);
262
+ writeStored(stored, accessor, accessor.narrowValue(value));
263
+ }
264
+
265
+ function storedGetter(accessor: PropertyAccessor): (this: object) => unknown {
266
+ return function get(this: object) {
267
+ return readStored(this as Record<symbol, unknown>, accessor);
268
+ };
269
+ }
49
270
 
50
- this[accessor.storage] = value;
51
- (this as NotifyingObject).notify?.(accessor.name);
52
- },
53
- });
271
+ function memberGetter(accessor: PropertyAccessor): (this: object) => unknown {
272
+ return function get(this: object) {
273
+ return (this as Record<string, unknown>)[accessor.memberName];
274
+ };
275
+ }
276
+
277
+ function memberSetter(accessor: PropertyAccessor): (this: object, value: unknown) => void {
278
+ return function set(this: object, value: unknown) {
279
+ (this as Record<string, unknown>)[accessor.memberName] = value;
280
+ };
281
+ }
282
+
283
+ function refusalFor(accessor: PropertyAccessor, reason: string): (this: object, value: unknown) => void {
284
+ return function set(this: object, value: unknown) {
285
+ throw new TypeError(propertyMessage(this, accessor, refusalTail(accessor, value, reason)));
286
+ };
287
+ }
288
+
289
+ function checkedSetter(accessor: PropertyAccessor): (this: object, value: unknown) => void {
290
+ if (!isParamWritable(accessor.flags)) {
291
+ return refusalFor(accessor, READ_ONLY_REASON);
292
+ }
293
+
294
+ if (isParamConstructOnly(accessor.flags)) {
295
+ return refusalFor(accessor, CONSTRUCT_ONLY_REASON);
296
+ }
297
+
298
+ return function set(this: object, value: unknown) {
299
+ writeProperty(this, accessor, value);
300
+ };
301
+ }
302
+
303
+ function hasOwnMember(prototype: object, alias: string): boolean {
304
+ return Object.getOwnPropertyDescriptor(prototype, alias) !== undefined;
305
+ }
306
+
307
+ function defineAccessor(prototype: object, descriptor: PropertyDescriptor, alias: string): void {
308
+ if (hasOwnMember(prototype, alias)) {
309
+ return;
310
+ }
311
+
312
+ Object.defineProperty(prototype, alias, descriptor);
54
313
  }
55
314
 
56
315
  function installAccessors(klass: AnyClass, accessor: PropertyAccessor): void {
57
316
  const prototype = (klass as { prototype: object }).prototype;
317
+ accessor.hasGeneratedMember = !hasOwnMember(prototype, accessor.memberName);
318
+
319
+ const descriptor: PropertyDescriptor = {
320
+ configurable: true,
321
+ enumerable: true,
322
+ get: accessor.hasGeneratedMember ? storedGetter(accessor) : memberGetter(accessor),
323
+ set: accessor.hasGeneratedMember ? checkedSetter(accessor) : memberSetter(accessor),
324
+ };
58
325
 
59
326
  for (const alias of accessorNames(accessor.name)) {
60
- defineAccessor(prototype, accessor, alias);
327
+ defineAccessor(prototype, descriptor, alias);
61
328
  }
62
329
  }
63
330
 
64
- function resolveAccessor(accessors: PropertyAccessor[], propertyId: number): PropertyAccessor {
65
- const accessor = accessors[propertyId - FIRST_PROPERTY_ID];
331
+ function storageFor(propertyName: string): symbol {
332
+ const existing = interfaceStorages.get(propertyName);
66
333
 
67
- if (accessor === undefined) {
68
- throw new RangeError(`registerClass: no property registered for id ${String(propertyId)}`);
334
+ if (existing !== undefined) {
335
+ return existing;
336
+ }
337
+
338
+ const created = Symbol(`gtkx:property:${propertyName}`);
339
+ interfaceStorages.set(propertyName, created);
340
+
341
+ return created;
342
+ }
343
+
344
+ function interfaceAccessorFor(dispatch: PropertyDispatch, pspec: PropertySpec): PropertyAccessor {
345
+ const handle = getHandle(pspec);
346
+ const propertyName = paramSpecName(handle) as string;
347
+ const delegate = dispatch.delegates.get(propertyName);
348
+
349
+ const accessor: PropertyAccessor = {
350
+ ...checkFor(handle, propertyName),
351
+ memberName: camelCase(propertyName),
352
+ storage: storageFor(propertyName),
353
+ hasGeneratedMember: false,
354
+ isInterfaceProperty: true,
355
+ };
356
+
357
+ if (delegate !== undefined) {
358
+ accessor.delegate = delegate;
69
359
  }
70
360
 
71
361
  return accessor;
72
362
  }
73
363
 
74
- function makeGetProperty(accessors: PropertyAccessor[]) {
75
- return function getProperty(this: object, propertyId: number, value: object): void {
76
- const accessor = resolveAccessor(accessors, propertyId);
77
- const current = (this as Record<string, unknown>)[camelCase(accessor.name)];
78
- intoValue(getHandle(value), current);
364
+ function resolveAccessor(
365
+ dispatch: PropertyDispatch,
366
+ propertyId: number,
367
+ pspec: PropertySpec,
368
+ ): PropertyAccessor {
369
+ const accessor = dispatch.accessors[propertyId - FIRST_PROPERTY_ID];
370
+
371
+ if (accessor !== undefined) {
372
+ return accessor;
373
+ }
374
+
375
+ const created = interfaceAccessorFor(dispatch, pspec);
376
+ dispatch.accessors[propertyId - FIRST_PROPERTY_ID] = created;
377
+
378
+ return created;
379
+ }
380
+
381
+ function callMember(instance: object, member: string, args: unknown[]): unknown {
382
+ const fn: unknown = (instance as Record<string, unknown>)[member];
383
+
384
+ if (typeof fn !== "function") {
385
+ throw new TypeError(`registerClass: ${instanceClassName(instance)} has no '${member}' member`);
386
+ }
387
+
388
+ return (fn as (...values: unknown[]) => unknown).apply(instance, args);
389
+ }
390
+
391
+ function backingMemberFor(instance: object, accessor: PropertyAccessor): string | undefined {
392
+ if (accessor.hasGeneratedMember && !Object.hasOwn(instance, accessor.memberName)) {
393
+ return undefined;
394
+ }
395
+
396
+ return accessor.memberName;
397
+ }
398
+
399
+ function readCurrent(instance: object, accessor: PropertyAccessor): unknown {
400
+ const getter = accessor.delegate?.getter;
401
+
402
+ if (getter !== undefined) {
403
+ return callMember(instance, getter, []);
404
+ }
405
+
406
+ if (accessor.isInterfaceProperty === true) {
407
+ return readStored(instance as Record<symbol, unknown>, accessor);
408
+ }
409
+
410
+ const member = backingMemberFor(instance, accessor);
411
+
412
+ return member === undefined
413
+ ? readStored(instance as Record<symbol, unknown>, accessor)
414
+ : (instance as Record<string, unknown>)[member];
415
+ }
416
+
417
+ function writeCurrent(instance: object, accessor: PropertyAccessor, value: unknown): void {
418
+ const setter = accessor.delegate?.setter;
419
+
420
+ if (setter !== undefined) {
421
+ callMember(instance, setter, [value]);
422
+
423
+ return;
424
+ }
425
+
426
+ if (accessor.isInterfaceProperty === true) {
427
+ writeStored(instance as Record<symbol, unknown>, accessor, value);
428
+
429
+ return;
430
+ }
431
+
432
+ const member = backingMemberFor(instance, accessor);
433
+
434
+ if (member === undefined) {
435
+ storeValue(instance as Record<symbol, unknown>, accessor, value);
436
+
437
+ return;
438
+ }
439
+
440
+ (instance as Record<string, unknown>)[member] = value;
441
+ }
442
+
443
+ function makeGetProperty(dispatch: PropertyDispatch) {
444
+ return function getProperty(this: object, propertyId: number, value: object, pspec: PropertySpec): void {
445
+ const accessor = resolveAccessor(dispatch, propertyId, pspec);
446
+ const current = readCurrent(this, accessor);
447
+ assertValueServes(this, accessor, current);
448
+ writeHeld(accessor, getHandle(value), current);
79
449
  };
80
450
  }
81
451
 
82
- function makeSetProperty(accessors: PropertyAccessor[]) {
83
- return function setProperty(this: object, propertyId: number, value: object): void {
84
- const accessor = resolveAccessor(accessors, propertyId);
85
- const jsValue = fromValue(getHandle(value));
86
- (this as Record<string, unknown>)[camelCase(accessor.name)] = jsValue;
452
+ function makeSetProperty(dispatch: PropertyDispatch) {
453
+ return function setProperty(this: object, propertyId: number, value: object, pspec: PropertySpec): void {
454
+ const accessor = resolveAccessor(dispatch, propertyId, pspec);
455
+ writeCurrent(this, accessor, fromValue(getHandle(value)));
87
456
  };
88
457
  }
89
458
 
90
- function buildAccessors(klass: AnyClass, properties: Record<string, PropertySpec>): PropertyAccessor[] {
91
- const accessors: PropertyAccessor[] = [];
459
+ function assertCanonicalName(klass: AnyClass, name: string, propertyName: string): void {
460
+ const canonical = canonicalCase(name);
461
+
462
+ if (propertyName === canonical) {
463
+ return;
464
+ }
92
465
 
466
+ throw new TypeError(
467
+ `registerClass: ${klass.name} keys the property '${name}' to a GObject.ParamSpec named ` +
468
+ `'${propertyName}', which is the name GObject notifies under; name the ParamSpec '${canonical}'`,
469
+ );
470
+ }
471
+
472
+ function assertCanonicalNames(klass: AnyClass, properties: Record<string, PropertySpec>): void {
93
473
  for (const [name, pspec] of Object.entries(properties)) {
94
- const accessor: PropertyAccessor = { name, storage: Symbol(`gtkx:property:${name}`), pspec };
95
- installAccessors(klass, accessor);
96
- accessors.push(accessor);
474
+ assertCanonicalName(klass, name, getPropertyName(pspec));
475
+ }
476
+ }
477
+
478
+ function buildAccessor(klass: AnyClass, name: string, pspec: PropertySpec): PropertyAccessor {
479
+ const accessor: PropertyAccessor = {
480
+ ...checkFor(getHandle(pspec), name),
481
+ memberName: camelCase(name),
482
+ storage: Symbol(`gtkx:property:${name}`),
483
+ hasGeneratedMember: false,
484
+ };
485
+
486
+ installAccessors(klass, accessor);
487
+
488
+ return accessor;
489
+ }
490
+
491
+ function buildAccessors(source: PropertyDispatchSource): PropertyAccessor[] {
492
+ const { klass, properties } = source;
493
+ assertCanonicalNames(klass, properties);
494
+
495
+ return Object.entries(properties).map(([name, pspec]) => buildAccessor(klass, name, pspec));
496
+ }
497
+
498
+ function addInterfaceDelegates(delegates: Map<string, InterfaceProperty>, gtype: bigint): void {
499
+ const properties = getInterfaceProperties(gtype) ?? {};
500
+
501
+ for (const [name, delegate] of Object.entries(properties)) {
502
+ if (!delegates.has(name)) {
503
+ delegates.set(name, delegate);
504
+ }
97
505
  }
506
+ }
507
+
508
+ function interfaceDelegatesFor(adoptedTypes: bigint[]): Map<string, InterfaceProperty> {
509
+ const delegates: Map<string, InterfaceProperty> = new Map();
98
510
 
99
- return accessors;
511
+ for (const gtype of adoptedTypes) {
512
+ addInterfaceDelegates(delegates, gtype);
513
+ }
514
+
515
+ return delegates;
516
+ }
517
+
518
+ function buildPropertyDispatch(source: PropertyDispatchSource): PropertyDispatch {
519
+ return {
520
+ accessors: buildAccessors(source),
521
+ delegates: interfaceDelegatesFor(source.adoptedTypes),
522
+ };
100
523
  }
101
524
 
102
525
  function toNativeProperties(properties: Record<string, PropertySpec>): RegisterClassProperty[] {
@@ -107,10 +530,14 @@ function toNativeProperties(properties: Record<string, PropertySpec>): RegisterC
107
530
  }
108
531
 
109
532
  export {
110
- buildAccessors,
533
+ buildPropertyDispatch,
534
+ constructPropertyFor,
535
+ GET_PROPERTY_VFUNC,
111
536
  makeGetProperty,
112
537
  makeSetProperty,
538
+ SET_PROPERTY_VFUNC,
113
539
  toNativeProperties,
114
- type PropertyAccessor,
540
+ type ConstructProperty,
541
+ type PropertyDispatch,
115
542
  type PropertySpec,
116
543
  };