@forgeax/engine-app 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +20 -14
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
  4. package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
  6. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
  7. package/dist/animation-asset-lookup.d.ts +1 -2
  8. package/dist/animation-asset-lookup.d.ts.map +1 -1
  9. package/dist/create-app.d.ts +31 -10
  10. package/dist/create-app.d.ts.map +1 -1
  11. package/dist/engine-worker-runtime.mjs +72 -129
  12. package/dist/engine-worker-runtime.mjs.map +1 -1
  13. package/dist/errors.d.ts +3 -12
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/execution/attached-world-swap.d.ts +1 -12
  16. package/dist/execution/attached-world-swap.d.ts.map +1 -1
  17. package/dist/execution/bootstrap-url.d.ts +5 -0
  18. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  19. package/dist/execution/control.d.ts +0 -2
  20. package/dist/execution/control.d.ts.map +1 -1
  21. package/dist/execution/engine-worker.d.ts +1 -0
  22. package/dist/execution/engine-worker.d.ts.map +1 -1
  23. package/dist/execution/host-controller.d.ts.map +1 -1
  24. package/dist/execution/index.d.ts +1 -1
  25. package/dist/execution/index.d.ts.map +1 -1
  26. package/dist/execution/protocol.d.ts +4 -1
  27. package/dist/execution/protocol.d.ts.map +1 -1
  28. package/dist/execution/types.d.ts +17 -1
  29. package/dist/execution/types.d.ts.map +1 -1
  30. package/dist/game-context.d.ts +1 -1
  31. package/dist/game-context.d.ts.map +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.mjs +332 -366
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/internal/assembled-engine-profile.d.ts.map +1 -1
  37. package/dist/internal/assets-world-plugin.d.ts +3 -17
  38. package/dist/internal/assets-world-plugin.d.ts.map +1 -1
  39. package/dist/internal/browser-remote-bridge.d.ts +1 -2
  40. package/dist/internal/browser-remote-bridge.d.ts.map +1 -1
  41. package/dist/internal/browser-rhi-debug-runtime.d.ts +1 -1
  42. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
  43. package/dist/internal/debug-draw.d.ts +9 -0
  44. package/dist/internal/debug-draw.d.ts.map +1 -0
  45. package/dist/internal/engine-profile-common.d.ts +2 -2
  46. package/dist/internal/engine-profile-common.d.ts.map +1 -1
  47. package/dist/internal/frame-loop.d.ts +7 -4
  48. package/dist/internal/frame-loop.d.ts.map +1 -1
  49. package/dist/internal/main-engine-profile.d.ts +4 -0
  50. package/dist/internal/main-engine-profile.d.ts.map +1 -1
  51. package/dist/internal/remote-serve-flag.d.ts +3 -7
  52. package/dist/internal/remote-serve-flag.d.ts.map +1 -1
  53. package/dist/internal/rhi-capture.d.ts +1 -1
  54. package/dist/internal/rhi-capture.d.ts.map +1 -1
  55. package/dist/internal/worker-engine-profile.d.ts +2 -0
  56. package/dist/internal/worker-engine-profile.d.ts.map +1 -1
  57. package/dist/renderer-plugin.d.ts +25 -0
  58. package/dist/renderer-plugin.d.ts.map +1 -0
  59. package/dist/tool-preview/bootstrap.d.ts.map +1 -1
  60. package/dist/types.d.ts +21 -61
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +25 -26
  63. package/src/__tests__/callback-inference.test-d.ts +7 -3
  64. package/src/__tests__/consumer-migration.unit.test.ts +13 -0
  65. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  66. package/src/__tests__/create-app-features.test.ts +21 -10
  67. package/src/__tests__/create-app-listener-sync.test.ts +23 -19
  68. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  69. package/src/__tests__/create-app-stop.test.ts +4 -12
  70. package/src/__tests__/create-app.test-d.ts +12 -11
  71. package/src/__tests__/create-app.test.ts +1 -60
  72. package/src/__tests__/create-renderer-error.integration.test.ts +3 -2
  73. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +10 -5
  74. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +7 -13
  75. package/src/__tests__/draw-source-single-world-regression.test.ts +23 -24
  76. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +8 -0
  77. package/src/__tests__/engine-worker-world-swap.unit.test.ts +15 -22
  78. package/src/__tests__/errors-pointer-lock-failed.test.ts +4 -7
  79. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +5 -6
  80. package/src/__tests__/execution-frame-order.unit.test.ts +2 -2
  81. package/src/__tests__/execution-frame-protocol.unit.test.ts +3 -4
  82. package/src/__tests__/execution-public-api.test-d.ts +4 -0
  83. package/src/__tests__/frame-loop-time-elapsed.test.ts +1 -1
  84. package/src/__tests__/frame-loop-time.unit.test.ts +1 -1
  85. package/src/__tests__/frame-loop-world-array.test.ts +82 -73
  86. package/src/__tests__/plugin-profiles.unit.test.ts +3 -3
  87. package/src/__tests__/profiler-default-off.test.ts +1 -1
  88. package/src/__tests__/profiler-frame-token.test.ts +17 -30
  89. package/src/__tests__/profiler-phase-integration.test.ts +15 -27
  90. package/src/__tests__/surface-handoff.test.ts +1 -4
  91. package/src/__tests__/visibility-remote.integration.test.ts +2 -5
  92. package/src/__tests__/worker-execution-stop.test.ts +64 -7
  93. package/src/animation-asset-lookup.ts +2 -2
  94. package/src/create-app.ts +197 -296
  95. package/src/errors.ts +27 -39
  96. package/src/execution/attached-world-swap.ts +4 -38
  97. package/src/execution/bootstrap-url.ts +25 -0
  98. package/src/execution/control.ts +3 -7
  99. package/src/execution/engine-worker-runtime.ts +23 -40
  100. package/src/execution/engine-worker.ts +2 -0
  101. package/src/execution/host-controller.ts +8 -2
  102. package/src/execution/index.ts +1 -0
  103. package/src/execution/protocol.ts +4 -1
  104. package/src/execution/types.ts +18 -1
  105. package/src/game-context.ts +1 -1
  106. package/src/index.ts +11 -4
  107. package/src/internal/assembled-engine-profile.ts +3 -11
  108. package/src/internal/assets-world-plugin.ts +7 -53
  109. package/src/internal/browser-remote-bridge.ts +12 -10
  110. package/src/internal/browser-rhi-debug-runtime.ts +1 -1
  111. package/src/internal/debug-draw.ts +38 -0
  112. package/src/internal/engine-profile-common.ts +2 -3
  113. package/src/internal/frame-loop.ts +77 -63
  114. package/src/internal/main-engine-profile.ts +21 -17
  115. package/src/internal/remote-serve-flag.ts +5 -14
  116. package/src/internal/rhi-capture.ts +1 -1
  117. package/src/internal/worker-engine-profile.ts +10 -10
  118. package/src/renderer-plugin.ts +96 -0
  119. package/src/tool-preview/bootstrap.ts +1 -14
  120. package/src/types.ts +22 -77
  121. package/src/vite-env.d.ts +0 -2
  122. package/dist/__tests__/app-rebuild.test.d.ts +0 -2
  123. package/dist/__tests__/app-rebuild.test.d.ts.map +0 -1
  124. package/dist/__tests__/asset-registry-types.test-d.d.ts +0 -2
  125. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +0 -1
  126. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +0 -2
  127. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +0 -1
  128. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +0 -2
  129. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +0 -1
  130. package/dist/__tests__/ecs-import.unit.test.d.ts +0 -2
  131. package/dist/__tests__/ecs-import.unit.test.d.ts.map +0 -1
  132. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +0 -2
  133. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +0 -1
  134. package/dist/__tests__/remote-serve-flag.test.d.ts +0 -2
  135. package/dist/__tests__/remote-serve-flag.test.d.ts.map +0 -1
  136. package/dist/assets-runtime-assembly.d.ts +0 -19
  137. package/dist/assets-runtime-assembly.d.ts.map +0 -1
  138. package/src/__tests__/app-rebuild.test.ts +0 -31
  139. package/src/__tests__/asset-registry-types.test-d.ts +0 -16
  140. package/src/__tests__/assets-runtime-assembly.integration.test.ts +0 -68
  141. package/src/__tests__/draw-source-headless-full-chain.test.ts +0 -226
  142. package/src/__tests__/ecs-import.unit.test.ts +0 -40
  143. package/src/__tests__/execution-world-health-owner.test-d.ts +0 -9
  144. package/src/__tests__/remote-serve-flag.test.ts +0 -24
  145. package/src/assets-runtime-assembly.ts +0 -70
