@flighthq/entity 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.
- package/dist/binding.d.ts +4 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +13 -0
- package/dist/binding.js.map +1 -0
- package/dist/clone.d.ts +4 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +20 -0
- package/dist/clone.js.map +1 -0
- package/dist/entity.d.ts +3 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +9 -0
- package/dist/entity.js.map +1 -0
- package/dist/guards.d.ts +6 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +57 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +16 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +37 -0
- package/src/binding.test.ts +42 -0
- package/src/clone.test.ts +82 -0
- package/src/entity.test.ts +15 -0
- package/src/guards.test.ts +64 -0
- package/src/runtime.test.ts +41 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAK9C,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D"}
|
package/dist/binding.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
import { createEntityRuntime, getEntityRuntime } from './runtime';
|
|
3
|
+
export function attachEntityBinding(entity, binding) {
|
|
4
|
+
if (entity[EntityRuntimeKey] === undefined) {
|
|
5
|
+
entity[EntityRuntimeKey] = createEntityRuntime();
|
|
6
|
+
}
|
|
7
|
+
entity[EntityRuntimeKey].binding = binding;
|
|
8
|
+
}
|
|
9
|
+
export function getEntityBinding(source) {
|
|
10
|
+
const runtime = getEntityRuntime(source);
|
|
11
|
+
return runtime?.binding ?? null;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElE,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,OAAe;IACjE,IAAI,MAAM,CAAC,gBAAgB,CAAC,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,CAAC,gBAAgB,CAAC,GAAG,mBAAmB,EAAE,CAAC;IACnD,CAAC;IACD,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;AAClC,CAAC"}
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Entity, EntityWithoutRuntime } from '@flighthq/types';
|
|
2
|
+
export declare function cloneEntity<Type extends Entity>(source: Readonly<Type>): Type;
|
|
3
|
+
export declare function stripEntityRuntime<Type extends Entity>(source: Readonly<Type>): EntityWithoutRuntime<Type>;
|
|
4
|
+
//# sourceMappingURL=clone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAQpE,wBAAgB,WAAW,CAAC,IAAI,SAAS,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAI7E;AAMD,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAI1G"}
|
package/dist/clone.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
import { createEntity } from './entity';
|
|
3
|
+
// Allocates a new entity by copying the public data fields of the source, with the runtime
|
|
4
|
+
// slot reset to undefined. The clone is a fresh, unbound entity — runtime and binding are
|
|
5
|
+
// intentionally not carried over.
|
|
6
|
+
export function cloneEntity(source) {
|
|
7
|
+
const copy = { ...source };
|
|
8
|
+
copy[EntityRuntimeKey] = undefined;
|
|
9
|
+
return createEntity(copy);
|
|
10
|
+
}
|
|
11
|
+
// Returns a plain object with the EntityRuntimeKey slot removed, ready to pass to a
|
|
12
|
+
// serializer or JSON.stringify. The runtime is explicitly non-serializable; this helper
|
|
13
|
+
// provides the one canonical strip path so each consuming package does not need to know
|
|
14
|
+
// about the slot key.
|
|
15
|
+
export function stripEntityRuntime(source) {
|
|
16
|
+
const copy = { ...source };
|
|
17
|
+
delete copy[EntityRuntimeKey];
|
|
18
|
+
return copy;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=clone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.js","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,2FAA2F;AAC3F,0FAA0F;AAC1F,kCAAkC;AAClC,MAAM,UAAU,WAAW,CAAsB,MAAsB;IACrE,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,EAAkC,CAAC;IAC3D,IAAI,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAAC;IACnC,OAAO,YAAY,CAAC,IAAuB,CAAC,CAAC;AAC/C,CAAC;AAED,oFAAoF;AACpF,wFAAwF;AACxF,wFAAwF;AACxF,sBAAsB;AACtB,MAAM,UAAU,kBAAkB,CAAsB,MAAsB;IAC5E,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,EAAkC,CAAC;IAC3D,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC9B,OAAO,IAA6C,CAAC;AACvD,CAAC"}
|
package/dist/entity.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAG9C,wBAAgB,YAAY,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAK3E"}
|
package/dist/entity.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,UAAU,YAAY,CAAsB,GAAU;IAC1D,IAAI,CAAC,GAAG;QAAE,GAAG,GAAG,EAAU,CAAC;IAC3B,MAAM,MAAM,GAAG,GAAoB,CAAC;IACpC,MAAM,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAAC;IACrC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Entity, EntityRuntime } from '@flighthq/types';
|
|
2
|
+
export declare function areEntityRuntimeGuardsEnabled(): boolean;
|
|
3
|
+
export declare function createGuardedEntity<Type extends object>(entity: Type & Entity): Type & Entity;
|
|
4
|
+
export declare function createGuardedEntityRuntime(runtime: EntityRuntime): EntityRuntime;
|
|
5
|
+
export declare function enableEntityRuntimeGuards(): void;
|
|
6
|
+
//# sourceMappingURL=guards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAI7D,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAmB7F;AAKD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAehF;AASD,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD"}
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
// Returns true if entity runtime guards have been enabled via enableEntityRuntimeGuards.
|
|
3
|
+
export function areEntityRuntimeGuardsEnabled() {
|
|
4
|
+
return _guardsEnabled;
|
|
5
|
+
}
|
|
6
|
+
// Creates a guarded entity that warns when the runtime slot is written directly rather
|
|
7
|
+
// than through ensureEntityRuntime or attachEntityBinding. Only active when guards are
|
|
8
|
+
// enabled via enableEntityRuntimeGuards.
|
|
9
|
+
export function createGuardedEntity(entity) {
|
|
10
|
+
if (!_guardsEnabled || typeof Proxy === 'undefined')
|
|
11
|
+
return entity;
|
|
12
|
+
return new Proxy(entity, {
|
|
13
|
+
set(target, prop, value) {
|
|
14
|
+
if (prop === EntityRuntimeKey && _guardsEnabled) {
|
|
15
|
+
// Allow writes from trusted paths by checking the stack. Since we cannot reliably
|
|
16
|
+
// inspect the stack in all environments, we emit a warning and allow the write.
|
|
17
|
+
// In practice, ensureEntityRuntime and attachEntityBinding are the only callers that
|
|
18
|
+
// should write to this slot.
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.warn('[entity] Direct write to EntityRuntimeKey detected. Use ensureEntityRuntime or attachEntityBinding instead.', entity);
|
|
21
|
+
}
|
|
22
|
+
target[prop] = value;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// Returns a guarded proxy over an existing EntityRuntime that warns when the binding slot
|
|
28
|
+
// is written outside of attachEntityBinding / detachEntityBinding. Only active when guards
|
|
29
|
+
// are enabled.
|
|
30
|
+
export function createGuardedEntityRuntime(runtime) {
|
|
31
|
+
if (!_guardsEnabled || typeof Proxy === 'undefined')
|
|
32
|
+
return runtime;
|
|
33
|
+
return new Proxy(runtime, {
|
|
34
|
+
set(target, prop, value) {
|
|
35
|
+
if (prop === 'binding' && _guardsEnabled) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.warn('[entity] Direct write to EntityRuntime.binding detected. Use attachEntityBinding or detachEntityBinding instead.', runtime);
|
|
38
|
+
}
|
|
39
|
+
target[prop] = value;
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Opt-in development guard mode. When enabled, direct writes to the EntityRuntimeKey slot
|
|
45
|
+
// that bypass ensureEntityRuntime / attachEntityBinding will warn in the console, making
|
|
46
|
+
// "writes landed on the wrong entity or raw slot poke" bugs visible early.
|
|
47
|
+
//
|
|
48
|
+
// This function is a no-op in production if the calling module is never imported — it is
|
|
49
|
+
// fully tree-shakable, never called at module top level, and has no effect on
|
|
50
|
+
// "sideEffects": false.
|
|
51
|
+
export function enableEntityRuntimeGuards() {
|
|
52
|
+
if (typeof Proxy === 'undefined')
|
|
53
|
+
return;
|
|
54
|
+
_guardsEnabled = true;
|
|
55
|
+
}
|
|
56
|
+
let _guardsEnabled = false;
|
|
57
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,yFAAyF;AACzF,MAAM,UAAU,6BAA6B;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,uFAAuF;AACvF,uFAAuF;AACvF,yCAAyC;AACzC,MAAM,UAAU,mBAAmB,CAAsB,MAAqB;IAC5E,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC;IACnE,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACvB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,gBAAgB,IAAI,cAAc,EAAE,CAAC;gBAChD,kFAAkF;gBAClF,gFAAgF;gBAChF,qFAAqF;gBACrF,6BAA6B;gBAC7B,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACV,6GAA6G,EAC7G,MAAM,CACP,CAAC;YACJ,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,2FAA2F;AAC3F,eAAe;AACf,MAAM,UAAU,0BAA0B,CAAC,OAAsB;IAC/D,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,OAAO,CAAC;IACpE,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,SAAS,IAAI,cAAc,EAAE,CAAC;gBACzC,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACV,kHAAkH,EAClH,OAAO,CACR,CAAC;YACJ,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,yFAAyF;AACzF,2EAA2E;AAC3E,EAAE;AACF,yFAAyF;AACzF,8EAA8E;AAC9E,wBAAwB;AACxB,MAAM,UAAU,yBAAyB;IACvC,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO;IACzC,cAAc,GAAG,IAAI,CAAC;AACxB,CAAC;AAED,IAAI,cAAc,GAAG,KAAK,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Entity, EntityRuntime } from '@flighthq/types';
|
|
2
|
+
export declare function createEntityRuntime(): EntityRuntime;
|
|
3
|
+
export declare function getEntityRuntime(source: Readonly<Entity>): Readonly<EntityRuntime>;
|
|
4
|
+
export declare function hasEntityRuntime(source: Readonly<Entity>): boolean;
|
|
5
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAG7D,wBAAgB,mBAAmB,IAAI,aAAa,CAInD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,CAElF;AAKD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAElE"}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
export function createEntityRuntime() {
|
|
3
|
+
return {
|
|
4
|
+
binding: null,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function getEntityRuntime(source) {
|
|
8
|
+
return source[EntityRuntimeKey];
|
|
9
|
+
}
|
|
10
|
+
// Returns true when the entity's runtime slot has been allocated — i.e. the entity has been
|
|
11
|
+
// bound (attachEntityBinding) or otherwise given a runtime. A fresh or cloned entity has no
|
|
12
|
+
// runtime and returns false.
|
|
13
|
+
export function hasEntityRuntime(source) {
|
|
14
|
+
return source[EntityRuntimeKey] !== undefined;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACvD,OAAO,MAAM,CAAC,gBAAgB,CAAE,CAAC;AACnC,CAAC;AAED,4FAA4F;AAC5F,4FAA4F;AAC5F,6BAA6B;AAC7B,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACvD,OAAO,MAAM,CAAC,gBAAgB,CAAC,KAAK,SAAS,CAAC;AAChD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/entity",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "Core entity/runtime data model and binding system",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { attachEntityBinding, getEntityBinding } from './binding';
|
|
4
|
+
import { createEntity } from './entity';
|
|
5
|
+
import { createEntityRuntime, getEntityRuntime } from './runtime';
|
|
6
|
+
|
|
7
|
+
describe('attachEntityBinding', () => {
|
|
8
|
+
it('creates a runtime object if none is present', () => {
|
|
9
|
+
const entity = createEntity();
|
|
10
|
+
expect(getEntityRuntime(entity)).toBeUndefined();
|
|
11
|
+
attachEntityBinding(entity, {});
|
|
12
|
+
expect(getEntityRuntime(entity)).not.toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('assigns to the binding slot', () => {
|
|
16
|
+
const entity = createEntity();
|
|
17
|
+
const binding = {};
|
|
18
|
+
attachEntityBinding(entity, binding);
|
|
19
|
+
expect(getEntityRuntime(entity).binding).toStrictEqual(binding);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('getEntityBinding', () => {
|
|
24
|
+
it('returns null if the entity has no runtime', () => {
|
|
25
|
+
const entity = createEntity();
|
|
26
|
+
expect(getEntityBinding(entity)).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns null if the binding slot is empty', () => {
|
|
30
|
+
const entity = createEntity();
|
|
31
|
+
entity[EntityRuntimeKey] = createEntityRuntime();
|
|
32
|
+
expect(getEntityBinding(entity)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns the binding slot if set', () => {
|
|
36
|
+
const entity = createEntity();
|
|
37
|
+
const runtime = createEntityRuntime();
|
|
38
|
+
runtime.binding = {};
|
|
39
|
+
entity[EntityRuntimeKey] = runtime;
|
|
40
|
+
expect(getEntityBinding(entity)).toStrictEqual(runtime.binding);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { attachEntityBinding } from './binding';
|
|
4
|
+
import { cloneEntity, stripEntityRuntime } from './clone';
|
|
5
|
+
import { createEntity } from './entity';
|
|
6
|
+
import { hasEntityRuntime } from './runtime';
|
|
7
|
+
|
|
8
|
+
describe('cloneEntity', () => {
|
|
9
|
+
it('returns a new entity with the same public data fields', () => {
|
|
10
|
+
const source = createEntity({ x: 1, y: 2 });
|
|
11
|
+
const clone = cloneEntity(source);
|
|
12
|
+
expect(clone.x).toBe(1);
|
|
13
|
+
expect(clone.y).toBe(2);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resets the runtime slot to undefined on the clone', () => {
|
|
17
|
+
const source = createEntity();
|
|
18
|
+
attachEntityBinding(source, {});
|
|
19
|
+
expect(hasEntityRuntime(source)).toBe(true);
|
|
20
|
+
const clone = cloneEntity(source);
|
|
21
|
+
expect(hasEntityRuntime(clone)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not share the runtime between source and clone', () => {
|
|
25
|
+
const source = createEntity();
|
|
26
|
+
const clone = cloneEntity(source);
|
|
27
|
+
expect(clone[EntityRuntimeKey]).toBeUndefined();
|
|
28
|
+
expect(clone).not.toBe(source);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns a new object reference', () => {
|
|
32
|
+
const source = createEntity();
|
|
33
|
+
const clone = cloneEntity(source);
|
|
34
|
+
expect(clone).not.toBe(source);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('clone of a bound entity yields an unbound clone', () => {
|
|
38
|
+
const source = createEntity({ name: 'test' });
|
|
39
|
+
attachEntityBinding(source, { hostRef: 42 });
|
|
40
|
+
const clone = cloneEntity(source);
|
|
41
|
+
expect(hasEntityRuntime(clone)).toBe(false);
|
|
42
|
+
expect(clone.name).toBe('test');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('stripEntityRuntime', () => {
|
|
47
|
+
it('removes the EntityRuntimeKey slot', () => {
|
|
48
|
+
const entity = createEntity({ x: 10 });
|
|
49
|
+
const stripped = stripEntityRuntime(entity);
|
|
50
|
+
expect((stripped as Record<PropertyKey, unknown>)[EntityRuntimeKey]).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('retains the public data fields', () => {
|
|
54
|
+
const entity = createEntity({ x: 10, y: 20 });
|
|
55
|
+
const stripped = stripEntityRuntime(entity);
|
|
56
|
+
expect(stripped.x).toBe(10);
|
|
57
|
+
expect(stripped.y).toBe(20);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not mutate the source entity', () => {
|
|
61
|
+
const entity = createEntity({ x: 10 });
|
|
62
|
+
stripEntityRuntime(entity);
|
|
63
|
+
expect(entity[EntityRuntimeKey]).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('strips bound entity — binding is not in the output', () => {
|
|
67
|
+
const entity = createEntity({ label: 'hello' });
|
|
68
|
+
attachEntityBinding(entity, { nativeRef: true });
|
|
69
|
+
const stripped = stripEntityRuntime(entity);
|
|
70
|
+
expect((stripped as Record<PropertyKey, unknown>)[EntityRuntimeKey]).toBeUndefined();
|
|
71
|
+
expect(stripped.label).toBe('hello');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('round-trips: stripEntityRuntime then createEntity yields a valid unbound entity', () => {
|
|
75
|
+
const original = createEntity({ value: 99 });
|
|
76
|
+
attachEntityBinding(original, {});
|
|
77
|
+
const stripped = stripEntityRuntime(original);
|
|
78
|
+
const restored = createEntity(stripped);
|
|
79
|
+
expect(restored.value).toBe(99);
|
|
80
|
+
expect(hasEntityRuntime(restored)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createEntity } from './entity';
|
|
4
|
+
|
|
5
|
+
describe('createEntity', () => {
|
|
6
|
+
it('returns an object', () => {
|
|
7
|
+
const entity = createEntity();
|
|
8
|
+
expect(entity).not.toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('has an undefined runtime', () => {
|
|
12
|
+
const entity = createEntity();
|
|
13
|
+
expect(entity[EntityRuntimeKey]).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createEntity } from './entity';
|
|
4
|
+
import {
|
|
5
|
+
areEntityRuntimeGuardsEnabled,
|
|
6
|
+
createGuardedEntity,
|
|
7
|
+
createGuardedEntityRuntime,
|
|
8
|
+
enableEntityRuntimeGuards,
|
|
9
|
+
} from './guards';
|
|
10
|
+
import { createEntityRuntime } from './runtime';
|
|
11
|
+
|
|
12
|
+
describe('areEntityRuntimeGuardsEnabled', () => {
|
|
13
|
+
it('returns false before enableEntityRuntimeGuards is called', () => {
|
|
14
|
+
// Vitest isolates modules per file, so this starts false in a fresh module import.
|
|
15
|
+
expect(areEntityRuntimeGuardsEnabled()).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('createGuardedEntity', () => {
|
|
20
|
+
it('returns the entity unchanged when guards are not enabled', () => {
|
|
21
|
+
const entity = createEntity({ x: 1 });
|
|
22
|
+
const guarded = createGuardedEntity(entity);
|
|
23
|
+
expect(guarded).toBe(entity);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('createGuardedEntityRuntime', () => {
|
|
28
|
+
it('returns the runtime unchanged when guards are not enabled', () => {
|
|
29
|
+
const runtime = createEntityRuntime();
|
|
30
|
+
const guarded = createGuardedEntityRuntime(runtime);
|
|
31
|
+
expect(guarded).toBe(runtime);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('enableEntityRuntimeGuards', () => {
|
|
36
|
+
it('enables guards', () => {
|
|
37
|
+
enableEntityRuntimeGuards();
|
|
38
|
+
expect(areEntityRuntimeGuardsEnabled()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('createGuardedEntity returns a proxy when guards are enabled', () => {
|
|
42
|
+
enableEntityRuntimeGuards();
|
|
43
|
+
const entity = createEntity({ x: 1 });
|
|
44
|
+
const guarded = createGuardedEntity(entity);
|
|
45
|
+
expect(guarded.x).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('createGuardedEntityRuntime returns a proxy when guards are enabled', () => {
|
|
49
|
+
enableEntityRuntimeGuards();
|
|
50
|
+
const runtime = createEntityRuntime();
|
|
51
|
+
const guarded = createGuardedEntityRuntime(runtime);
|
|
52
|
+
expect(guarded.binding).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('guarded entity warns on direct EntityRuntimeKey write', () => {
|
|
56
|
+
enableEntityRuntimeGuards();
|
|
57
|
+
const entity = createEntity();
|
|
58
|
+
const guarded = createGuardedEntity(entity);
|
|
59
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
60
|
+
guarded[EntityRuntimeKey] = undefined;
|
|
61
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[entity]'), expect.anything());
|
|
62
|
+
warnSpy.mockRestore();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EntityRuntime } from '@flighthq/types';
|
|
2
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { attachEntityBinding } from './binding';
|
|
5
|
+
import { createEntity } from './entity';
|
|
6
|
+
import { createEntityRuntime, getEntityRuntime, hasEntityRuntime } from './runtime';
|
|
7
|
+
|
|
8
|
+
describe('createEntityRuntime', () => {
|
|
9
|
+
it('returns an object', () => {
|
|
10
|
+
const runtime = createEntityRuntime();
|
|
11
|
+
expect(runtime).not.toBeNull();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('has a null api slot', () => {
|
|
15
|
+
const runtime = createEntityRuntime();
|
|
16
|
+
expect(runtime.binding).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getEntityRuntime', () => {
|
|
21
|
+
it('returns the runtime object', () => {
|
|
22
|
+
const entity = createEntity();
|
|
23
|
+
expect(getEntityRuntime(entity)).toBeUndefined();
|
|
24
|
+
const runtime = {} as EntityRuntime;
|
|
25
|
+
entity[EntityRuntimeKey] = runtime;
|
|
26
|
+
expect(getEntityRuntime(entity)).toStrictEqual(runtime);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('hasEntityRuntime', () => {
|
|
31
|
+
it('returns false for a fresh entity', () => {
|
|
32
|
+
const entity = createEntity();
|
|
33
|
+
expect(hasEntityRuntime(entity)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns true after a binding is attached', () => {
|
|
37
|
+
const entity = createEntity();
|
|
38
|
+
attachEntityBinding(entity, {});
|
|
39
|
+
expect(hasEntityRuntime(entity)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|