@bldrs-ai/conway 1.403.1239 → 1.405.1243

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 (49) hide show
  1. package/compiled/examples/browser-bundled.cjs +1 -1
  2. package/compiled/examples/cli-bundled.cjs +1 -1
  3. package/compiled/examples/cli-step-bundled.cjs +1 -1
  4. package/compiled/examples/validator-bundled.cjs +1 -1
  5. package/compiled/src/core/composed_model_skeleton.d.ts +63 -0
  6. package/compiled/src/core/composed_model_skeleton.d.ts.map +1 -0
  7. package/compiled/src/core/composed_model_skeleton.js +66 -0
  8. package/compiled/src/core/composed_model_skeleton.test.d.ts +2 -0
  9. package/compiled/src/core/composed_model_skeleton.test.d.ts.map +1 -0
  10. package/compiled/src/core/composed_model_skeleton.test.js +69 -0
  11. package/compiled/src/core/model_registry.d.ts +87 -0
  12. package/compiled/src/core/model_registry.d.ts.map +1 -0
  13. package/compiled/src/core/model_registry.js +101 -0
  14. package/compiled/src/core/model_registry.test.d.ts +2 -0
  15. package/compiled/src/core/model_registry.test.d.ts.map +1 -0
  16. package/compiled/src/core/model_registry.test.js +60 -0
  17. package/compiled/src/core/model_uri.d.ts +57 -0
  18. package/compiled/src/core/model_uri.d.ts.map +1 -0
  19. package/compiled/src/core/model_uri.js +101 -0
  20. package/compiled/src/core/model_uri.test.d.ts +2 -0
  21. package/compiled/src/core/model_uri.test.d.ts.map +1 -0
  22. package/compiled/src/core/model_uri.test.js +49 -0
  23. package/compiled/src/core/shared_byte_budget.d.ts +60 -0
  24. package/compiled/src/core/shared_byte_budget.d.ts.map +1 -0
  25. package/compiled/src/core/shared_byte_budget.js +85 -0
  26. package/compiled/src/core/shared_byte_budget.test.d.ts +2 -0
  27. package/compiled/src/core/shared_byte_budget.test.d.ts.map +1 -0
  28. package/compiled/src/core/shared_byte_budget.test.js +55 -0
  29. package/compiled/src/step/parsing/cross_reference_registry.d.ts +95 -0
  30. package/compiled/src/step/parsing/cross_reference_registry.d.ts.map +1 -0
  31. package/compiled/src/step/parsing/cross_reference_registry.js +91 -0
  32. package/compiled/src/step/parsing/cross_reference_registry.test.d.ts +2 -0
  33. package/compiled/src/step/parsing/cross_reference_registry.test.d.ts.map +1 -0
  34. package/compiled/src/step/parsing/cross_reference_registry.test.js +66 -0
  35. package/compiled/src/step/parsing/index_sidecar.d.ts +53 -0
  36. package/compiled/src/step/parsing/index_sidecar.d.ts.map +1 -0
  37. package/compiled/src/step/parsing/index_sidecar.js +166 -0
  38. package/compiled/src/step/parsing/index_sidecar.test.d.ts +2 -0
  39. package/compiled/src/step/parsing/index_sidecar.test.d.ts.map +1 -0
  40. package/compiled/src/step/parsing/index_sidecar.test.js +92 -0
  41. package/compiled/src/step/parsing/range_byte_source.d.ts +69 -0
  42. package/compiled/src/step/parsing/range_byte_source.d.ts.map +1 -0
  43. package/compiled/src/step/parsing/range_byte_source.js +78 -0
  44. package/compiled/src/step/parsing/range_byte_source.test.d.ts +2 -0
  45. package/compiled/src/step/parsing/range_byte_source.test.d.ts.map +1 -0
  46. package/compiled/src/step/parsing/range_byte_source.test.js +70 -0
  47. package/compiled/src/version/version.js +1 -1
  48. package/compiled/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +1 -1
@@ -30,7 +30,7 @@ var import_node_process = require("node:process");
30
30
  var readline = __toESM(require("node:readline"), 1);
31
31
 
32
32
  // compiled/src/version/version.js
33
- var versionString = "Conway v1.403.1239";
33
+ var versionString = "Conway v1.405.1243";
34
34
 
35
35
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
36
36
  var wasmType = "";
