@hla4ts/spacekit 0.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.
@@ -0,0 +1,514 @@
1
+ import type {
2
+ AttributeSchema,
3
+ InteractionClassSchema,
4
+ ObjectClassSchema,
5
+ OrderType,
6
+ OwnershipType,
7
+ ParameterSchema,
8
+ SharingType,
9
+ UpdateType,
10
+ } from "@hla4ts/fom-codegen";
11
+ import type { DeclarativeAttribute, DeclarativeInteractionClass, DeclarativeObjectClass, DeclarativeParameter } from "./declarative.ts";
12
+ import { DeclarativeFom } from "./declarative.ts";
13
+
14
+ export type FomClassConstructor<T = object> = new (...args: never[]) => T;
15
+
16
+ export interface FomCoder<T> {
17
+ encode(value: T): Uint8Array;
18
+ decode?(data: Uint8Array): T;
19
+ }
20
+
21
+ export interface FomObjectClassOptions {
22
+ name?: string;
23
+ parent?: string;
24
+ sharing?: SharingType;
25
+ semantics?: string;
26
+ }
27
+
28
+ export interface FomAttributeOptions<T> {
29
+ name?: string;
30
+ dataType: string;
31
+ updateType: UpdateType;
32
+ updateCondition: string;
33
+ ownership: OwnershipType;
34
+ sharing: SharingType;
35
+ transportation: string;
36
+ order: OrderType;
37
+ semantics?: string;
38
+ valueRequired?: boolean;
39
+ encode?: (value: T) => Uint8Array;
40
+ decode?: (data: Uint8Array) => T;
41
+ coder?: FomCoder<T>;
42
+ }
43
+
44
+ export interface FomInteractionClassOptions {
45
+ name?: string;
46
+ parent?: string;
47
+ sharing?: SharingType;
48
+ transportation?: string;
49
+ order?: OrderType;
50
+ semantics?: string;
51
+ }
52
+
53
+ export interface FomParameterOptions<T> {
54
+ name?: string;
55
+ dataType: string;
56
+ semantics?: string;
57
+ encode?: (value: T) => Uint8Array;
58
+ decode?: (data: Uint8Array) => T;
59
+ coder?: FomCoder<T>;
60
+ }
61
+
62
+ interface ObjectClassMetadata {
63
+ options?: FomObjectClassOptions;
64
+ attributes: Map<string, FomAttributeOptions<unknown>>;
65
+ }
66
+
67
+ interface InteractionClassMetadata {
68
+ options?: FomInteractionClassOptions;
69
+ parameters: Map<string, FomParameterOptions<unknown>>;
70
+ }
71
+
72
+ interface MetadataEntry<T> {
73
+ ctor: Function;
74
+ meta: T;
75
+ }
76
+
77
+ const objectClassMetadata = new Map<Function, ObjectClassMetadata>();
78
+ const interactionClassMetadata = new Map<Function, InteractionClassMetadata>();
79
+
80
+ export function FomObjectClass(options: FomObjectClassOptions = {}): ClassDecorator {
81
+ return (target) => {
82
+ const meta = getOrCreateObjectMetadata(target);
83
+ meta.options = options;
84
+ objectClassMetadata.set(target, meta);
85
+ };
86
+ }
87
+
88
+ export function FomAttribute<T>(options: FomAttributeOptions<T>): PropertyDecorator {
89
+ return (target, propertyKey) => {
90
+ const ctor = target.constructor as Function;
91
+ const meta = getOrCreateObjectMetadata(ctor);
92
+ meta.attributes.set(String(propertyKey), options as FomAttributeOptions<unknown>);
93
+ objectClassMetadata.set(ctor, meta);
94
+ };
95
+ }
96
+
97
+ export function FomInteractionClass(
98
+ options: FomInteractionClassOptions = {}
99
+ ): ClassDecorator {
100
+ return (target) => {
101
+ const meta = getOrCreateInteractionMetadata(target);
102
+ meta.options = options;
103
+ interactionClassMetadata.set(target, meta);
104
+ };
105
+ }
106
+
107
+ export function FomParameter<T>(options: FomParameterOptions<T>): PropertyDecorator {
108
+ return (target, propertyKey) => {
109
+ const ctor = target.constructor as Function;
110
+ const meta = getOrCreateInteractionMetadata(ctor);
111
+ meta.parameters.set(String(propertyKey), options as FomParameterOptions<unknown>);
112
+ interactionClassMetadata.set(ctor, meta);
113
+ };
114
+ }
115
+
116
+ export function getDecoratedObjectClassMetadata(
117
+ ctor: Function
118
+ ): ObjectClassSchema | undefined {
119
+ const chain = getObjectMetadataChain(ctor);
120
+ if (!chain.length) return undefined;
121
+ const meta = chain[chain.length - 1].meta;
122
+ if (!meta) return undefined;
123
+ const { name, parent } = resolveObjectNames(ctor, chain, meta.options);
124
+ return {
125
+ name,
126
+ parent,
127
+ sharing: meta.options?.sharing,
128
+ semantics: meta.options?.semantics,
129
+ attributes: Array.from(mergeObjectAttributes(chain).entries()).map(
130
+ ([key, attribute]) => toAttributeSchema(key, attribute)
131
+ ),
132
+ };
133
+ }
134
+
135
+ export function getDecoratedInteractionClassMetadata(
136
+ ctor: Function
137
+ ): InteractionClassSchema | undefined {
138
+ const chain = getInteractionMetadataChain(ctor);
139
+ if (!chain.length) return undefined;
140
+ const meta = chain[chain.length - 1].meta;
141
+ if (!meta) return undefined;
142
+ const { name, parent } = resolveInteractionNames(ctor, chain, meta.options);
143
+ return {
144
+ name,
145
+ parent,
146
+ sharing: meta.options?.sharing,
147
+ transportation: meta.options?.transportation,
148
+ order: meta.options?.order,
149
+ semantics: meta.options?.semantics,
150
+ parameters: Array.from(mergeInteractionParameters(chain).entries()).map(
151
+ ([key, parameter]) => toParameterSchema(key, parameter)
152
+ ),
153
+ };
154
+ }
155
+
156
+ export function registerDecoratedClasses(
157
+ declarative: DeclarativeFom,
158
+ options: {
159
+ objectClasses?: FomClassConstructor[];
160
+ interactionClasses?: FomClassConstructor[];
161
+ register?: boolean;
162
+ }
163
+ ): void {
164
+ for (const ctor of options.objectClasses ?? []) {
165
+ const schema = getDecoratedObjectSchema(ctor);
166
+ const shouldRegister = options.register ?? true;
167
+ declarative.defineObjectClass(schema, { register: false });
168
+ if (shouldRegister) {
169
+ declarative.registry.addObjectClass(getDecoratedObjectSchemaForFom(ctor));
170
+ }
171
+ }
172
+ for (const ctor of options.interactionClasses ?? []) {
173
+ const schema = getDecoratedInteractionSchema(ctor);
174
+ const shouldRegister = options.register ?? true;
175
+ declarative.defineInteractionClass(schema, { register: false });
176
+ if (shouldRegister) {
177
+ declarative.registry.addInteractionClass(getDecoratedInteractionSchemaForFom(ctor));
178
+ }
179
+ }
180
+ }
181
+
182
+ export function buildDeclarativeFomFromDecorators(options: {
183
+ objectClasses?: FomClassConstructor[];
184
+ interactionClasses?: FomClassConstructor[];
185
+ register?: boolean;
186
+ }): DeclarativeFom {
187
+ const declarative = new DeclarativeFom();
188
+ registerDecoratedClasses(declarative, options);
189
+ return declarative;
190
+ }
191
+
192
+ export function getDecoratedObjectClassName(ctor: Function): string | undefined {
193
+ const meta = getDecoratedObjectClassMetadata(ctor);
194
+ if (!meta) return undefined;
195
+ return resolveDecoratedName(meta.name, meta.parent);
196
+ }
197
+
198
+ export function getDecoratedInteractionClassName(ctor: Function): string | undefined {
199
+ const meta = getDecoratedInteractionClassMetadata(ctor);
200
+ if (!meta) return undefined;
201
+ return resolveDecoratedName(meta.name, meta.parent);
202
+ }
203
+
204
+ function getDecoratedObjectSchema<T extends object>(
205
+ ctor: FomClassConstructor<T>
206
+ ): DeclarativeObjectClass<T> {
207
+ const chain = getObjectMetadataChain(ctor);
208
+ if (!chain.length) {
209
+ throw new Error(`Missing @FomObjectClass metadata for ${ctor.name}.`);
210
+ }
211
+ const meta = chain[chain.length - 1].meta;
212
+ const { name, parent } = resolveObjectNames(ctor, chain, meta.options);
213
+ const attributes = toDeclarativeAttributes(mergeObjectAttributes(chain));
214
+ return {
215
+ name,
216
+ parent,
217
+ sharing: meta.options?.sharing,
218
+ semantics: meta.options?.semantics,
219
+ attributes: attributes as DeclarativeObjectClass<T>["attributes"],
220
+ };
221
+ }
222
+
223
+ function getDecoratedObjectSchemaForFom(
224
+ ctor: FomClassConstructor
225
+ ): ObjectClassSchema {
226
+ const chain = getObjectMetadataChain(ctor);
227
+ if (!chain.length) {
228
+ throw new Error(`Missing @FomObjectClass metadata for ${ctor.name}.`);
229
+ }
230
+ const meta = chain[chain.length - 1].meta;
231
+ const { name, parent } = resolveObjectNames(ctor, chain, meta.options);
232
+ const attributes = toDeclarativeAttributes(meta.attributes);
233
+ return {
234
+ name,
235
+ parent,
236
+ sharing: meta.options?.sharing,
237
+ semantics: meta.options?.semantics,
238
+ attributes: Object.values(attributes).map((attribute) => ({
239
+ name: attribute.name,
240
+ dataType: attribute.dataType,
241
+ updateType: attribute.updateType,
242
+ updateCondition: attribute.updateCondition,
243
+ ownership: attribute.ownership,
244
+ sharing: attribute.sharing,
245
+ transportation: attribute.transportation,
246
+ order: attribute.order,
247
+ semantics: attribute.semantics,
248
+ valueRequired: attribute.valueRequired,
249
+ })),
250
+ };
251
+ }
252
+
253
+ function getDecoratedInteractionSchema<T extends object>(
254
+ ctor: FomClassConstructor<T>
255
+ ): DeclarativeInteractionClass<T> {
256
+ const chain = getInteractionMetadataChain(ctor);
257
+ if (!chain.length) {
258
+ throw new Error(`Missing @FomInteractionClass metadata for ${ctor.name}.`);
259
+ }
260
+ const meta = chain[chain.length - 1].meta;
261
+ const { name, parent } = resolveInteractionNames(ctor, chain, meta.options);
262
+ const parameters = toDeclarativeParameters(mergeInteractionParameters(chain));
263
+ return {
264
+ name,
265
+ parent,
266
+ sharing: meta.options?.sharing,
267
+ transportation: meta.options?.transportation,
268
+ order: meta.options?.order,
269
+ semantics: meta.options?.semantics,
270
+ parameters: parameters as DeclarativeInteractionClass<T>["parameters"],
271
+ };
272
+ }
273
+
274
+ function getDecoratedInteractionSchemaForFom(
275
+ ctor: FomClassConstructor
276
+ ): InteractionClassSchema {
277
+ const chain = getInteractionMetadataChain(ctor);
278
+ if (!chain.length) {
279
+ throw new Error(`Missing @FomInteractionClass metadata for ${ctor.name}.`);
280
+ }
281
+ const meta = chain[chain.length - 1].meta;
282
+ const { name, parent } = resolveInteractionNames(ctor, chain, meta.options);
283
+ const parameters = toDeclarativeParameters(meta.parameters);
284
+ return {
285
+ name,
286
+ parent,
287
+ sharing: meta.options?.sharing,
288
+ transportation: meta.options?.transportation,
289
+ order: meta.options?.order,
290
+ semantics: meta.options?.semantics,
291
+ parameters: Object.values(parameters).map((parameter) => ({
292
+ name: parameter.name,
293
+ dataType: parameter.dataType,
294
+ semantics: parameter.semantics,
295
+ })),
296
+ };
297
+ }
298
+
299
+ function toDeclarativeAttributes(
300
+ attributes: Map<string, FomAttributeOptions<unknown>>
301
+ ): Record<string, DeclarativeAttribute<unknown>> {
302
+ const result: Record<string, DeclarativeAttribute<unknown>> = {};
303
+ for (const [key, attribute] of attributes.entries()) {
304
+ const coder = resolveCoder(attribute, "attribute", key);
305
+ result[key] = {
306
+ name: attribute.name ?? key,
307
+ dataType: attribute.dataType,
308
+ updateType: attribute.updateType,
309
+ updateCondition: attribute.updateCondition,
310
+ ownership: attribute.ownership,
311
+ sharing: attribute.sharing,
312
+ transportation: attribute.transportation,
313
+ order: attribute.order,
314
+ semantics: attribute.semantics,
315
+ valueRequired: attribute.valueRequired,
316
+ encode: coder.encode,
317
+ decode: coder.decode,
318
+ };
319
+ }
320
+ return result;
321
+ }
322
+
323
+ function toDeclarativeParameters(
324
+ parameters: Map<string, FomParameterOptions<unknown>>
325
+ ): Record<string, DeclarativeParameter<unknown>> {
326
+ const result: Record<string, DeclarativeParameter<unknown>> = {};
327
+ for (const [key, parameter] of parameters.entries()) {
328
+ const coder = resolveCoder(parameter, "parameter", key);
329
+ result[key] = {
330
+ name: parameter.name ?? key,
331
+ dataType: parameter.dataType,
332
+ semantics: parameter.semantics,
333
+ encode: coder.encode,
334
+ decode: coder.decode,
335
+ };
336
+ }
337
+ return result;
338
+ }
339
+
340
+ function toAttributeSchema(key: string, attribute: FomAttributeOptions<unknown>): AttributeSchema {
341
+ return {
342
+ name: attribute.name ?? key,
343
+ dataType: attribute.dataType,
344
+ updateType: attribute.updateType,
345
+ updateCondition: attribute.updateCondition,
346
+ ownership: attribute.ownership,
347
+ sharing: attribute.sharing,
348
+ transportation: attribute.transportation,
349
+ order: attribute.order,
350
+ semantics: attribute.semantics,
351
+ valueRequired: attribute.valueRequired,
352
+ };
353
+ }
354
+
355
+ function toParameterSchema(key: string, parameter: FomParameterOptions<unknown>): ParameterSchema {
356
+ return {
357
+ name: parameter.name ?? key,
358
+ dataType: parameter.dataType,
359
+ semantics: parameter.semantics,
360
+ };
361
+ }
362
+
363
+ function getOrCreateObjectMetadata(target: Function): ObjectClassMetadata {
364
+ const existing = objectClassMetadata.get(target);
365
+ if (existing) return existing;
366
+ const created: ObjectClassMetadata = { attributes: new Map() };
367
+ objectClassMetadata.set(target, created);
368
+ return created;
369
+ }
370
+
371
+ function getOrCreateInteractionMetadata(target: Function): InteractionClassMetadata {
372
+ const existing = interactionClassMetadata.get(target);
373
+ if (existing) return existing;
374
+ const created: InteractionClassMetadata = { parameters: new Map() };
375
+ interactionClassMetadata.set(target, created);
376
+ return created;
377
+ }
378
+
379
+ function getObjectMetadataChain(target: Function): Array<MetadataEntry<ObjectClassMetadata>> {
380
+ const chain: Array<MetadataEntry<ObjectClassMetadata>> = [];
381
+ let current: Function | null = target;
382
+ while (current && current !== Function && current !== Object) {
383
+ const meta = objectClassMetadata.get(current);
384
+ if (meta) chain.unshift({ ctor: current, meta });
385
+ const proto = Object.getPrototypeOf(current.prototype);
386
+ current = proto?.constructor ?? null;
387
+ }
388
+ return chain;
389
+ }
390
+
391
+ function getInteractionMetadataChain(
392
+ target: Function
393
+ ): Array<MetadataEntry<InteractionClassMetadata>> {
394
+ const chain: Array<MetadataEntry<InteractionClassMetadata>> = [];
395
+ let current: Function | null = target;
396
+ while (current && current !== Function && current !== Object) {
397
+ const meta = interactionClassMetadata.get(current);
398
+ if (meta) chain.unshift({ ctor: current, meta });
399
+ const proto = Object.getPrototypeOf(current.prototype);
400
+ current = proto?.constructor ?? null;
401
+ }
402
+ return chain;
403
+ }
404
+
405
+ function mergeObjectAttributes(
406
+ chain: Array<MetadataEntry<ObjectClassMetadata>>
407
+ ): Map<string, FomAttributeOptions<unknown>> {
408
+ const merged = new Map<string, FomAttributeOptions<unknown>>();
409
+ for (const entry of chain) {
410
+ for (const [key, attribute] of entry.meta.attributes.entries()) {
411
+ merged.set(key, attribute);
412
+ }
413
+ }
414
+ return merged;
415
+ }
416
+
417
+ function mergeInteractionParameters(
418
+ chain: Array<MetadataEntry<InteractionClassMetadata>>
419
+ ): Map<string, FomParameterOptions<unknown>> {
420
+ const merged = new Map<string, FomParameterOptions<unknown>>();
421
+ for (const entry of chain) {
422
+ for (const [key, parameter] of entry.meta.parameters.entries()) {
423
+ merged.set(key, parameter);
424
+ }
425
+ }
426
+ return merged;
427
+ }
428
+
429
+ function getParentClassName(
430
+ chain: Array<MetadataEntry<ObjectClassMetadata>>
431
+ ): string | undefined {
432
+ if (chain.length < 2) return undefined;
433
+ const parentMeta = chain[chain.length - 2];
434
+ return (
435
+ (parentMeta.ctor as { className?: string }).className ??
436
+ getDecoratedObjectClassName(parentMeta.ctor) ??
437
+ parentMeta.meta.options?.name ??
438
+ parentMeta.ctor.name
439
+ );
440
+ }
441
+
442
+ function getParentInteractionName(
443
+ chain: Array<MetadataEntry<InteractionClassMetadata>>
444
+ ): string | undefined {
445
+ if (chain.length < 2) return undefined;
446
+ const parentMeta = chain[chain.length - 2];
447
+ return (
448
+ (parentMeta.ctor as { className?: string }).className ??
449
+ getDecoratedInteractionClassName(parentMeta.ctor) ??
450
+ parentMeta.meta.options?.name ??
451
+ parentMeta.ctor.name
452
+ );
453
+ }
454
+
455
+ function resolveDecoratedName(name?: string, parent?: string): string | undefined {
456
+ if (!name) return undefined;
457
+ if (name.includes(".")) return name;
458
+ if (!parent) return name;
459
+ return `${parent}.${name}`;
460
+ }
461
+
462
+ function resolveObjectNames(
463
+ ctor: Function,
464
+ chain: Array<MetadataEntry<ObjectClassMetadata>>,
465
+ options?: FomObjectClassOptions
466
+ ): { name: string; parent?: string } {
467
+ const name = options?.name ?? ctor.name;
468
+ const explicitParent = options?.parent;
469
+ if (explicitParent) {
470
+ return { name, parent: explicitParent };
471
+ }
472
+ if (name.includes(".")) {
473
+ return { name, parent: inferParentName(name) };
474
+ }
475
+ const parent = getParentClassName(chain) ?? "HLAobjectRoot";
476
+ return { name, parent };
477
+ }
478
+
479
+ function resolveInteractionNames(
480
+ ctor: Function,
481
+ chain: Array<MetadataEntry<InteractionClassMetadata>>,
482
+ options?: FomInteractionClassOptions
483
+ ): { name: string; parent?: string } {
484
+ const name = options?.name ?? ctor.name;
485
+ const explicitParent = options?.parent;
486
+ if (explicitParent) {
487
+ return { name, parent: explicitParent };
488
+ }
489
+ if (name.includes(".")) {
490
+ return { name, parent: inferParentName(name) };
491
+ }
492
+ const parent = getParentInteractionName(chain) ?? "HLAinteractionRoot";
493
+ return { name, parent };
494
+ }
495
+
496
+ function resolveCoder<T>(
497
+ options: { coder?: FomCoder<T>; encode?: (value: T) => Uint8Array; decode?: (data: Uint8Array) => T },
498
+ kind: "attribute" | "parameter",
499
+ key: string
500
+ ): FomCoder<T> {
501
+ if (options.coder) return options.coder;
502
+ if (!options.encode) {
503
+ throw new Error(`Missing encoder for ${kind} ${key}.`);
504
+ }
505
+ return {
506
+ encode: options.encode,
507
+ decode: options.decode,
508
+ };
509
+ }
510
+
511
+ function inferParentName(name: string): string | undefined {
512
+ if (!name.includes(".")) return undefined;
513
+ return name.slice(0, name.lastIndexOf("."));
514
+ }
package/src/entity.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { LogicalTime } from "@hla4ts/hla-api";
2
+ import type { SpacekitApp } from "./app.ts";
3
+ import type { ObjectClassAdapter, ObjectInstanceAdapter } from "./object-model.ts";
4
+
5
+ const ENTITY_BINDING = Symbol.for("spacekit.entityBinding");
6
+
7
+ export interface SpacekitEntityBinding<T extends object> {
8
+ app: SpacekitApp;
9
+ adapter: ObjectClassAdapter<T>;
10
+ instance: ObjectInstanceAdapter<T>;
11
+ }
12
+
13
+ export abstract class SpacekitEntity<T extends object> {
14
+ private _lastAttributes?: Record<string, unknown>;
15
+ private readonly _dirtyKeys = new Set<string>();
16
+ private _dirtyAll = true;
17
+
18
+ abstract toAttributes(): T;
19
+
20
+ markDirty(...keys: Array<keyof T & string>): void {
21
+ if (keys.length === 0) {
22
+ this._dirtyAll = true;
23
+ return;
24
+ }
25
+ for (const key of keys) {
26
+ this._dirtyKeys.add(String(key) as keyof T & string);
27
+ }
28
+ }
29
+
30
+ protected _consumeChangesInternal(): Partial<T> {
31
+ const current = this.toAttributes();
32
+ const currentRecord = current as Record<string, unknown>;
33
+ if (this._dirtyAll || !this._lastAttributes) {
34
+ this._dirtyAll = false;
35
+ this._dirtyKeys.clear();
36
+ this._lastAttributes = currentRecord;
37
+ return current;
38
+ }
39
+
40
+ const changes: Partial<T> = {};
41
+ for (const key of Object.keys(current) as Array<keyof T & string>) {
42
+ if (this._dirtyKeys.has(key)) {
43
+ changes[key] = current[key];
44
+ continue;
45
+ }
46
+ if (!Object.is(current[key], this._lastAttributes[key])) {
47
+ changes[key] = current[key];
48
+ }
49
+ }
50
+
51
+ this._dirtyKeys.clear();
52
+ this._lastAttributes = currentRecord;
53
+ return changes;
54
+ }
55
+
56
+ async sync(time?: LogicalTime, userSuppliedTag?: Uint8Array): Promise<void> {
57
+ const binding = requireEntityBinding(this);
58
+ await binding.app.syncEntity(this, time, userSuppliedTag);
59
+ }
60
+
61
+ async delete(time?: LogicalTime, userSuppliedTag?: Uint8Array): Promise<void> {
62
+ const binding = requireEntityBinding(this);
63
+ await binding.app.deleteObject(binding.instance, userSuppliedTag ?? new Uint8Array(), time);
64
+ }
65
+
66
+ get instanceName(): string | undefined {
67
+ return getEntityBinding(this)?.instance.objectInstanceName;
68
+ }
69
+
70
+ get className(): string | undefined {
71
+ return getEntityBinding(this)?.adapter.className;
72
+ }
73
+ }
74
+
75
+ export function bindEntity<T extends object>(
76
+ entity: SpacekitEntity<T>,
77
+ binding: SpacekitEntityBinding<T>
78
+ ): void {
79
+ (entity as unknown as Record<symbol, SpacekitEntityBinding<T>>)[ENTITY_BINDING] = binding;
80
+ }
81
+
82
+ export function getEntityBinding<T extends object>(
83
+ entity: SpacekitEntity<T>
84
+ ): SpacekitEntityBinding<T> | undefined {
85
+ return (entity as unknown as Record<symbol, SpacekitEntityBinding<T> | undefined>)[
86
+ ENTITY_BINDING
87
+ ];
88
+ }
89
+
90
+ export function requireEntityBinding<T extends object>(
91
+ entity: SpacekitEntity<T>
92
+ ): SpacekitEntityBinding<T> {
93
+ const binding = getEntityBinding(entity);
94
+ if (!binding) {
95
+ throw new Error("Entity is not registered with a SpacekitApp.");
96
+ }
97
+ return binding;
98
+ }
99
+
100
+ export function consumeEntityChanges<T extends object>(entity: SpacekitEntity<T>): Partial<T> {
101
+ return (entity as unknown as { _consumeChangesInternal: () => Partial<T> })
102
+ ._consumeChangesInternal();
103
+ }