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