@@ -14965,7 +14965,7 @@ ${t5.join("\n")}` : "";
14965
14965
  var import_process = require("process");
14966
14966
 
14967
14967
  // compiled/src/version/version.js
14968
- var versionString = "Conway v1.403.1239";
14968
+ var versionString = "Conway v1.405.1243";
14969
14969
 
14970
14970
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
14971
14971
  function pThreadsAllowed() {
@@ -15943,7 +15943,7 @@ var ParsingBuffer = class {
15943
15943
  };
15944
15944
 
15945
15945
  // compiled/src/version/version.js
15946
- var versionString = "Conway v1.403.1239";
15946
+ var versionString = "Conway v1.405.1243";
15947
15947
 
15948
15948
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
15949
15949
  function pThreadsAllowed() {
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
944
944
  var entity_types_ifc_gen_default = EntityTypesIfc;
945
945
 
946
946
  // compiled/src/version/version.js
947
- var versionString = "Conway v1.403.1239";
947
+ var versionString = "Conway v1.405.1243";
948
948
 
949
949
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
950
950
  var wasmType = "";
@@ -0,0 +1,63 @@
1
+ import { StepEntityConstructorAbstract } from "../step/step_entity_constructor.js";
2
+ import { EntityAddress } from "./model_uri.js";
3
+ import { ModelRegistry } from "./model_registry.js";
4
+ /**
5
+ * A model that can answer a type query — the minimum a federated model must
6
+ * expose to take part in cross-file composition. Both the resident model's
7
+ * type index and the streaming {@link IncrementalTypeIndex} satisfy this, so a
8
+ * composed view works whether its members are fully parsed or still streaming.
9
+ */
10
+ export interface TypeQueryableModel<TypeIDType extends number> {
11
+ /**
12
+ * Express IDs of all records of the given types (including subtypes).
13
+ *
14
+ * @param types The entity constructors to query.
15
+ * @return {IterableIterator<number>} The matching express IDs.
16
+ */
17
+ expressIDsOfTypes(...types: StepEntityConstructorAbstract<TypeIDType>[]): IterableIterator<number>;
18
+ }
19
+ /**
20
+ * A composed view over every model in a {@link ModelRegistry} (M5 / design S3).
21
+ *
22
+ * Federation's read side: a site plan that references per-building files, or a
23
+ * project split across disciplines, is browsed as **one** model whose entities
24
+ * carry universal {@link EntityAddress}es (`modelURI#expressID`). A type query
25
+ * here fans out across all registered members and yields addresses, not bare
26
+ * express IDs — so "every `IfcWall` in the project" spans files, and each
27
+ * result round-trips back through {@link ModelRegistry.resolve} to the right
28
+ * model.
29
+ *
30
+ * This is the composition scaffold: it unions the members' type skeletons
31
+ * under the shared address space. The spatial-containment merge (a site's
32
+ * `IfcRelAggregates` linking to building files) layers on top by resolving the
33
+ * cross-file references the {@link CrossReferenceRegistry} discovers — the next
34
+ * increment, gated on the loader registering streamed models here.
35
+ */
36
+ export declare class ComposedModelSkeleton<TypeIDType extends number, H extends TypeQueryableModel<TypeIDType>> {
37
+ private readonly registry_;
38
+ /**
39
+ * @param registry_ The registry whose models this composes over.
40
+ */
41
+ constructor(registry_: ModelRegistry<H>);
42
+ /**
43
+ * @return {ModelRegistry<H>} The registry backing this composed view.
44
+ */
45
+ get registry(): ModelRegistry<H>;
46
+ /**
47
+ * Yield the universal address of every entity of the given types across all
48
+ * registered models.
49
+ *
50
+ * @param types The entity constructors to query (subtype closures unioned).
51
+ * @return {IterableIterator<EntityAddress>} Addresses of matching entities.
52
+ * @yields {EntityAddress} Each matching entity's universal address.
53
+ */
54
+ entitiesOfType(...types: StepEntityConstructorAbstract<TypeIDType>[]): IterableIterator<EntityAddress>;
55
+ /**
56
+ * Count entities of the given types across all registered models.
57
+ *
58
+ * @param types The entity constructors to count.
59
+ * @return {number} The total across the federation.
60
+ */
61
+ countOfType(...types: StepEntityConstructorAbstract<TypeIDType>[]): number;
62
+ }
63
+ //# sourceMappingURL=composed_model_skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composed_model_skeleton.d.ts","sourceRoot":"","sources":["../../../src/core/composed_model_skeleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAA;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGhD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB,CAAC,UAAU,SAAS,MAAM;IAE3D;;;;;OAKG;IACH,iBAAiB,CACf,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;CACpF;AAGD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAqB,CAChC,UAAU,SAAS,MAAM,EACzB,CAAC,SAAS,kBAAkB,CAAC,UAAU,CAAC;IAK3B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAHvC;;OAEG;gBAC2B,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAEzD;;OAEG;IACH,IAAW,QAAQ,IAAI,aAAa,CAAC,CAAC,CAAC,CAEtC;IAED;;;;;;;OAOG;IACK,cAAc,CAClB,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GACrD,gBAAgB,CAAC,aAAa,CAAC;IAgBnC;;;;;OAKG;IACI,WAAW,CACd,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GAAI,MAAM;CAapE"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A composed view over every model in a {@link ModelRegistry} (M5 / design S3).
3
+ *
4
+ * Federation's read side: a site plan that references per-building files, or a
5
+ * project split across disciplines, is browsed as **one** model whose entities
6
+ * carry universal {@link EntityAddress}es (`modelURI#expressID`). A type query
7
+ * here fans out across all registered members and yields addresses, not bare
8
+ * express IDs — so "every `IfcWall` in the project" spans files, and each
9
+ * result round-trips back through {@link ModelRegistry.resolve} to the right
10
+ * model.
11
+ *
12
+ * This is the composition scaffold: it unions the members' type skeletons
13
+ * under the shared address space. The spatial-containment merge (a site's
14
+ * `IfcRelAggregates` linking to building files) layers on top by resolving the
15
+ * cross-file references the {@link CrossReferenceRegistry} discovers — the next
16
+ * increment, gated on the loader registering streamed models here.
17
+ */
18
+ export class ComposedModelSkeleton {
19
+ /**
20
+ * @param registry_ The registry whose models this composes over.
21
+ */
22
+ constructor(registry_) {
23
+ this.registry_ = registry_;
24
+ }
25
+ /**
26
+ * @return {ModelRegistry<H>} The registry backing this composed view.
27
+ */
28
+ get registry() {
29
+ return this.registry_;
30
+ }
31
+ /**
32
+ * Yield the universal address of every entity of the given types across all
33
+ * registered models.
34
+ *
35
+ * @param types The entity constructors to query (subtype closures unioned).
36
+ * @return {IterableIterator<EntityAddress>} Addresses of matching entities.
37
+ * @yields {EntityAddress} Each matching entity's universal address.
38
+ */
39
+ *entitiesOfType(...types) {
40
+ for (const modelURI of this.registry_.uris()) {
41
+ const model = this.registry_.get(modelURI);
42
+ if (model === void 0) {
43
+ continue;
44
+ }
45
+ for (const expressID of model.expressIDsOfTypes(...types)) {
46
+ yield { modelURI, expressID };
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * Count entities of the given types across all registered models.
52
+ *
53
+ * @param types The entity constructors to count.
54
+ * @return {number} The total across the federation.
55
+ */
56
+ countOfType(...types) {
57
+ let total = 0;
58
+ for (const model of this.registry_.models()) {
59
+ for (const expressID of model.expressIDsOfTypes(...types)) {
60
+ void expressID;
61
+ ++total;
62
+ }
63
+ }
64
+ return total;
65
+ }
66
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=composed_model_skeleton.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composed_model_skeleton.test.d.ts","sourceRoot":"","sources":["../../../src/core/composed_model_skeleton.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M5: the composed view fans a type query across every registered model and
3
+ // yields universal addresses, so "every wall in the project" spans files and
4
+ // each result round-trips back through the registry to its model.
5
+ import { describe, expect, test } from "@jest/globals";
6
+ import { ComposedModelSkeleton } from "./composed_model_skeleton.js";
7
+ import { ModelRegistry } from "./model_registry.js";
8
+ import { SharedByteBudget } from "./shared_byte_budget.js";
9
+ /**
10
+ * A fake type-queryable model returning a fixed express-ID set regardless of
11
+ * the queried types (the query-closure matching itself is covered by the
12
+ * incremental-type-index tests; here we exercise cross-model composition).
13
+ */
14
+ class FakeModel {
15
+ /**
16
+ * @param ids The express IDs this model reports for any query.
17
+ */
18
+ constructor(ids) {
19
+ this.ids = ids;
20
+ }
21
+ /**
22
+ * @return {IterableIterator<number>} The fixed express IDs.
23
+ */
24
+ *expressIDsOfTypes() {
25
+ yield* this.ids;
26
+ }
27
+ }
28
+ // A dummy constructor to satisfy the query signature (unused by FakeModel).
29
+ const ANY_TYPE = { query: [1] };
30
+ describe("ComposedModelSkeleton", () => {
31
+ /**
32
+ * A composed view over two fake models.
33
+ *
34
+ * @return {ComposedModelSkeleton} The composed view.
35
+ */
36
+ function composed() {
37
+ const registry = new ModelRegistry(new SharedByteBudget(1000));
38
+ registry.register("site/a.ifc", new FakeModel([1, 2]));
39
+ registry.register("site/b.ifc", new FakeModel([10, 11, 12]));
40
+ return new ComposedModelSkeleton(registry);
41
+ }
42
+ test("yields universal addresses across every registered model", () => {
43
+ const view = composed();
44
+ const addresses = [...view.entitiesOfType(ANY_TYPE)];
45
+ expect(addresses).toEqual([
46
+ { modelURI: "site/a.ifc", expressID: 1 },
47
+ { modelURI: "site/a.ifc", expressID: 2 },
48
+ { modelURI: "site/b.ifc", expressID: 10 },
49
+ { modelURI: "site/b.ifc", expressID: 11 },
50
+ { modelURI: "site/b.ifc", expressID: 12 },
51
+ ]);
52
+ });
53
+ test("counts across the federation", () => {
54
+ expect(composed().countOfType(ANY_TYPE)).toBe(5);
55
+ });
56
+ test("each composed address resolves back to its own model", () => {
57
+ const view = composed();
58
+ for (const address of view.entitiesOfType(ANY_TYPE)) {
59
+ const resolved = view.registry.resolve(address);
60
+ expect(resolved).toBeDefined();
61
+ expect(resolved?.expressID).toBe(address.expressID);
62
+ }
63
+ });
64
+ test("an empty registry composes to nothing", () => {
65
+ const view = new ComposedModelSkeleton(new ModelRegistry(new SharedByteBudget(10)));
66
+ expect([...view.entitiesOfType(ANY_TYPE)]).toEqual([]);
67
+ expect(view.countOfType(ANY_TYPE)).toBe(0);
68
+ });
69
+ });
@@ -0,0 +1,87 @@
1
+ import { EntityAddress } from "./model_uri.js";
2
+ import { SharedByteBudget } from "./shared_byte_budget.js";
3
+ /**
4
+ * A model resolved from the registry, paired with the express ID an address
5
+ * selected within it.
6
+ */
7
+ export interface ResolvedEntity<H> {
8
+ model: H;
9
+ expressID: number;
10
+ }
11
+ /**
12
+ * The set of open models in one browser session (M5 / design S3).
13
+ *
14
+ * Federation is a registry of loader instances keyed by **model URI**, all
15
+ * sharing one {@link SharedByteBudget} so total resident memory is bounded per
16
+ * browser rather than per model. An {@link EntityAddress} (`modelURI#expressID`)
17
+ * resolves through here to the live model handle; a URI with no registered
18
+ * model is the signal to open a sibling loader (its own sidecar/stream) and
19
+ * register it.
20
+ *
21
+ * The registry is deliberately generic over the model handle `H` — it owns
22
+ * identity, resolution and the shared budget, not the loader. The streaming
23
+ * loader registers its model instance; higher layers (the composed skeleton,
24
+ * the UI link layer) query through the same addresses.
25
+ */
26
+ export declare class ModelRegistry<H> {
27
+ private readonly byURI_;
28
+ private readonly budget_;
29
+ /**
30
+ * @param budget The shared per-browser byte budget every registered model
31
+ * draws from.
32
+ */
33
+ constructor(budget: SharedByteBudget);
34
+ /**
35
+ * @return {SharedByteBudget} The shared budget all registered models draw
36
+ * from.
37
+ */
38
+ get budget(): SharedByteBudget;
39
+ /**
40
+ * @return {number} The number of registered models.
41
+ */
42
+ get size(): number;
43
+ /**
44
+ * @return {IterableIterator<string>} The URIs of all registered models.
45
+ */
46
+ uris(): IterableIterator<string>;
47
+ /**
48
+ * @return {IterableIterator<H>} The registered model handles.
49
+ */
50
+ models(): IterableIterator<H>;
51
+ /**
52
+ * Register (or replace) the model for a URI.
53
+ *
54
+ * @param uri The model URI (no `#` fragment).
55
+ * @param model The model handle.
56
+ */
57
+ register(uri: string, model: H): void;
58
+ /**
59
+ * Remove a model from the registry.
60
+ *
61
+ * @param uri The model URI.
62
+ * @return {boolean} True if a model was removed.
63
+ */
64
+ unregister(uri: string): boolean;
65
+ /**
66
+ * @param uri The model URI.
67
+ * @return {boolean} True if a model is registered for the URI.
68
+ */
69
+ has(uri: string): boolean;
70
+ /**
71
+ * @param uri The model URI.
72
+ * @return {H | undefined} The registered model, or undefined.
73
+ */
74
+ get(uri: string): H | undefined;
75
+ /**
76
+ * Resolve an entity address to its model + express ID. Returns undefined
77
+ * when the address's model isn't registered — the caller's cue to open and
78
+ * register it, then resolve again.
79
+ *
80
+ * @param address The entity address, as a URI string or a parsed
81
+ * {@link EntityAddress}.
82
+ * @return {ResolvedEntity | undefined} The resolved model + express ID, or
83
+ * undefined if the model isn't registered.
84
+ */
85
+ resolve(address: string | EntityAddress): ResolvedEntity<H> | undefined;
86
+ }
87
+ //# sourceMappingURL=model_registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model_registry.d.ts","sourceRoot":"","sources":["../../../src/core/model_registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAGvD;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,KAAK,EAAE,CAAC,CAAA;IACR,SAAS,EAAE,MAAM,CAAA;CAClB;AAGD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,aAAa,CAAC,CAAC;IAE1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAE9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAE1C;;;OAGG;gBACU,MAAM,EAAE,gBAAgB;IAIrC;;;OAGG;IACH,IAAW,MAAM,IAAI,gBAAgB,CAEpC;IAED;;OAEG;IACH,IAAW,IAAI,IAAI,MAAM,CAExB;IAED;;OAEG;IACI,IAAI,IAAI,gBAAgB,CAAC,MAAM,CAAC;IAIvC;;OAEG;IACI,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC;IAIpC;;;;;OAKG;IACI,QAAQ,CAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAI,IAAI;IAS9C;;;;;OAKG;IACI,UAAU,CAAE,GAAG,EAAE,MAAM,GAAI,OAAO;IAIzC;;;OAGG;IACI,GAAG,CAAE,GAAG,EAAE,MAAM,GAAI,OAAO;IAIlC;;;OAGG;IACI,GAAG,CAAE,GAAG,EAAE,MAAM,GAAI,CAAC,GAAG,SAAS;IAIxC;;;;;;;;;OASG;IACI,OAAO,CAAE,OAAO,EAAE,MAAM,GAAG,aAAa,GAAI,cAAc,CAAC,CAAC,CAAC,GAAG,SAAS;CASjF"}
@@ -0,0 +1,101 @@
1
+ import { parseEntityAddress } from "./model_uri.js";
2
+ /**
3
+ * The set of open models in one browser session (M5 / design S3).
4
+ *
5
+ * Federation is a registry of loader instances keyed by **model URI**, all
6
+ * sharing one {@link SharedByteBudget} so total resident memory is bounded per
7
+ * browser rather than per model. An {@link EntityAddress} (`modelURI#expressID`)
8
+ * resolves through here to the live model handle; a URI with no registered
9
+ * model is the signal to open a sibling loader (its own sidecar/stream) and
10
+ * register it.
11
+ *
12
+ * The registry is deliberately generic over the model handle `H` — it owns
13
+ * identity, resolution and the shared budget, not the loader. The streaming
14
+ * loader registers its model instance; higher layers (the composed skeleton,
15
+ * the UI link layer) query through the same addresses.
16
+ */
17
+ export class ModelRegistry {
18
+ /**
19
+ * @param budget The shared per-browser byte budget every registered model
20
+ * draws from.
21
+ */
22
+ constructor(budget) {
23
+ this.byURI_ = new Map();
24
+ this.budget_ = budget;
25
+ }
26
+ /**
27
+ * @return {SharedByteBudget} The shared budget all registered models draw
28
+ * from.
29
+ */
30
+ get budget() {
31
+ return this.budget_;
32
+ }
33
+ /**
34
+ * @return {number} The number of registered models.
35
+ */
36
+ get size() {
37
+ return this.byURI_.size;
38
+ }
39
+ /**
40
+ * @return {IterableIterator<string>} The URIs of all registered models.
41
+ */
42
+ uris() {
43
+ return this.byURI_.keys();
44
+ }
45
+ /**
46
+ * @return {IterableIterator<H>} The registered model handles.
47
+ */
48
+ models() {
49
+ return this.byURI_.values();
50
+ }
51
+ /**
52
+ * Register (or replace) the model for a URI.
53
+ *
54
+ * @param uri The model URI (no `#` fragment).
55
+ * @param model The model handle.
56
+ */
57
+ register(uri, model) {
58
+ if (uri.includes("#")) {
59
+ throw new Error(`Model URI must not contain '#': ${uri}`);
60
+ }
61
+ this.byURI_.set(uri, model);
62
+ }
63
+ /**
64
+ * Remove a model from the registry.
65
+ *
66
+ * @param uri The model URI.
67
+ * @return {boolean} True if a model was removed.
68
+ */
69
+ unregister(uri) {
70
+ return this.byURI_.delete(uri);
71
+ }
72
+ /**
73
+ * @param uri The model URI.
74
+ * @return {boolean} True if a model is registered for the URI.
75
+ */
76
+ has(uri) {
77
+ return this.byURI_.has(uri);
78
+ }
79
+ /**
80
+ * @param uri The model URI.
81
+ * @return {H | undefined} The registered model, or undefined.
82
+ */
83
+ get(uri) {
84
+ return this.byURI_.get(uri);
85
+ }
86
+ /**
87
+ * Resolve an entity address to its model + express ID. Returns undefined
88
+ * when the address's model isn't registered — the caller's cue to open and
89
+ * register it, then resolve again.
90
+ *
91
+ * @param address The entity address, as a URI string or a parsed
92
+ * {@link EntityAddress}.
93
+ * @return {ResolvedEntity | undefined} The resolved model + express ID, or
94
+ * undefined if the model isn't registered.
95
+ */
96
+ resolve(address) {
97
+ const { modelURI, expressID } = typeof address === "string" ? parseEntityAddress(address) : address;
98
+ const model = this.byURI_.get(modelURI);
99
+ return model === void 0 ? void 0 : { model, expressID };
100
+ }
101
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model_registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model_registry.test.d.ts","sourceRoot":"","sources":["../../../src/core/model_registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M5: the registry keys open models by URI, resolves universal addresses to
3
+ // the live model, and hands every model the one shared budget.
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import { ModelRegistry } from "./model_registry.js";
6
+ import { SharedByteBudget } from "./shared_byte_budget.js";
7
+ describe("ModelRegistry", () => {
8
+ /**
9
+ * A registry with two registered fake models.
10
+ *
11
+ * @return {ModelRegistry} The populated registry.
12
+ */
13
+ function twoModels() {
14
+ const registry = new ModelRegistry(new SharedByteBudget(1000));
15
+ registry.register("https://ex.com/a.ifc", { uri: "a" });
16
+ registry.register("https://ex.com/b.ifc", { uri: "b" });
17
+ return registry;
18
+ }
19
+ test("registers, looks up and counts models", () => {
20
+ const registry = twoModels();
21
+ expect(registry.size).toBe(2);
22
+ expect(registry.has("https://ex.com/a.ifc")).toBe(true);
23
+ expect(registry.get("https://ex.com/b.ifc")?.uri).toBe("b");
24
+ expect([...registry.uris()].sort()).toEqual(["https://ex.com/a.ifc", "https://ex.com/b.ifc"]);
25
+ });
26
+ test("resolves an address string to the model and express ID", () => {
27
+ const registry = twoModels();
28
+ const resolved = registry.resolve("https://ex.com/b.ifc#42");
29
+ expect(resolved?.model.uri).toBe("b");
30
+ expect(resolved?.expressID).toBe(42);
31
+ });
32
+ test("resolves a pre-parsed address object too", () => {
33
+ const registry = twoModels();
34
+ const resolved = registry.resolve({ modelURI: "https://ex.com/a.ifc", expressID: 7 });
35
+ expect(resolved?.model.uri).toBe("a");
36
+ expect(resolved?.expressID).toBe(7);
37
+ });
38
+ test("returns undefined for an unregistered model (open-then-retry cue)", () => {
39
+ const registry = twoModels();
40
+ expect(registry.resolve("https://ex.com/missing.ifc#1"))
41
+ .toBeUndefined();
42
+ });
43
+ test("unregister removes a model", () => {
44
+ const registry = twoModels();
45
+ expect(registry.unregister("https://ex.com/a.ifc")).toBe(true);
46
+ expect(registry.has("https://ex.com/a.ifc")).toBe(false);
47
+ expect(registry.size).toBe(1);
48
+ });
49
+ test("rejects registering a URI with a fragment", () => {
50
+ const registry = new ModelRegistry(new SharedByteBudget(10));
51
+ expect(() => registry.register("a.ifc#1", { uri: "a" })).toThrow(/#/);
52
+ });
53
+ test("exposes the one shared budget every model draws from", () => {
54
+ const budget = new SharedByteBudget(1000);
55
+ const registry = new ModelRegistry(budget);
56
+ expect(registry.budget).toBe(budget);
57
+ registry.budget.reserve(400);
58
+ expect(registry.budget.available).toBe(600);
59
+ });
60
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cross-model entity addressing (M5 / design S3).
3
+ *
4
+ * Federation turns "thousands of cross-referenced files" from a loader problem
5
+ * into an addressing problem: every entity has a **universal address** of a
6
+ * model URI plus an express ID — `https://…/part-B.ifc#4022`. The engine never
7
+ * holds all the files; it holds a registry of models keyed by URI (see
8
+ * {@link ModelRegistry}) and resolves an address to a (model, expressID) pair
9
+ * on demand, opening a sibling loader for a URI it hasn't seen.
10
+ *
11
+ * The URI is opaque to the engine except for its `#expressID` fragment: the
12
+ * scheme, host, path and any version identity live entirely in the model URI,
13
+ * so "many versions side by side" is expressible without an engine change.
14
+ */
15
+ /**
16
+ * A universal entity address: which model, and which express ID within it.
17
+ */
18
+ export interface EntityAddress {
19
+ /** The model's URI (everything left of the `#expressID` fragment). */
20
+ modelURI: string;
21
+ /** The entity's express ID within that model. */
22
+ expressID: number;
23
+ }
24
+ /**
25
+ * Format a model URI + express ID as a single entity address URI.
26
+ *
27
+ * @param modelURI The model URI (must not itself contain a `#`).
28
+ * @param expressID The express ID (a non-negative integer).
29
+ * @return {string} The `modelURI#expressID` address.
30
+ */
31
+ export declare function formatEntityAddress(modelURI: string, expressID: number): string;
32
+ /**
33
+ * Parse an entity address URI into its model URI and express ID.
34
+ *
35
+ * The split is on the **last** `#`, so a fragment is required; the model URI
36
+ * is everything before it. A missing or non-integer fragment is an error —
37
+ * an address without an express ID isn't an entity address.
38
+ *
39
+ * @param uri The `modelURI#expressID` address.
40
+ * @return {EntityAddress} The parsed address.
41
+ */
42
+ export declare function parseEntityAddress(uri: string): EntityAddress;
43
+ /**
44
+ * Resolve a (possibly relative) reference URI against a base model URI, so an
45
+ * `IfcExternalReference.Location` like `../shared/grid.ifc` becomes an absolute
46
+ * model URI addressable in the registry. Absolute references (with a scheme,
47
+ * or protocol-relative) pass through unchanged.
48
+ *
49
+ * Uses the standard URL resolver when the base is absolute; falls back to a
50
+ * plain path join when the base has no scheme (e.g. a bare filename in tests).
51
+ *
52
+ * @param base The base model URI the reference was found in.
53
+ * @param reference The reference URI (absolute or relative).
54
+ * @return {string} The resolved absolute reference URI.
55
+ */
56
+ export declare function resolveReference(base: string, reference: string): string;
57
+ //# sourceMappingURL=model_uri.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model_uri.d.ts","sourceRoot":"","sources":["../../../src/core/model_uri.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;GAEG;AACH,MAAM,WAAW,aAAa;IAE5B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAA;IAEhB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;CAClB;AAOD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAI,MAAM,CAWjF;AAGD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAE,GAAG,EAAE,MAAM,GAAI,aAAa,CAgB/D;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAI,MAAM,CAgB1E"}