lecodes-sdk 1.2.0 → 2.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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// A TypeScript stand-in for `_creatorTree` (sdk/src/bridges/tree.d.ts) for the SDK's unit tests:
|
|
2
|
+
// an in-memory tree with the runtime's semantics the SDK relies on (ids, insert / remove, parent /
|
|
3
|
+
// children, the pager's pages, the vlist window, destinations + the router stack, widgets) and a
|
|
4
|
+
// call recorder. Events reach the SDK through the emitter it installs (`fake.emit(...)` fires one
|
|
5
|
+
// by hand; the fake itself fires open / close / routerChange / detached / vlistSync the way the
|
|
6
|
+
// runtime does, synchronously). Installed for every test file by tests/helpers/preload.ts.
|
|
7
|
+
import {
|
|
8
|
+
TREE_DEST_NONE, TREE_DEST_SCREEN, TREE_DEST_VIDEO,
|
|
9
|
+
TREE_EVENT_CLOSE, TREE_EVENT_DETACHED, TREE_EVENT_FREED, TREE_EVENT_OPEN, TREE_EVENT_ROUTER_CHANGE, TREE_EVENT_VLIST_SYNC,
|
|
10
|
+
} from "../../src/ui/tree"
|
|
11
|
+
import { _resetTransitions } from "../../src/ui/transitions"
|
|
12
|
+
|
|
13
|
+
export type FakeNode = {
|
|
14
|
+
id: number, type: string, parent: number, children: number[],
|
|
15
|
+
strings: Record<string, string | null>, ints: Record<string, number>, flags: number,
|
|
16
|
+
style: Record<string, unknown>, classes: Set<string>,
|
|
17
|
+
pages?: number[], lens?: number[], index?: number, pagerOwner?: number,
|
|
18
|
+
vlist?: { keys: string[], mounted: Map<string, number> },
|
|
19
|
+
}
|
|
20
|
+
export type Call = { method: string, args: any[] }
|
|
21
|
+
type Dest = { kind: number, id: number, viewName: string, paramsJson: string }
|
|
22
|
+
|
|
23
|
+
export class FakeTree {
|
|
24
|
+
nodes = new Map<number, FakeNode>()
|
|
25
|
+
calls: Call[] = []
|
|
26
|
+
emitter: ((id: number, kind: number, ...args: any[]) => any) | null = null
|
|
27
|
+
rects = new Map<number, [number, number, number, number]>()
|
|
28
|
+
displaySize: [number, number] = [0, 0]
|
|
29
|
+
current: Dest | null = null
|
|
30
|
+
stack: Dest[] = []
|
|
31
|
+
widgets: number[] = []
|
|
32
|
+
freed: number[] = []
|
|
33
|
+
tweens: { id: number, data: Float32Array, strings: string[], targets: number[] }[] = []
|
|
34
|
+
tweenEvent: ((id: number, kind: number, index: number, ...lanes: number[]) => void) | null = null
|
|
35
|
+
/** What `defineTransition` registered; `transitions[id - 1]` is the transition of a wire id. */
|
|
36
|
+
transitions: { id: number, data: Float32Array, strings: string[], roles: number[], flags: number }[] = []
|
|
37
|
+
fontFormat?: "woff2" | "ttf"
|
|
38
|
+
private nextId = 1
|
|
39
|
+
private nextTween = 1
|
|
40
|
+
|
|
41
|
+
// ---- helpers for tests ---------------------------------------------------------------------------
|
|
42
|
+
node(id: number): FakeNode { const n = this.nodes.get(id); if (!n) throw new Error(`fakeTree: no node ${id}`); return n }
|
|
43
|
+
names(): string[] { return this.calls.map((c) => c.method) }
|
|
44
|
+
find(method: string): Call | undefined { return this.calls.find((c) => c.method === method) }
|
|
45
|
+
filter(method: string): Call[] { return this.calls.filter((c) => c.method === method) }
|
|
46
|
+
reset(): void { this.calls = [] }
|
|
47
|
+
/** Every node id this fake ever handed out (alive or not). */
|
|
48
|
+
allIds(): number[] { return Array.from({ length: this.nextId - 1 }, (_, i) => i + 1) }
|
|
49
|
+
/** Fire an event at the SDK the way the runtime would. */
|
|
50
|
+
emit(id: number, kind: number, ...args: any[]): any { return this.emitter?.(id, kind, ...args) }
|
|
51
|
+
private rec(method: string, ...args: any[]): void { this.calls.push({ method, args }) }
|
|
52
|
+
private destId(d: Dest): number { return d.id }
|
|
53
|
+
private fireDest(d: Dest | null, kind: number): void {
|
|
54
|
+
if (!d) return
|
|
55
|
+
const node = d.kind === TREE_DEST_SCREEN || d.kind === TREE_DEST_VIDEO ? d.id : 0
|
|
56
|
+
this.emitter?.(node, kind, d.kind, this.destId(d))
|
|
57
|
+
}
|
|
58
|
+
private sameDest(a: Dest | null, b: Dest | null): boolean {
|
|
59
|
+
return !!a && !!b && a.kind === b.kind && a.id === b.id
|
|
60
|
+
}
|
|
61
|
+
private inStack(d: Dest): boolean { return this.stack.some((s) => this.sameDest(s, d)) }
|
|
62
|
+
|
|
63
|
+
// ---- nodes -----------------------------------------------------------------------------------------
|
|
64
|
+
create = (type: string): Handle => {
|
|
65
|
+
const id = this.nextId++
|
|
66
|
+
this.nodes.set(id, { id, type, parent: 0, children: [], strings: {}, ints: {}, flags: 0, style: {}, classes: new Set() })
|
|
67
|
+
this.rec("create", type, id)
|
|
68
|
+
return { id }
|
|
69
|
+
}
|
|
70
|
+
destroy = (id: number): void => {
|
|
71
|
+
this.rec("destroy", id)
|
|
72
|
+
const n = this.nodes.get(id)
|
|
73
|
+
if (!n) return
|
|
74
|
+
const freeRec = (m: FakeNode) => {
|
|
75
|
+
for (const c of m.children) { const cn = this.nodes.get(c); if (cn) freeRec(cn) }
|
|
76
|
+
this.nodes.delete(m.id)
|
|
77
|
+
if (m.id !== id) this.freed.push(m.id)
|
|
78
|
+
}
|
|
79
|
+
freeRec(n)
|
|
80
|
+
}
|
|
81
|
+
insert = (parent: number, child: number, index: number): void => {
|
|
82
|
+
this.rec("insert", parent, child, index)
|
|
83
|
+
const p = this.nodes.get(parent), c = this.nodes.get(child)
|
|
84
|
+
if (!p || !c) return
|
|
85
|
+
if (c.parent !== 0) return
|
|
86
|
+
c.parent = parent
|
|
87
|
+
const i = Math.max(0, Math.min(index, p.children.length))
|
|
88
|
+
p.children.splice(i, 0, child)
|
|
89
|
+
}
|
|
90
|
+
remove = (parent: number, child: number): void => {
|
|
91
|
+
this.rec("remove", parent, child)
|
|
92
|
+
const p = this.nodes.get(parent), c = this.nodes.get(child)
|
|
93
|
+
if (!p || !c || c.parent !== parent) return
|
|
94
|
+
c.parent = 0
|
|
95
|
+
p.children = p.children.filter((x) => x !== child)
|
|
96
|
+
}
|
|
97
|
+
parent = (id: number): number => {
|
|
98
|
+
const n = this.nodes.get(id)
|
|
99
|
+
return n ? (n.pagerOwner ?? n.parent) : 0
|
|
100
|
+
}
|
|
101
|
+
children = (id: number): number[] => {
|
|
102
|
+
const n = this.nodes.get(id)
|
|
103
|
+
if (!n) return []
|
|
104
|
+
return n.pages ? n.pages.slice() : n.children.slice()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- content / style -------------------------------------------------------------------------------
|
|
108
|
+
setString = (id: number, key: string, value: string | null): void => { this.rec("setString", id, key, value); const n = this.nodes.get(id); if (n) n.strings[key] = value }
|
|
109
|
+
setInt = (id: number, key: string, value: number): void => { this.rec("setInt", id, key, value); const n = this.nodes.get(id); if (n) n.ints[key] = value }
|
|
110
|
+
setFlags = (id: number, mask: number): void => { this.rec("setFlags", id, mask); const n = this.nodes.get(id); if (n) n.flags = mask }
|
|
111
|
+
getTextValue = (id: number): string => this.nodes.get(id)?.strings.value ?? ""
|
|
112
|
+
setStyle = (id: number, key: string, value: any): void => { this.rec("setStyle", id, key, value); const n = this.nodes.get(id); if (n) n.style[key] = value }
|
|
113
|
+
mergeStyle = (id: number, style: object): void => { this.rec("mergeStyle", id, style); const n = this.nodes.get(id); if (n) Object.assign(n.style, style) }
|
|
114
|
+
setClass = (id: number, name: string, enabled: boolean): void => {
|
|
115
|
+
this.rec("setClass", id, name, enabled)
|
|
116
|
+
const n = this.nodes.get(id); if (!n) return
|
|
117
|
+
if (enabled) n.classes.add(name); else n.classes.delete(name)
|
|
118
|
+
}
|
|
119
|
+
setTheme = (values: Record<string, any>): void => { this.rec("setTheme", values) }
|
|
120
|
+
|
|
121
|
+
// ---- presentation ----------------------------------------------------------------------------------
|
|
122
|
+
private present(d: Dest, transition: number): void {
|
|
123
|
+
if (this.sameDest(this.current, d)) return
|
|
124
|
+
const prev = this.current
|
|
125
|
+
this.fireDest(prev, TREE_EVENT_CLOSE)
|
|
126
|
+
this.current = d
|
|
127
|
+
this.fireDest(d, TREE_EVENT_OPEN)
|
|
128
|
+
if (prev && !this.inStack(prev)) this.retire(prev)
|
|
129
|
+
}
|
|
130
|
+
private retire(d: Dest): void {
|
|
131
|
+
if (d.kind === TREE_DEST_SCREEN && !this.inStack(d) && !this.sameDest(this.current, d)) this.emitter?.(d.id, TREE_EVENT_DETACHED)
|
|
132
|
+
}
|
|
133
|
+
defineTransition = (data: Float32Array, strings: string[], roles: number[], flags: number): number => {
|
|
134
|
+
const id = this.transitions.length + 1
|
|
135
|
+
this.transitions.push({ id, data, strings: strings.slice(), roles: roles.slice(), flags })
|
|
136
|
+
this.rec("defineTransition", id, roles.slice(), flags)
|
|
137
|
+
return id
|
|
138
|
+
}
|
|
139
|
+
open = (kind: number, id: number, viewName: string, paramsJson: string, transition: number): void => {
|
|
140
|
+
this.rec("open", kind, id, viewName, paramsJson, transition)
|
|
141
|
+
this.present({ kind, id, viewName, paramsJson }, transition)
|
|
142
|
+
}
|
|
143
|
+
// Like the runtime's closeViewOf: only the destination on screen closes.
|
|
144
|
+
close = (kind: number, id: number, transition: number): void => {
|
|
145
|
+
this.rec("close", kind, id, transition)
|
|
146
|
+
const prev = this.current
|
|
147
|
+
if (!prev || prev.kind !== kind || prev.id !== id) return
|
|
148
|
+
this.closeCurrent()
|
|
149
|
+
}
|
|
150
|
+
private closeCurrent(): void {
|
|
151
|
+
const prev = this.current
|
|
152
|
+
if (!prev) return
|
|
153
|
+
this.fireDest(prev, TREE_EVENT_CLOSE)
|
|
154
|
+
this.current = null
|
|
155
|
+
if (!this.inStack(prev)) this.retire(prev)
|
|
156
|
+
}
|
|
157
|
+
private change(d: Dest, transition: number): void {
|
|
158
|
+
this.present(d, transition)
|
|
159
|
+
this.emitter?.(0, TREE_EVENT_ROUTER_CHANGE, d.kind, this.destId(d))
|
|
160
|
+
}
|
|
161
|
+
routerOpen = (kind: number, id: number, viewName: string, paramsJson: string, showBack: boolean): void => {
|
|
162
|
+
this.rec("routerOpen", kind, id, viewName, paramsJson, showBack)
|
|
163
|
+
const old = this.stack
|
|
164
|
+
this.stack = []
|
|
165
|
+
for (const d of old) if (!this.sameDest(d, this.current)) this.retire(d)
|
|
166
|
+
const d = { kind, id, viewName, paramsJson }
|
|
167
|
+
this.stack.push(d)
|
|
168
|
+
this.present(d, 0)
|
|
169
|
+
}
|
|
170
|
+
routerPush = (kind: number, id: number, viewName: string, paramsJson: string, transition: number, back: number): void => {
|
|
171
|
+
this.rec("routerPush", kind, id, viewName, paramsJson, transition, back)
|
|
172
|
+
const d = { kind, id, viewName, paramsJson }
|
|
173
|
+
this.stack.push(d)
|
|
174
|
+
this.change(d, transition)
|
|
175
|
+
}
|
|
176
|
+
routerReplace = (kind: number, id: number, viewName: string, paramsJson: string, transition: number, back: number): void => {
|
|
177
|
+
this.rec("routerReplace", kind, id, viewName, paramsJson, transition, back)
|
|
178
|
+
const d = { kind, id, viewName, paramsJson }
|
|
179
|
+
const evicted = this.stack.pop()
|
|
180
|
+
this.stack.push(d)
|
|
181
|
+
this.change(d, transition)
|
|
182
|
+
if (evicted) this.retire(evicted)
|
|
183
|
+
}
|
|
184
|
+
routerPop = (index: number, transition: number): void => {
|
|
185
|
+
this.rec("routerPop", index, transition)
|
|
186
|
+
if (this.stack.length <= 1) return
|
|
187
|
+
const count = this.stack.length
|
|
188
|
+
const target = index === -1 ? count - 2 : index < -1 ? Math.max(count - 1 + index, 0) : index === 0 ? 0 : Math.min(index, count - 1)
|
|
189
|
+
const evicted = this.stack.splice(target + 1)
|
|
190
|
+
this.change(this.stack[this.stack.length - 1]!, transition)
|
|
191
|
+
for (const d of evicted) this.retire(d)
|
|
192
|
+
}
|
|
193
|
+
routerHide = (transition: number): void => { this.rec("routerHide", transition); this.closeCurrent() }
|
|
194
|
+
routerRestore = (transition: number): void => { this.rec("routerRestore", transition); const top = this.stack[this.stack.length - 1]; if (top) this.present(top, transition) }
|
|
195
|
+
isViewSupported = (name: string): boolean => { this.rec("isViewSupported", name); return this.supportedViews.has(name) }
|
|
196
|
+
supportedViews = new Set<string>()
|
|
197
|
+
/** The contract version of a supported view (1 unless a test says otherwise). */
|
|
198
|
+
viewVersions = new Map<string, number>()
|
|
199
|
+
viewVersion = (name: string): number => this.supportedViews.has(name) ? this.viewVersions.get(name) ?? 1 : 0
|
|
200
|
+
viewCall = (viewId: number, method: string, args: unknown[], onComplete: (r?: unknown) => void, onReject: (m: string) => void): void => {
|
|
201
|
+
this.rec("viewCall", viewId, method, args)
|
|
202
|
+
const h = this.viewCallHandler
|
|
203
|
+
if (h) h(viewId, method, args, onComplete, onReject)
|
|
204
|
+
else onReject(`no view ${viewId}`)
|
|
205
|
+
}
|
|
206
|
+
viewCallHandler: ((viewId: number, method: string, args: any[], res: (r?: unknown) => void, rej: (m: string) => void) => void) | null = null
|
|
207
|
+
showWidget = (id: number, ownerKind: number, ownerId: number): void => { this.rec("showWidget", id, ownerKind, ownerId); if (!this.widgets.includes(id)) this.widgets.push(id) }
|
|
208
|
+
hideWidget = (id: number): void => { this.rec("hideWidget", id); this.widgets = this.widgets.filter((w) => w !== id) }
|
|
209
|
+
|
|
210
|
+
// ---- pager -----------------------------------------------------------------------------------------
|
|
211
|
+
private pagerTop(p: FakeNode): number | null {
|
|
212
|
+
if (!p.pages || !p.lens) return null
|
|
213
|
+
let off = 0
|
|
214
|
+
for (let t = 0; t < (p.index ?? 0); t++) off += p.lens[t] ?? 0
|
|
215
|
+
const len = p.lens[p.index ?? 0] ?? 0
|
|
216
|
+
return len > 0 ? p.pages[off + len - 1] ?? null : null
|
|
217
|
+
}
|
|
218
|
+
private pagerOpen = new Map<number, number>() // pager → the open page
|
|
219
|
+
private pagerRefresh(p: FakeNode): void {
|
|
220
|
+
const top = this.pagerTop(p)
|
|
221
|
+
const open = this.pagerOpen.get(p.id) ?? null
|
|
222
|
+
if (top === open) return
|
|
223
|
+
if (open !== null) { this.pagerOpen.delete(p.id); this.emitter?.(open, TREE_EVENT_CLOSE, TREE_DEST_SCREEN, open) }
|
|
224
|
+
if (top !== null) { this.pagerOpen.set(p.id, top); this.emitter?.(top, TREE_EVENT_OPEN, TREE_DEST_SCREEN, top) }
|
|
225
|
+
}
|
|
226
|
+
pagerSetContent = (id: number, pages: number[], lens: number[], index: number): void => {
|
|
227
|
+
this.rec("pagerSetContent", id, pages.slice(), lens.slice(), index)
|
|
228
|
+
const p = this.nodes.get(id); if (!p) return
|
|
229
|
+
const gone = (p.pages ?? []).filter((old) => !pages.includes(old))
|
|
230
|
+
p.pages = pages.slice(); p.lens = lens.slice(); p.index = Math.max(0, Math.min(index, lens.length - 1))
|
|
231
|
+
for (const pg of pages) { const n = this.nodes.get(pg); if (n) n.pagerOwner = id }
|
|
232
|
+
this.pagerRefresh(p) // the old top's CLOSE fires BEFORE its DETACHED, like the runtime
|
|
233
|
+
for (const old of gone) { const n = this.nodes.get(old); if (n) n.pagerOwner = undefined; this.emitter?.(old, TREE_EVENT_DETACHED) }
|
|
234
|
+
}
|
|
235
|
+
pagerSelect = (id: number, index: number, animated: boolean): void => {
|
|
236
|
+
this.rec("pagerSelect", id, index, animated)
|
|
237
|
+
const p = this.nodes.get(id); if (!p || !p.lens) return
|
|
238
|
+
p.index = Math.max(0, Math.min(index, p.lens.length - 1))
|
|
239
|
+
this.pagerRefresh(p)
|
|
240
|
+
}
|
|
241
|
+
private pagerOffset(p: FakeNode): number { let off = 0; for (let t = 0; t < (p.index ?? 0); t++) off += p.lens![t] ?? 0; return off }
|
|
242
|
+
pagerPush = (id: number, page: number, transition: number, back: number): void => {
|
|
243
|
+
this.rec("pagerPush", id, page, transition, back)
|
|
244
|
+
const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
|
|
245
|
+
p.pages.splice(this.pagerOffset(p) + p.lens[p.index!]!, 0, page); p.lens[p.index!]!++
|
|
246
|
+
const n = this.nodes.get(page); if (n) n.pagerOwner = id
|
|
247
|
+
this.pagerRefresh(p)
|
|
248
|
+
}
|
|
249
|
+
pagerPop = (id: number, transition: number): void => {
|
|
250
|
+
this.rec("pagerPop", id, transition)
|
|
251
|
+
const p = this.nodes.get(id); if (!p || !p.pages || !p.lens || p.lens[p.index!]! <= 1) return
|
|
252
|
+
const [gone] = p.pages.splice(this.pagerOffset(p) + p.lens[p.index!]! - 1, 1); p.lens[p.index!]!--
|
|
253
|
+
this.pagerRefresh(p)
|
|
254
|
+
if (gone !== undefined) { const n = this.nodes.get(gone); if (n) n.pagerOwner = undefined; this.emitter?.(gone, TREE_EVENT_DETACHED) }
|
|
255
|
+
}
|
|
256
|
+
pagerPopToRoot = (id: number, transition: number): void => {
|
|
257
|
+
this.rec("pagerPopToRoot", id, transition)
|
|
258
|
+
const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
|
|
259
|
+
const len = p.lens[p.index!]!; if (len <= 1) return
|
|
260
|
+
const gone = p.pages.splice(this.pagerOffset(p) + 1, len - 1); p.lens[p.index!] = 1
|
|
261
|
+
this.pagerRefresh(p)
|
|
262
|
+
for (const g of gone) { const n = this.nodes.get(g); if (n) n.pagerOwner = undefined; this.emitter?.(g, TREE_EVENT_DETACHED) }
|
|
263
|
+
}
|
|
264
|
+
pagerReplace = (id: number, page: number, transition: number, back: number): void => {
|
|
265
|
+
this.rec("pagerReplace", id, page, transition, back)
|
|
266
|
+
const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
|
|
267
|
+
const at = this.pagerOffset(p) + p.lens[p.index!]! - 1
|
|
268
|
+
const old = p.pages[at]
|
|
269
|
+
p.pages[at] = page
|
|
270
|
+
const n = this.nodes.get(page); if (n) n.pagerOwner = id
|
|
271
|
+
this.pagerRefresh(p)
|
|
272
|
+
if (old !== undefined) { const on = this.nodes.get(old); if (on) on.pagerOwner = undefined; this.emitter?.(old, TREE_EVENT_DETACHED) }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- vlist (the window = every key: tests don't scroll) --------------------------------------------
|
|
276
|
+
private vl(id: number): FakeNode["vlist"] | undefined { const n = this.nodes.get(id); if (!n) return; return n.vlist ??= { keys: [], mounted: new Map() } }
|
|
277
|
+
private vlistSync(id: number): void {
|
|
278
|
+
const v = this.vl(id); if (!v) return
|
|
279
|
+
const unmount = [...v.mounted.keys()].filter((k) => !v.keys.includes(k))
|
|
280
|
+
const mount = v.keys.filter((k) => !v.mounted.has(k))
|
|
281
|
+
for (const k of unmount) v.mounted.delete(k)
|
|
282
|
+
if (unmount.length || mount.length) this.emitter?.(id, TREE_EVENT_VLIST_SYNC, unmount, mount)
|
|
283
|
+
}
|
|
284
|
+
vlistInit = (id: number, overscan: number, inverted: boolean, start: number, end: number): void => { this.rec("vlistInit", id, overscan, inverted, start, end); this.vl(id) }
|
|
285
|
+
vlistSetKeys = (id: number, keys: string[], estimates: number[]): void => { this.rec("vlistSetKeys", id, keys.slice(), estimates.slice()); const v = this.vl(id); if (!v) return; v.keys = keys.slice(); this.vlistSync(id) }
|
|
286
|
+
vlistInsertKeys = (id: number, index: number, keys: string[], estimates: number[]): void => {
|
|
287
|
+
this.rec("vlistInsertKeys", id, index, keys.slice(), estimates.slice())
|
|
288
|
+
const v = this.vl(id); if (!v) return
|
|
289
|
+
if (index < 0) v.keys.push(...keys); else v.keys.splice(index, 0, ...keys)
|
|
290
|
+
this.vlistSync(id)
|
|
291
|
+
}
|
|
292
|
+
vlistRemoveKeys = (id: number, keys: string[]): void => { this.rec("vlistRemoveKeys", id, keys.slice()); const v = this.vl(id); if (!v) return; v.keys = v.keys.filter((k) => !keys.includes(k)); this.vlistSync(id) }
|
|
293
|
+
vlistInvalidate = (id: number, key: string): void => { this.rec("vlistInvalidate", id, key) }
|
|
294
|
+
vlistMount = (id: number, key: string, item: number): void => { this.rec("vlistMount", id, key, item); const v = this.vl(id); if (v) v.mounted.set(key, item) }
|
|
295
|
+
vlistScrollTo = (id: number, offset: number, animated: boolean): void => { this.rec("vlistScrollTo", id, offset, animated) }
|
|
296
|
+
vlistScrollToKey = (id: number, key: string, animated: boolean): void => { this.rec("vlistScrollToKey", id, key, animated) }
|
|
297
|
+
vlistScrollToEnd = (id: number, animated: boolean): void => { this.rec("vlistScrollToEnd", id, animated) }
|
|
298
|
+
|
|
299
|
+
// ---- misc ------------------------------------------------------------------------------------------
|
|
300
|
+
registerFont = (family: string, url: string, weight: number, style: number, onComplete: () => void, onReject: () => void): void => { this.rec("registerFont", family, url, weight, style); void onReject; onComplete() }
|
|
301
|
+
toast = (message: string, durationMs: number): void => { this.rec("toast", message, durationMs) }
|
|
302
|
+
pressed = 0 // the node a test declares pressed (isButtonPressed)
|
|
303
|
+
isButtonPressed = (id: number): boolean => { this.rec("isButtonPressed", id); return id === this.pressed }
|
|
304
|
+
hovered = 0 // the node a test declares under the cursor (isHovered)
|
|
305
|
+
isHovered = (id: number): boolean => { this.rec("isHovered", id); return id === this.hovered }
|
|
306
|
+
getBoundingClientRect = (id: number): [number, number, number, number] | null => this.rects.get(id) ?? null
|
|
307
|
+
getDisplaySize = (): [number, number] => this.displaySize
|
|
308
|
+
tweenCreate = (data: Float32Array, strings: string[], targets: number[]): Handle => { const id = this.nextTween++; this.tweens.push({ id, data, strings, targets }); this.rec("tweenCreate", id, targets.slice()); return { id } }
|
|
309
|
+
tweenControl = (id: number, op: number): void => { this.rec("tweenControl", id, op) }
|
|
310
|
+
tweenSeek = (id: number, ms: number): void => { this.rec("tweenSeek", id, ms) }
|
|
311
|
+
tweenSetRate = (id: number, rate: number): void => { this.rec("tweenSetRate", id, rate) }
|
|
312
|
+
tweenGetTime = (id: number): number => 0
|
|
313
|
+
tweenGetElapsed = (id: number): number => 0
|
|
314
|
+
registerTweenEvent = (cb: (id: number, kind: number, index: number, ...lanes: number[]) => void): void => { this.tweenEvent = cb }
|
|
315
|
+
setEmitter = (fn: (id: number, kind: number, ...args: any[]) => any): void => {
|
|
316
|
+
this.emitter = fn
|
|
317
|
+
if (!FakeTree.primary) FakeTree.primary = fn
|
|
318
|
+
}
|
|
319
|
+
/** The FIRST emitter ever installed in this process: the test SDK's (the preload calls
|
|
320
|
+
* `ensureEmitter()` right after installing the first fake). A test that compiles an app bundle
|
|
321
|
+
* and imports it runs a SECOND SDK copy whose first element calls `setEmitter` too, hijacking
|
|
322
|
+
* the shared fake for the rest of the process — every later file's tree events would land in
|
|
323
|
+
* that dead copy's pins. `installFakeTree()` therefore restores the primary, not whatever the
|
|
324
|
+
* previous fake last had; within the bundle test itself the bundle's emitter stays active. */
|
|
325
|
+
static primary: ((id: number, kind: number, ...args: any[]) => any) | null = null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Install a fresh fake as `_creatorTree`. The SDK installs its emitter / tween callback once per
|
|
329
|
+
* world, so the fake carries them forward: the PRIMARY emitter (the test SDK's — see
|
|
330
|
+
* `FakeTree.primary`), and the tween callback the previous fake had. The previous fake's nodes are
|
|
331
|
+
* all reported freed first (TREE_EVENT_FREED, as the runtime reports a torn-down subtree): the new
|
|
332
|
+
* fake restarts the ids at 1, and an element the SDK still pinned from an earlier test or file
|
|
333
|
+
* would otherwise answer for the new node with the same id (`parent`, `children`, events). */
|
|
334
|
+
export const installFakeTree = (): FakeTree => {
|
|
335
|
+
const prev = (globalThis as any)._creatorTree as FakeTree | undefined
|
|
336
|
+
if (prev) FakeTree.primary?.(0, TREE_EVENT_FREED, prev.allIds())
|
|
337
|
+
const fake = new FakeTree()
|
|
338
|
+
fake.emitter = FakeTree.primary ?? prev?.emitter ?? null
|
|
339
|
+
if (prev?.tweenEvent) fake.tweenEvent = prev.tweenEvent // registered once per world too
|
|
340
|
+
_resetTransitions() // …while a transition is registered per RUNTIME: the new fake has none
|
|
341
|
+
;(globalThis as any)._creatorTree = fake
|
|
342
|
+
return fake
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The fake currently installed. */
|
|
346
|
+
export const fakeTree = (): FakeTree => (globalThis as any)._creatorTree as FakeTree
|
|
347
|
+
|
|
348
|
+
/** Hand the installed fake back to the test SDK's emitter (`FakeTree.primary`). A file that evaluates
|
|
349
|
+
* an app bundle runs a second SDK copy whose first element calls `setEmitter` on the SHARED fake:
|
|
350
|
+
* it calls this in its afterAll, or every later file's tree events land in that dead copy. */
|
|
351
|
+
export const restorePrimaryEmitter = (): void => {
|
|
352
|
+
if (FakeTree.primary) fakeTree().emitter = FakeTree.primary
|
|
353
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The split host tables (sdk/src/bridges: `_creatorApp _creatorDevice _creatorFetch _creatorStorage
|
|
2
|
+
// _creatorMedia _creatorInput`) as MUTABLE stub objects, installed by the preload (bunfig.toml) BEFORE
|
|
3
|
+
// any SDK module loads. The SDK reads the globals at call time, but bun test shares one module
|
|
4
|
+
// registry across every test file, and the SDK's once-per-process registrations (the app-event
|
|
5
|
+
// dispatcher, the media emitter) hand their callback to whichever table was current at that moment.
|
|
6
|
+
// A test therefore MUTATES a stub (add / replace / delete members, or `Object.assign`), never
|
|
7
|
+
// replaces the global; `resetHostStubs()` empties them all (call it from a beforeEach that installs
|
|
8
|
+
// its own members, or `beforeAll` in a file that sets them once). `_creatorService`, `_creatorSocket`
|
|
9
|
+
// and `_creatorFiles` are feature-detected at CALL time (typeof), so a test sets / deletes those
|
|
10
|
+
// globals itself.
|
|
11
|
+
export const appStub: any = {}
|
|
12
|
+
export const deviceStub: any = {}
|
|
13
|
+
export const fetchStub: any = {}
|
|
14
|
+
export const storageStub: any = {}
|
|
15
|
+
export const mediaStub: any = {}
|
|
16
|
+
export const inputStub: any = {}
|
|
17
|
+
|
|
18
|
+
const g = globalThis as any
|
|
19
|
+
g._creatorApp = appStub
|
|
20
|
+
g._creatorDevice = deviceStub
|
|
21
|
+
g._creatorFetch = fetchStub
|
|
22
|
+
g._creatorStorage = storageStub
|
|
23
|
+
g._creatorMedia = mediaStub
|
|
24
|
+
g._creatorInput = inputStub
|
|
25
|
+
|
|
26
|
+
/** Empty every stub table (the members a previous test / file installed). */
|
|
27
|
+
export const resetHostStubs = () => {
|
|
28
|
+
for (const s of [appStub, deviceStub, fetchStub, storageStub, mediaStub, inputStub]) {
|
|
29
|
+
for (const k of Object.keys(s)) delete s[k]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// `lecodes-sdk/testing` — the fake host of the SDK's own unit tests, for a test OUTSIDE this repo
|
|
2
|
+
// that evaluates a compiled bundle with no runtime around (the scene editor's harness test in
|
|
3
|
+
// lecodes-tools): the in-memory tree bridge, the host tables as mutable stubs, the entity world's
|
|
4
|
+
// teardown. Such a suite preloads `lecodes-sdk/testing/preload` (bunfig.toml), exactly as this
|
|
5
|
+
// package's does (tests/README.md has the rules: one process, leave nothing behind).
|
|
6
|
+
//
|
|
7
|
+
// Also what a `_creator` stub needs of the SDK to answer the way an engine does: the tree's event
|
|
8
|
+
// constants and the quaternion (the euler order is the SDK's).
|
|
9
|
+
|
|
10
|
+
export { fakeTree, installFakeTree, restorePrimaryEmitter, FakeTree, type FakeNode, type Call } from "./fakeTree"
|
|
11
|
+
export { appStub, deviceStub, fetchStub, inputStub, mediaStub, storageStub, resetHostStubs } from "./hostStubs"
|
|
12
|
+
export { freeEntities } from "./engineWorld"
|
|
13
|
+
export * from "../../src/ui/tree"
|
|
14
|
+
export { Quat } from "../../src/math/quat"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory `MarciTransport` for tests: enough of marcidb's query language for the auth runtime and
|
|
3
|
+
* simple endpoint tests (equality `$where` on scalars and `one` relations, `$and`, `$order`/`$limit`/
|
|
4
|
+
* `$skip`, select shapes with nested `one` selects, `$increment`, updateMany/deleteMany, count, batch).
|
|
5
|
+
* Defaults come from the model definitions. Not a marcidb — the e2e test against the real engine is
|
|
6
|
+
* the source of truth; this makes the flows runnable everywhere.
|
|
7
|
+
*/
|
|
8
|
+
import type { MarciOp, MarciTransport } from "../../src/server/db/marci/query"
|
|
9
|
+
import type { Field, FieldDef } from "../../src/server/db/fields"
|
|
10
|
+
import type { Model } from "../../src/server/db/types"
|
|
11
|
+
|
|
12
|
+
type Row = Record<string, any>
|
|
13
|
+
|
|
14
|
+
export const memoryMarci = (models: Record<string, Model> | (() => Record<string, Model>)) => {
|
|
15
|
+
const modelsOf = typeof models === "function" ? models : () => models
|
|
16
|
+
const tables = new Map<string, Row[]>()
|
|
17
|
+
const nextId = new Map<string, number>()
|
|
18
|
+
const defs = (model: string): Record<string, FieldDef> =>
|
|
19
|
+
Object.fromEntries(Object.entries(modelsOf()[model]?.fields ?? {}).map(([k, f]) => [k, (f as Field<any, any>).def]))
|
|
20
|
+
const rows = (model: string) => { let t = tables.get(model); if (!t) { t = []; tables.set(model, t) } return t }
|
|
21
|
+
const ops: MarciOp[] = []
|
|
22
|
+
|
|
23
|
+
const matchesField = (row: Row, key: string, cond: any, d: FieldDef | undefined): boolean => {
|
|
24
|
+
const v = row[key]
|
|
25
|
+
if (d && (d.kind === "one")) {
|
|
26
|
+
if (cond === null) return v === null || v === undefined
|
|
27
|
+
if (typeof cond === "object" && "id" in cond) return v?.id === cond.id
|
|
28
|
+
if (typeof cond === "object" && "$not" in cond) return !matchesField(row, key, cond.$not, d)
|
|
29
|
+
throw new Error(`memoryMarci: unsupported relation condition ${JSON.stringify(cond)}`)
|
|
30
|
+
}
|
|
31
|
+
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
32
|
+
for (const [op, x] of Object.entries(cond)) {
|
|
33
|
+
switch (op) {
|
|
34
|
+
case "$eq": if (v !== x) return false; break
|
|
35
|
+
case "$ne": case "$not": if (v === x) return false; break
|
|
36
|
+
case "$in": if (!(x as any[]).includes(v)) return false; break
|
|
37
|
+
case "$gt": if (!(v > (x as any))) return false; break
|
|
38
|
+
case "$gte": if (!(v >= (x as any))) return false; break
|
|
39
|
+
case "$lt": if (!(v < (x as any))) return false; break
|
|
40
|
+
case "$lte": if (!(v <= (x as any))) return false; break
|
|
41
|
+
default: throw new Error(`memoryMarci: unsupported operator ${op}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
return v === cond
|
|
47
|
+
}
|
|
48
|
+
const matches = (model: string, row: Row, where: any): boolean => {
|
|
49
|
+
if (!where) return true
|
|
50
|
+
if (where.$and) return where.$and.every((w: any) => matches(model, row, w))
|
|
51
|
+
if (where.$or) return where.$or.some((w: any) => matches(model, row, w))
|
|
52
|
+
if (where.$not) return !matches(model, row, where.$not)
|
|
53
|
+
const d = defs(model)
|
|
54
|
+
return Object.entries(where).every(([k, c]) => matchesField(row, k, c, d[k]))
|
|
55
|
+
}
|
|
56
|
+
const project = (model: string, row: Row, shape: Record<string, any>): Row => {
|
|
57
|
+
const out: Row = {}
|
|
58
|
+
const d = defs(model)
|
|
59
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
60
|
+
if (k.startsWith("$") || v === false || v === undefined) continue
|
|
61
|
+
const def = d[k]
|
|
62
|
+
if (def?.kind === "one") {
|
|
63
|
+
const ref = row[k]
|
|
64
|
+
if (!ref) { out[k] = null; continue }
|
|
65
|
+
const target = rows(def.model!).find(r => r.id === ref.id)
|
|
66
|
+
out[k] = target ? (v === true ? { id: target.id } : project(def.model!, target, v)) : null
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
out[k] = row[k] === undefined ? null : row[k]
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
const select = (model: string, q: Record<string, any>): Row[] => {
|
|
74
|
+
let list = rows(model).filter(r => matches(model, r, q.$where))
|
|
75
|
+
if (q.$order) {
|
|
76
|
+
const [[f, dir]] = Object.entries(q.$order) as [string, string][]
|
|
77
|
+
list = [...list].sort((a, b) => (a[f] < b[f] ? -1 : a[f] > b[f] ? 1 : 0) * (dir === "desc" ? -1 : 1))
|
|
78
|
+
}
|
|
79
|
+
if (q.$skip) list = list.slice(q.$skip)
|
|
80
|
+
if (q.$limit !== undefined) list = list.slice(0, q.$limit)
|
|
81
|
+
return list.map(r => project(model, r, q))
|
|
82
|
+
}
|
|
83
|
+
const applyUpdate = (model: string, row: Row, data: Row) => {
|
|
84
|
+
const d = defs(model)
|
|
85
|
+
for (const [k, v] of Object.entries(data)) {
|
|
86
|
+
if (v !== null && typeof v === "object" && "$increment" in v) { row[k] = (row[k] ?? 0) + v.$increment; continue }
|
|
87
|
+
if (d[k]?.kind === "one") { row[k] = v === null ? null : { id: (v as any).id ?? (v as any).$connect?.id }; continue }
|
|
88
|
+
row[k] = v
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const insert = (model: string, data: Row): Row => {
|
|
92
|
+
const d = defs(model)
|
|
93
|
+
const id = (nextId.get(model) ?? 0) + 1
|
|
94
|
+
nextId.set(model, id)
|
|
95
|
+
const row: Row = { id }
|
|
96
|
+
for (const [k, def] of Object.entries(d)) {
|
|
97
|
+
if (def.kind === "many" || def.kind === "list") continue
|
|
98
|
+
if (k in data && data[k] !== undefined) { row[k] = def.kind === "one" ? { id: data[k].id } : data[k]; continue }
|
|
99
|
+
if (def.hasDefault) { row[k] = def.default === "now" ? Date.now() : def.default; continue }
|
|
100
|
+
row[k] = null
|
|
101
|
+
}
|
|
102
|
+
rows(model).push(row)
|
|
103
|
+
return row
|
|
104
|
+
}
|
|
105
|
+
const idOf = (id: any) => (typeof id === "object" ? id.id : id)
|
|
106
|
+
|
|
107
|
+
const exec = async (op: MarciOp): Promise<any> => {
|
|
108
|
+
ops.push(op)
|
|
109
|
+
const { model, action } = op
|
|
110
|
+
switch (action) {
|
|
111
|
+
case "insert": return { id: insert(model, op.data).id }
|
|
112
|
+
case "findMany": return select(model, op.query ?? {})
|
|
113
|
+
case "findFirst": return select(model, { ...(op.query ?? {}), $limit: 1 })[0] ?? null
|
|
114
|
+
case "update": { const r = rows(model).find(r => r.id === idOf(op.id)); if (!r) throw new Error("not found"); applyUpdate(model, r, op.data); return null }
|
|
115
|
+
case "updateMany": { let n = 0; for (const r of rows(model)) if (matches(model, r, op.query?.$where)) { applyUpdate(model, r, op.data); n++ } return n }
|
|
116
|
+
case "delete": { const t = rows(model); const i = t.findIndex(r => r.id === idOf(op.id)); if (i >= 0) t.splice(i, 1); return i >= 0 }
|
|
117
|
+
case "deleteMany": { const t = rows(model); const keep = t.filter(r => !matches(model, r, op.query?.$where)); const n = t.length - keep.length; tables.set(model, keep); return n }
|
|
118
|
+
case "count": return rows(model).filter(r => matches(model, r, op.query?.$where)).length
|
|
119
|
+
default: throw new Error(`memoryMarci: unsupported action ${action}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const transport: MarciTransport = { exec, batch: async (batch) => { const out = []; for (const op of batch) out.push(await exec(op)); return out } }
|
|
123
|
+
return { transport, ops, rows, tables }
|
|
124
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The aspect frame source for every test file, installed by the preload (bunfig.toml) BEFORE any
|
|
2
|
+
// SDK module runs. core/Aspect.ts wires its phase runners ONCE per process: the first engine layer's
|
|
3
|
+
// installer wins (`_installAspectFrames`, called by the first Scene / Scene2D), and the first
|
|
4
|
+
// attached updater hands the runners to it — or, with no installer yet, to the `setLoop` of that
|
|
5
|
+
// moment. bun test shares one module registry across files, so a file's own
|
|
6
|
+
// `_creator.setEarlyUpdate` stub only ever saw them if that file happened to attach first (a
|
|
7
|
+
// DynamicBone attached with no Scene, in an earlier file, sent them into its `setLoop = () => 1`).
|
|
8
|
+
// A file that drives the phases by hand ticks through `tick` here instead.
|
|
9
|
+
//
|
|
10
|
+
// No fixed hook is installed: the SDK's own 1/60 accumulator steps `updateFixed` inside the early
|
|
11
|
+
// phase, as on a host without setFixedUpdate. Files that compile a bundle (aspect-update,
|
|
12
|
+
// aspect-lifecycle) run their own copy of the SDK and still capture the bridges' hooks themselves.
|
|
13
|
+
import { _installAspectFrames } from "../../src/core/Aspect"
|
|
14
|
+
|
|
15
|
+
type PhaseFn = (dt: number) => void
|
|
16
|
+
|
|
17
|
+
/** The two render-synced phase runners, set on the first updater's attach (undefined until then). */
|
|
18
|
+
export const phases: { early?: PhaseFn; late?: PhaseFn } = {}
|
|
19
|
+
|
|
20
|
+
/** One frame: the early phase (updateBefore + the fixed fallback), then the late one (update). */
|
|
21
|
+
export const tick = (dt: number): void => { phases.early?.(dt); phases.late?.(dt) }
|
|
22
|
+
|
|
23
|
+
_installAspectFrames((early, late) => { phases.early = early; phases.late = late; return true })
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// bun test preload (sdk/bunfig.toml): every test file starts with the split host tables installed as
|
|
2
|
+
// mutable stubs (./hostStubs.ts — they MUST exist before the first SDK import, see there) and a fake
|
|
3
|
+
// tree bridge, so UI elements can be constructed anywhere (the tree of truth: an element IS a runtime
|
|
4
|
+
// node). Tests that inspect bridge traffic call `installFakeTree()` in their beforeEach for a fresh
|
|
5
|
+
// recorder, and mutate / `resetHostStubs()` the host stubs for their own members. The aspect phase
|
|
6
|
+
// runners go to ./phases.ts (once per process — see there); a file drives frames with its `tick`.
|
|
7
|
+
//
|
|
8
|
+
// `EDITOR` is a compile-time define (bundler.ts), so SDK source run as it is — here — has to be
|
|
9
|
+
// given one: `true`, the editor bundle's value, which leaves edit mode to the run-time flag a test
|
|
10
|
+
// sets (`__lecodesSceneEdit`) exactly as before.
|
|
11
|
+
;(globalThis as { EDITOR?: boolean }).EDITOR = true
|
|
12
|
+
import "./hostStubs"
|
|
13
|
+
import "./phases"
|
|
14
|
+
import { installFakeTree } from "./fakeTree"
|
|
15
|
+
import { ensureEmitter } from "../../src/ui/tree"
|
|
16
|
+
|
|
17
|
+
installFakeTree()
|
|
18
|
+
ensureEmitter() // the test SDK's emitter becomes the fake's PRIMARY (see FakeTree.primary)
|