@@ -1,226 +0,0 @@
1
- // feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w10
2
- // (RED — impl lands in w12). Full-chain consumer test on the rhi-null headless
3
- // backend: the drawSource seam drives a REAL Renderer (createRenderer +
4
- // RhiNull) end-to-end — update -> extract -> record — with no GPU.
5
- //
6
- // Unlike w8 (spy renderer, call-shape) and w9 (spy renderer, stale-matrix), this
7
- // test exercises the actual runtime render pipeline through the seam. The camera
8
- // + light + visible mesh live ONLY in the injected world; the frame-loop's own
9
- // world is empty. So a non-zero draw count proves three things at once:
10
- // 1. drawSource is consumed (else the empty loop world is drawn -> 0 draws),
11
- // 2. the injected world was world.update(1 / 60).unwrap()'d (propagate ran; extract finds the
12
- // camera + resolved Transform.world),
13
- // 3. the full URP record path fired headlessly (bookkeeper counts draws).
14
- //
15
- // rhi-null needs no GPU: navigator.gpu is stubbed undefined so createRenderer
16
- // selects the injected RhiNull instance, and the RhiNullDevice bookkeeper counts
17
- // draw / bind-group / pipeline records. File suffix `.test.ts` runs in the node
18
- // vitest project (no dawn / no browser).
19
- //
20
- // The frame-loop is driven directly (createFrameLoop + injected now/raf/caf)
21
- // rather than through createApp, because createApp's assemble form does not (yet
22
- // — that is w11) forward drawSource; w12 threads it into the loop. Driving the
23
- // loop directly keeps this test focused on the seam's runtime contract. The
24
- // drawSource option is reached through a typed alias before w12 widens the type.
25
- //
26
- // Anchors:
27
- // plan-strategy §2 D-3 (drawSource pull -> update injected worlds -> draw)
28
- // research F1 (frame-loop is the update -> draw driver; the seam insertion pt)
29
- // feat-20260623 rhi-null-command-flow.unit.test.ts (headless full-chain
30
- // precedent: createRenderer + RhiNull + bookkeeper counters)
31
-
32
- import { World } from '@forgeax/engine-ecs';
33
- import { createPrimitiveMesh } from '@forgeax/engine-geometry';
34
- import {
35
- Camera,
36
- DirectionalLight,
37
- MeshFilter,
38
- MeshRenderer,
39
- type Renderer,
40
- } from '@forgeax/engine-render';
41
- import type { RhiNullDevice } from '@forgeax/engine-rhi-null';
42
- import { rhi } from '@forgeax/engine-rhi-null';
43
- import { createRenderer } from '@forgeax/engine-runtime';
44
- import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
45
-
46
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
47
- import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
48
-
49
- function makeStubCanvas(w = 800, h = 600): HTMLCanvasElement {
50
- return {
51
- width: w,
52
- height: h,
53
- getContext(_kind: string): unknown {
54
- return null;
55
- },
56
- addEventListener: () => undefined,
57
- removeEventListener: () => undefined,
58
- } as unknown as HTMLCanvasElement;
59
- }
60
-
61
- function buildManifestDataUrl(): string {
62
- const manifest = {
63
- schemaVersion: '1.0.0',
64
- entries: [
65
- { hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
66
- { hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
67
- {
68
- hash: 'tonemap0',
69
- wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
70
- glsl: '',
71
- bindings: '',
72
- },
73
- ],
74
- };
75
- return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
76
- }
77
-
78
- function identityTransform(
79
- overrides: Partial<Record<string, number[]>> = {},
80
- ): Record<string, number[]> {
81
- return {
82
- pos: [0, 0, 0],
83
- quat: [0, 0, 0, 1],
84
- scale: [1, 1, 1],
85
- ...overrides,
86
- };
87
- }
88
-
89
- // The injected world carries the FULL renderable set (camera + light + visible
90
- // mesh). It also registers propagateTransforms so the frame-loop's world.update
91
- // resolves Transform.world before extract reads it.
92
- function makeInjectedWorld(): World {
93
- const world = new World();
94
- const cube = createPrimitiveMesh('cube').unwrap();
95
- const cubeHandle = world.internSharedRef('MeshAsset', cube);
96
- registerPropagateTransforms(world);
97
- world.spawn(
98
- { component: Transform, data: identityTransform({ pos: [0, 0, 5] }) },
99
- { component: Camera, data: { fov: 60, near: 0.1, far: 1000, tonemap: 4 } },
100
- );
101
- world.spawn(
102
- { component: Transform, data: identityTransform() },
103
- {
104
- component: DirectionalLight,
105
- data: { direction: [0.5, -1, -0.5], castShadow: true, intensity: 1 },
106
- },
107
- );
108
- world.spawn(
109
- { component: Transform, data: identityTransform() },
110
- { component: MeshFilter, data: { assetHandle: cubeHandle } },
111
- { component: MeshRenderer, data: {} },
112
- );
113
- return world;
114
- }
115
-
116
- function makeSyncScheduler() {
117
- let pending: ((t: number) => void) | null = null;
118
- let clock = 0;
119
- const raf = (cb: (t: number) => void): number => {
120
- pending = cb;
121
- return 1;
122
- };
123
- const caf = (): void => {
124
- pending = null;
125
- };
126
- const now = (): number => {
127
- clock += 16;
128
- return clock;
129
- };
130
- const pump = (frames: number): void => {
131
- for (let i = 0; i < frames; i++) {
132
- const cb = pending;
133
- pending = null;
134
- if (cb === null) break;
135
- cb(clock);
136
- }
137
- };
138
- return { raf, caf, now, pump };
139
- }
140
-
141
- type DrawSourceCallback = () =>
142
- | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
143
- | undefined;
144
-
145
- type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
146
- drawSource?: DrawSourceCallback;
147
- };
148
-
149
- const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
150
- opts: FrameLoopOptionsWithDrawSource,
151
- ) => ReturnType<typeof createFrameLoop>;
152
-
153
- // The RhiNull instance is a structural stand-in for a real RhiInstance in this
154
- // headless test; cast the options bag to RendererOptions so the direct
155
- // createRenderer import (strict RendererOptions) accepts the injected backend.
156
- const rhiOptions = {
157
- rhi: rhi as unknown as import('@forgeax/engine-render').RendererOptions['rhi'],
158
- } as import('@forgeax/engine-render').RendererOptions;
159
-
160
- let renderer: Renderer | null = null;
161
-
162
- beforeEach(() => {
163
- vi.stubGlobal('navigator', { gpu: undefined });
164
- });
165
-
166
- afterEach(() => {
167
- vi.unstubAllGlobals();
168
- if (renderer) {
169
- try {
170
- renderer.dispose();
171
- } catch {
172
- // ignore
173
- }
174
- renderer = null;
175
- }
176
- });
177
-
178
- describe('drawSource full chain on rhi-null headless (w10)', () => {
179
- it('drives update -> extract -> record for an injected world with no GPU', async () => {
180
- const canvas = makeStubCanvas();
181
- renderer = await createRenderer(canvas, rhiOptions, {
182
- shaderManifestUrl: buildManifestDataUrl(),
183
- });
184
- await renderer.ready;
185
-
186
- const rhiNullDevice = renderer.device as unknown as RhiNullDevice;
187
-
188
- // The frame-loop's own world is EMPTY — it has no camera / mesh, so it
189
- // records zero draws on its own. Only the injected world is renderable.
190
- const loopWorld = new World();
191
- const injectedWorld = makeInjectedWorld();
192
-
193
- const drawSource: DrawSourceCallback = () => ({
194
- worlds: [injectedWorld],
195
- cameraOwner: 0,
196
- resourceOwner: 0,
197
- });
198
-
199
- const { raf, caf, now, pump } = makeSyncScheduler();
200
- const loop = createFrameLoopWithDrawSource({
201
- world: loopWorld,
202
- renderer: renderer as Renderer,
203
- now,
204
- raf,
205
- caf,
206
- drawSource,
207
- });
208
-
209
- // Reset per-frame counters immediately before the observed frame.
210
- rhiNullDevice.totalDrawCount = 0;
211
- rhiNullDevice.totalBindGroupCount = 0;
212
- rhiNullDevice.framePassNames = [];
213
-
214
- expect(loop.start().ok).toBe(true);
215
- pump(1);
216
- loop.stop();
217
-
218
- // The full URP record path fired for the INJECTED world: a non-zero draw
219
- // count is only reachable if drawSource was consumed AND the injected world
220
- // was updated (camera + resolved Transform.world present for extract).
221
- expect(rhiNullDevice.totalDrawCount).toBeGreaterThanOrEqual(1);
222
- expect(rhiNullDevice.totalBindGroupCount).toBeGreaterThanOrEqual(1);
223
- expect(renderer.perFramePassNames.length).toBeGreaterThan(0);
224
- expect(renderer.perFramePassNames).toContain('main');
225
- });
226
- });
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- createEcsImportModule,
4
- ECS_PUBLIC_SYMBOLS,
5
- projectEcsPublicModule,
6
- } from '../internal/ecs-import';
7
-
8
- describe('host-owned ECS import projection', () => {
9
- it('keeps the explicit six-symbol surface and drops implementation exports', () => {
10
- expect(ECS_PUBLIC_SYMBOLS).toEqual([
11
- 'World',
12
- 'Entity',
13
- 'Update',
14
- 'FixedUpdate',
15
- 'Time',
16
- 'FixedTime',
17
- ]);
18
- expect(
19
- projectEcsPublicModule({
20
- World: 'World',
21
- Entity: 'Entity',
22
- defineComponent: 'implementation-only',
23
- }),
24
- ).toEqual({ World: 'World', Entity: 'Entity' });
25
- });
26
-
27
- it('projects only the ECS specifier and preserves other host modules', async () => {
28
- const calls: string[] = [];
29
- const importModule = createEcsImportModule(async (specifier) => {
30
- calls.push(specifier);
31
- return specifier === '@forgeax/engine-ecs'
32
- ? { World: 'World', defineComponent: 'implementation-only' }
33
- : { value: 'other-module' };
34
- });
35
-
36
- await expect(importModule('@forgeax/engine-ecs')).resolves.toEqual({ World: 'World' });
37
- await expect(importModule('virtual:host-module')).resolves.toEqual({ value: 'other-module' });
38
- expect(calls).toEqual(['@forgeax/engine-ecs', 'virtual:host-module']);
39
- });
40
- });
@@ -1,9 +0,0 @@
1
- import type { WorldExecutionHealth } from '@forgeax/engine-ecs/shared';
2
- import { expectTypeOf, it } from 'vitest';
3
- import type { ExecutionReport, ExecutionWorldHealth } from '../index';
4
-
5
- it('projects the public App health type from the ECS World owner', () => {
6
- expectTypeOf<ExecutionWorldHealth>().toEqualTypeOf<WorldExecutionHealth>();
7
- expectTypeOf<WorldExecutionHealth>().toEqualTypeOf<ExecutionWorldHealth>();
8
- expectTypeOf<ExecutionReport['world']['health']>().toEqualTypeOf<WorldExecutionHealth>();
9
- });
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { resolveRemoteServeFlag } from '../internal/remote-serve-flag';
4
-
5
- describe('resolveRemoteServeFlag', () => {
6
- it('keeps the Node server disabled in a browser dev host', () => {
7
- expect(resolveRemoteServeFlag(true, { FORGEAX_ENGINE_REMOTE_SERVE: '1' }, true)).toBe(false);
8
- });
9
-
10
- it('starts in a Node dev host', () => {
11
- expect(resolveRemoteServeFlag(true, undefined, false)).toBe(true);
12
- });
13
-
14
- it('allows explicit headless opt-in outside dev mode', () => {
15
- expect(resolveRemoteServeFlag(undefined, { FORGEAX_ENGINE_REMOTE_SERVE: '1' }, false)).toBe(
16
- true,
17
- );
18
- });
19
-
20
- it('stays off for production and unconfigured headless hosts', () => {
21
- expect(resolveRemoteServeFlag(false, undefined, false)).toBe(false);
22
- expect(resolveRemoteServeFlag(undefined, undefined, false)).toBe(false);
23
- });
24
- });
@@ -1,70 +0,0 @@
1
- import type { AssetDecoderLease, AssetRegistry } from '@forgeax/engine-assets-runtime';
2
- import { defaultAssetDecoderContributions } from '@forgeax/engine-runtime';
3
- import {
4
- type AssetDecoderContributionRef,
5
- type AssetKind,
6
- err,
7
- ok,
8
- type Result,
9
- } from '@forgeax/engine-types';
10
-
11
- export interface AssetRuntimeAssemblyError {
12
- readonly code: 'asset-assembly-failed';
13
- readonly expected: string;
14
- readonly hint: string;
15
- readonly detail: { readonly kind: string; readonly cause: unknown };
16
- }
17
-
18
- export interface AssetRuntimeAssembly {
19
- readonly registry: AssetRegistry;
20
- readonly leases: readonly AssetDecoderLease[];
21
- dispose(): void;
22
- }
23
-
24
- /** Install owner decoder contributions into the realm's one asset registry. */
25
- export function assembleAssetRuntime(
26
- registry: AssetRegistry,
27
- contributions: readonly AssetDecoderContributionRef[],
28
- ): Result<AssetRuntimeAssembly, AssetRuntimeAssemblyError> {
29
- const leases: AssetDecoderLease[] = [];
30
- const overrides = new Set(contributions.map((contribution) => contribution.kind.kind));
31
- const defaults = defaultAssetDecoderContributions ?? [];
32
- const assembledContributions = [
33
- ...defaults.filter((contribution) => !overrides.has(contribution.kind.kind)),
34
- ...contributions,
35
- ];
36
- try {
37
- const installedKinds = new Set<string>();
38
- for (const contribution of assembledContributions) {
39
- if (installedKinds.has(contribution.kind.kind)) {
40
- throw new TypeError(`duplicate decoder kind "${contribution.kind.kind}"`);
41
- }
42
- installedKinds.add(contribution.kind.kind);
43
- leases.push(
44
- registry.installDecoder(
45
- contribution.kind as AssetKind<unknown, string>,
46
- contribution.decoder,
47
- ),
48
- );
49
- }
50
- } catch (cause) {
51
- for (const lease of leases) lease.dispose();
52
- const failed = assembledContributions[leases.length];
53
- return err({
54
- code: 'asset-assembly-failed',
55
- expected: 'each owner decoder contribution to install exactly once in the realm registry',
56
- hint: 'remove the conflicting owner contribution and assemble the current realm again',
57
- detail: { kind: failed?.kind.kind ?? 'unknown', cause },
58
- });
59
- }
60
- let disposed = false;
61
- return ok({
62
- registry,
63
- leases: Object.freeze([...leases]),
64
- dispose() {
65
- if (disposed) return;
66
- disposed = true;
67
- for (const lease of leases) lease.dispose();
68
- },
69
- });
70
- }