@bldrs-ai/conway 1.404.1241 → 1.408.1248
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/compiled/examples/browser-bundled.cjs +1 -1
- package/compiled/examples/cli-bundled.cjs +1 -1
- package/compiled/examples/cli-step-bundled.cjs +1 -1
- package/compiled/examples/validator-bundled.cjs +1 -1
- package/compiled/src/core/composed_model_skeleton.d.ts +63 -0
- package/compiled/src/core/composed_model_skeleton.d.ts.map +1 -0
- package/compiled/src/core/composed_model_skeleton.js +66 -0
- package/compiled/src/core/composed_model_skeleton.test.d.ts +2 -0
- package/compiled/src/core/composed_model_skeleton.test.d.ts.map +1 -0
- package/compiled/src/core/composed_model_skeleton.test.js +69 -0
- package/compiled/src/core/geometry_tile_pool.d.ts +99 -0
- package/compiled/src/core/geometry_tile_pool.d.ts.map +1 -0
- package/compiled/src/core/geometry_tile_pool.js +104 -0
- package/compiled/src/core/geometry_tile_pool.test.d.ts +2 -0
- package/compiled/src/core/geometry_tile_pool.test.d.ts.map +1 -0
- package/compiled/src/core/geometry_tile_pool.test.js +180 -0
- package/compiled/src/core/mem/chunked_pool.d.ts +116 -0
- package/compiled/src/core/mem/chunked_pool.d.ts.map +1 -0
- package/compiled/src/core/mem/chunked_pool.js +149 -0
- package/compiled/src/core/mem/chunked_pool.test.d.ts +2 -0
- package/compiled/src/core/mem/chunked_pool.test.d.ts.map +1 -0
- package/compiled/src/core/mem/chunked_pool.test.js +63 -0
- package/compiled/src/core/mem/shared_asset_pool.d.ts +82 -0
- package/compiled/src/core/mem/shared_asset_pool.d.ts.map +1 -0
- package/compiled/src/core/mem/shared_asset_pool.js +113 -0
- package/compiled/src/core/mem/shared_asset_pool.test.d.ts +2 -0
- package/compiled/src/core/mem/shared_asset_pool.test.d.ts.map +1 -0
- package/compiled/src/core/mem/shared_asset_pool.test.js +49 -0
- package/compiled/src/core/model_registry.d.ts +87 -0
- package/compiled/src/core/model_registry.d.ts.map +1 -0
- package/compiled/src/core/model_registry.js +101 -0
- package/compiled/src/core/model_registry.test.d.ts +2 -0
- package/compiled/src/core/model_registry.test.d.ts.map +1 -0
- package/compiled/src/core/model_registry.test.js +60 -0
- package/compiled/src/core/model_uri.d.ts +57 -0
- package/compiled/src/core/model_uri.d.ts.map +1 -0
- package/compiled/src/core/model_uri.js +101 -0
- package/compiled/src/core/model_uri.test.d.ts +2 -0
- package/compiled/src/core/model_uri.test.d.ts.map +1 -0
- package/compiled/src/core/model_uri.test.js +49 -0
- package/compiled/src/core/shared_byte_budget.d.ts +60 -0
- package/compiled/src/core/shared_byte_budget.d.ts.map +1 -0
- package/compiled/src/core/shared_byte_budget.js +85 -0
- package/compiled/src/core/shared_byte_budget.test.d.ts +2 -0
- package/compiled/src/core/shared_byte_budget.test.d.ts.map +1 -0
- package/compiled/src/core/shared_byte_budget.test.js +55 -0
- package/compiled/src/step/parsing/cross_reference_registry.d.ts +95 -0
- package/compiled/src/step/parsing/cross_reference_registry.d.ts.map +1 -0
- package/compiled/src/step/parsing/cross_reference_registry.js +91 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.d.ts +2 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.d.ts.map +1 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.js +66 -0
- package/compiled/src/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- 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.
|
|
33
|
+
var versionString = "Conway v1.408.1248";
|
|
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.
|
|
14968
|
+
var versionString = "Conway v1.408.1248";
|
|
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.
|
|
15946
|
+
var versionString = "Conway v1.408.1248";
|
|
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.
|
|
947
|
+
var versionString = "Conway v1.408.1248";
|
|
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 @@
|
|
|
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,99 @@
|
|
|
1
|
+
import { GeometryTiles } from "./demand_geometry_queue.js";
|
|
2
|
+
import { ChunkedPool } from "./mem/chunked_pool.js";
|
|
3
|
+
import { SharedAssetPool } from "./mem/shared_asset_pool.js";
|
|
4
|
+
/**
|
|
5
|
+
* One shareable geometry payload an instance needs resident: the asset's
|
|
6
|
+
* identity and its byte size. For IFC, the asset is a representation's
|
|
7
|
+
* tessellated geometry and `byteSize` comes from the wasm-side extract.
|
|
8
|
+
*/
|
|
9
|
+
export interface GeometryAsset<AssetID> {
|
|
10
|
+
assetID: AssetID;
|
|
11
|
+
byteSize: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The domain half the tile pool delegates to: what assets an instance is made
|
|
15
|
+
* of, and how to materialise / discard one asset's payload. This is the seam
|
|
16
|
+
* the production conway-geom surface implements (extract → tessellate into
|
|
17
|
+
* the asset's chunks; discard on last release); tests implement it with
|
|
18
|
+
* recording mocks.
|
|
19
|
+
*/
|
|
20
|
+
export interface InstanceAssetSource<AssetID> {
|
|
21
|
+
/**
|
|
22
|
+
* The assets an instance requires resident, with their byte sizes. For a
|
|
23
|
+
* simple product this is one asset; mapped/instanced representations return
|
|
24
|
+
* the same assetID from many instances — that sharing is the point.
|
|
25
|
+
*
|
|
26
|
+
* @param instanceID The instance (product local ID in the geometry case).
|
|
27
|
+
* @return {GeometryAsset<AssetID>[]} The instance's assets.
|
|
28
|
+
*/
|
|
29
|
+
assetsOf(instanceID: number): GeometryAsset<AssetID>[];
|
|
30
|
+
/**
|
|
31
|
+
* Materialise an asset's payload (called exactly once per residency, on
|
|
32
|
+
* the 0→1 reference). In production: run the wasm extract/tessellate and
|
|
33
|
+
* write the result into the asset's pool chunks.
|
|
34
|
+
*
|
|
35
|
+
* @param assetID The asset to materialise.
|
|
36
|
+
*/
|
|
37
|
+
materialize(assetID: AssetID): void;
|
|
38
|
+
/**
|
|
39
|
+
* Discard an asset's payload (called exactly once per residency, on the
|
|
40
|
+
* 1→0 reference, after its chunks are already back in the pool).
|
|
41
|
+
*
|
|
42
|
+
* @param assetID The asset to discard.
|
|
43
|
+
*/
|
|
44
|
+
discard(assetID: AssetID): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The geometry narrowing of the general memory system (`src/core/mem/`): a
|
|
48
|
+
* {@link GeometryTiles} backend where a product's "tile" is a set of
|
|
49
|
+
* refcounted, chunk-resident **assets** in a {@link SharedAssetPool}.
|
|
50
|
+
*
|
|
51
|
+
* Composed with {@link DemandGeometryQueue} this completes the M3 resident
|
|
52
|
+
* design: the queue owns demand ordering and the logical budget; this pool
|
|
53
|
+
* owns physical residency. Two accounting views, one invariant:
|
|
54
|
+
*
|
|
55
|
+
* - **Logical (queue-side):** `extract` charges an instance the full
|
|
56
|
+
* chunk-rounded cost of *every* asset it references, shared or not. Sharing
|
|
57
|
+
* is thus double-charged logically — deliberately conservative: summed
|
|
58
|
+
* logical charges always cover physical use, so with the queue's budget set
|
|
59
|
+
* to the pool's budget, an acquire can never fail mid-extract. (Heavy
|
|
60
|
+
* sharing under-utilises the logical budget; the production wiring can
|
|
61
|
+
* widen the queue budget once measured. The safe direction first.)
|
|
62
|
+
* - **Physical (pool-side):** `bytesInUse` counts real chunks. Shared assets
|
|
63
|
+
* are stored once, refcounted, and freed only on the last release — so
|
|
64
|
+
* evicting product A never discards the representation product B still
|
|
65
|
+
* renders (the mapped-item correctness rule, held structurally).
|
|
66
|
+
*/
|
|
67
|
+
export declare class GeometryTilePool<AssetID> implements GeometryTiles {
|
|
68
|
+
private readonly assets_;
|
|
69
|
+
private readonly source_;
|
|
70
|
+
/** Assets each extracted instance holds references on. */
|
|
71
|
+
private readonly held_;
|
|
72
|
+
/**
|
|
73
|
+
* @param pool The chunk pool residents live in.
|
|
74
|
+
* @param source The domain half (asset composition + materialise/discard).
|
|
75
|
+
*/
|
|
76
|
+
constructor(pool: ChunkedPool, source: InstanceAssetSource<AssetID>);
|
|
77
|
+
/**
|
|
78
|
+
* @return {SharedAssetPool<AssetID>} The refcounted asset layer (physical
|
|
79
|
+
* accounting lives here / on its pool).
|
|
80
|
+
*/
|
|
81
|
+
get assets(): SharedAssetPool<AssetID>;
|
|
82
|
+
/**
|
|
83
|
+
* Materialise an instance's tile: take a reference on each of its assets,
|
|
84
|
+
* materialising the ones not yet resident.
|
|
85
|
+
*
|
|
86
|
+
* @param instanceID The instance (product local ID).
|
|
87
|
+
* @return {number} The instance's logical byte cost (chunk-rounded, every
|
|
88
|
+
* asset fully charged — see class docs for why sharing is double-charged).
|
|
89
|
+
*/
|
|
90
|
+
extract(instanceID: number): number;
|
|
91
|
+
/**
|
|
92
|
+
* Release an instance's tile: drop the reference on each of its assets,
|
|
93
|
+
* discarding those this instance was the last holder of.
|
|
94
|
+
*
|
|
95
|
+
* @param instanceID The instance (product local ID).
|
|
96
|
+
*/
|
|
97
|
+
release(instanceID: number): void;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=geometry_tile_pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry_tile_pool.d.ts","sourceRoot":"","sources":["../../../src/core/geometry_tile_pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAGD;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB,CAAC,OAAO;IAE1C;;;;;;;OAOG;IACH,QAAQ,CAAE,UAAU,EAAE,MAAM,GAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAA;IAExD;;;;;;OAMG;IACH,WAAW,CAAE,OAAO,EAAE,OAAO,GAAI,IAAI,CAAA;IAErC;;;;;OAKG;IACH,OAAO,CAAE,OAAO,EAAE,OAAO,GAAI,IAAI,CAAA;CAClC;AAGD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB,CAAC,OAAO,CAAE,YAAW,aAAa;IAE7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAEtD,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8C;IAEpE;;;OAGG;gBACU,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,mBAAmB,CAAC,OAAO,CAAC;IAKpE;;;OAGG;IACH,IAAW,MAAM,IAAI,eAAe,CAAC,OAAO,CAAC,CAE5C;IAED;;;;;;;OAOG;IACI,OAAO,CAAE,UAAU,EAAE,MAAM,GAAI,MAAM;IAoD5C;;;;;OAKG;IACI,OAAO,CAAE,UAAU,EAAE,MAAM,GAAI,IAAI;CAgB3C"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { SharedAssetPool } from "./mem/shared_asset_pool.js";
|
|
2
|
+
/**
|
|
3
|
+
* The geometry narrowing of the general memory system (`src/core/mem/`): a
|
|
4
|
+
* {@link GeometryTiles} backend where a product's "tile" is a set of
|
|
5
|
+
* refcounted, chunk-resident **assets** in a {@link SharedAssetPool}.
|
|
6
|
+
*
|
|
7
|
+
* Composed with {@link DemandGeometryQueue} this completes the M3 resident
|
|
8
|
+
* design: the queue owns demand ordering and the logical budget; this pool
|
|
9
|
+
* owns physical residency. Two accounting views, one invariant:
|
|
10
|
+
*
|
|
11
|
+
* - **Logical (queue-side):** `extract` charges an instance the full
|
|
12
|
+
* chunk-rounded cost of *every* asset it references, shared or not. Sharing
|
|
13
|
+
* is thus double-charged logically — deliberately conservative: summed
|
|
14
|
+
* logical charges always cover physical use, so with the queue's budget set
|
|
15
|
+
* to the pool's budget, an acquire can never fail mid-extract. (Heavy
|
|
16
|
+
* sharing under-utilises the logical budget; the production wiring can
|
|
17
|
+
* widen the queue budget once measured. The safe direction first.)
|
|
18
|
+
* - **Physical (pool-side):** `bytesInUse` counts real chunks. Shared assets
|
|
19
|
+
* are stored once, refcounted, and freed only on the last release — so
|
|
20
|
+
* evicting product A never discards the representation product B still
|
|
21
|
+
* renders (the mapped-item correctness rule, held structurally).
|
|
22
|
+
*/
|
|
23
|
+
export class GeometryTilePool {
|
|
24
|
+
/**
|
|
25
|
+
* @param pool The chunk pool residents live in.
|
|
26
|
+
* @param source The domain half (asset composition + materialise/discard).
|
|
27
|
+
*/
|
|
28
|
+
constructor(pool, source) {
|
|
29
|
+
/** Assets each extracted instance holds references on. */
|
|
30
|
+
this.held_ = new Map();
|
|
31
|
+
this.assets_ = new SharedAssetPool(pool);
|
|
32
|
+
this.source_ = source;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @return {SharedAssetPool<AssetID>} The refcounted asset layer (physical
|
|
36
|
+
* accounting lives here / on its pool).
|
|
37
|
+
*/
|
|
38
|
+
get assets() {
|
|
39
|
+
return this.assets_;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Materialise an instance's tile: take a reference on each of its assets,
|
|
43
|
+
* materialising the ones not yet resident.
|
|
44
|
+
*
|
|
45
|
+
* @param instanceID The instance (product local ID).
|
|
46
|
+
* @return {number} The instance's logical byte cost (chunk-rounded, every
|
|
47
|
+
* asset fully charged — see class docs for why sharing is double-charged).
|
|
48
|
+
*/
|
|
49
|
+
extract(instanceID) {
|
|
50
|
+
if (this.held_.has(instanceID)) {
|
|
51
|
+
throw new Error(`Instance ${instanceID} already extracted`);
|
|
52
|
+
}
|
|
53
|
+
const assets = this.source_.assetsOf(instanceID);
|
|
54
|
+
const pool = this.assets_.pool;
|
|
55
|
+
// Validate and price every asset BEFORE taking any references, so a bad
|
|
56
|
+
// byte size (a source bug) throws at zero state instead of mid-extract
|
|
57
|
+
// with references already taken. chunkRound throws on negatives.
|
|
58
|
+
let logicalBytes = 0;
|
|
59
|
+
for (const asset of assets) {
|
|
60
|
+
logicalBytes += pool.chunkRound(asset.byteSize);
|
|
61
|
+
}
|
|
62
|
+
for (const asset of assets) {
|
|
63
|
+
const { retained, wasAbsent } = this.assets_.retain(asset.assetID, asset.byteSize);
|
|
64
|
+
if (!retained) {
|
|
65
|
+
// Unwind references already taken so a failed extract has no effect —
|
|
66
|
+
// keeping the materialize/discard pairing: an asset this unwind takes
|
|
67
|
+
// to zero references must be discarded like any other last release.
|
|
68
|
+
for (const taken of assets) {
|
|
69
|
+
if (taken === asset) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (this.assets_.release(taken.assetID)) {
|
|
73
|
+
this.source_.discard(taken.assetID);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Geometry pool exhausted materialising instance ${instanceID} — ` +
|
|
77
|
+
"the scheduler budget must not exceed the pool budget");
|
|
78
|
+
}
|
|
79
|
+
if (wasAbsent) {
|
|
80
|
+
this.source_.materialize(asset.assetID);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.held_.set(instanceID, assets);
|
|
84
|
+
return logicalBytes;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Release an instance's tile: drop the reference on each of its assets,
|
|
88
|
+
* discarding those this instance was the last holder of.
|
|
89
|
+
*
|
|
90
|
+
* @param instanceID The instance (product local ID).
|
|
91
|
+
*/
|
|
92
|
+
release(instanceID) {
|
|
93
|
+
const assets = this.held_.get(instanceID);
|
|
94
|
+
if (assets === void 0) {
|
|
95
|
+
throw new Error(`Release of un-extracted instance ${instanceID}`);
|
|
96
|
+
}
|
|
97
|
+
this.held_.delete(instanceID);
|
|
98
|
+
for (const asset of assets) {
|
|
99
|
+
if (this.assets_.release(asset.assetID)) {
|
|
100
|
+
this.source_.discard(asset.assetID);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry_tile_pool.test.d.ts","sourceRoot":"","sources":["../../../src/core/geometry_tile_pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// M3 production design: the geometry narrowing of the mem system, alone and
|
|
3
|
+
// composed with DemandGeometryQueue. The load-bearing assertions: shared
|
|
4
|
+
// representations are stored once and never freed while any product still
|
|
5
|
+
// holds them, and the queue's logical budget always covers the pool's
|
|
6
|
+
// physical use (so an extract can never blow the pool mid-flight).
|
|
7
|
+
import { describe, expect, test } from "@jest/globals";
|
|
8
|
+
import { DemandGeometryQueue } from "./demand_geometry_queue.js";
|
|
9
|
+
import { GeometryTilePool } from "./geometry_tile_pool.js";
|
|
10
|
+
import { ChunkedPool } from "./mem/chunked_pool.js";
|
|
11
|
+
/**
|
|
12
|
+
* A recording mock of the domain half: instance → assets from a fixture map,
|
|
13
|
+
* with materialize/discard calls captured.
|
|
14
|
+
*
|
|
15
|
+
* @param composition Instance → assets fixture.
|
|
16
|
+
* @return {object} `{ source, materialized, discarded }`.
|
|
17
|
+
*/
|
|
18
|
+
function mockSource(composition) {
|
|
19
|
+
const materialized = [];
|
|
20
|
+
const discarded = [];
|
|
21
|
+
const source = {
|
|
22
|
+
assetsOf: (instanceID) => composition[instanceID] ?? [],
|
|
23
|
+
materialize: (assetID) => {
|
|
24
|
+
materialized.push(assetID);
|
|
25
|
+
},
|
|
26
|
+
discard: (assetID) => {
|
|
27
|
+
discarded.push(assetID);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return { source, materialized, discarded };
|
|
31
|
+
}
|
|
32
|
+
describe("GeometryTilePool", () => {
|
|
33
|
+
test("a shared representation is materialised once and stored once", () => {
|
|
34
|
+
// Products 1 and 2 both instance rep-X (mapped item); 2 also has its own.
|
|
35
|
+
const { source, materialized } = mockSource({
|
|
36
|
+
1: [{ assetID: "rep-X", byteSize: 100 }],
|
|
37
|
+
2: [{ assetID: "rep-X", byteSize: 100 }, { assetID: "rep-Y", byteSize: 100 }],
|
|
38
|
+
});
|
|
39
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
40
|
+
tiles.extract(1);
|
|
41
|
+
tiles.extract(2);
|
|
42
|
+
expect(materialized).toEqual(["rep-X", "rep-Y"]);
|
|
43
|
+
// Physically: two assets, 200 bytes — not 300.
|
|
44
|
+
expect(tiles.assets.pool.bytesInUse).toBe(200);
|
|
45
|
+
expect(tiles.assets.refCountOf("rep-X")).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
test("evicting one product never discards a representation another still renders", () => {
|
|
48
|
+
const { source, discarded } = mockSource({
|
|
49
|
+
1: [{ assetID: "rep-X", byteSize: 100 }],
|
|
50
|
+
2: [{ assetID: "rep-X", byteSize: 100 }],
|
|
51
|
+
});
|
|
52
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
53
|
+
tiles.extract(1);
|
|
54
|
+
tiles.extract(2);
|
|
55
|
+
tiles.release(1);
|
|
56
|
+
expect(discarded).toEqual([]);
|
|
57
|
+
expect(tiles.assets.isResident("rep-X")).toBe(true);
|
|
58
|
+
tiles.release(2);
|
|
59
|
+
expect(discarded).toEqual(["rep-X"]);
|
|
60
|
+
expect(tiles.assets.pool.bytesInUse).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
test("extract charges the logical (chunk-rounded, fully-charged) cost", () => {
|
|
63
|
+
const { source } = mockSource({
|
|
64
|
+
1: [{ assetID: "a", byteSize: 150 }, { assetID: "b", byteSize: 10 }],
|
|
65
|
+
2: [{ assetID: "a", byteSize: 150 }],
|
|
66
|
+
});
|
|
67
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
68
|
+
// 150 → 200 rounded, 10 → 100 rounded.
|
|
69
|
+
expect(tiles.extract(1)).toBe(300);
|
|
70
|
+
// Sharing is deliberately double-charged logically (conservative)...
|
|
71
|
+
expect(tiles.extract(2)).toBe(200);
|
|
72
|
+
// ...while physical stays single-stored.
|
|
73
|
+
expect(tiles.assets.pool.bytesInUse).toBe(300);
|
|
74
|
+
});
|
|
75
|
+
test("a failed extract unwinds cleanly, discarding what it materialised", () => {
|
|
76
|
+
// Asset a fits; asset b cannot (pool holds 2 chunks, b needs 3). The
|
|
77
|
+
// extract must throw, and the unwind must keep the materialize/discard
|
|
78
|
+
// pairing for a (which it took to zero references) and leave the pool
|
|
79
|
+
// exactly as before — a failed extract has no effect.
|
|
80
|
+
const { source, materialized, discarded } = mockSource({
|
|
81
|
+
1: [{ assetID: "a", byteSize: 100 }, { assetID: "b", byteSize: 300 }],
|
|
82
|
+
});
|
|
83
|
+
const pool = new ChunkedPool(200, 100);
|
|
84
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
85
|
+
expect(() => tiles.extract(1)).toThrow(/exhausted/);
|
|
86
|
+
expect(materialized).toEqual(["a"]);
|
|
87
|
+
expect(discarded).toEqual(["a"]);
|
|
88
|
+
expect(pool.bytesInUse).toBe(0);
|
|
89
|
+
expect(tiles.assets.isResident("a")).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
test("a failed extract never discards an asset another instance still holds", () => {
|
|
92
|
+
// Instance 1 holds shared. Instance 2 wants shared + big; big fails.
|
|
93
|
+
// The unwind drops 2's reference on shared but must NOT discard it —
|
|
94
|
+
// instance 1 still renders it.
|
|
95
|
+
const { source, discarded } = mockSource({
|
|
96
|
+
1: [{ assetID: "shared", byteSize: 100 }],
|
|
97
|
+
2: [{ assetID: "shared", byteSize: 100 }, { assetID: "big", byteSize: 300 }],
|
|
98
|
+
});
|
|
99
|
+
const pool = new ChunkedPool(200, 100);
|
|
100
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
101
|
+
tiles.extract(1);
|
|
102
|
+
expect(() => tiles.extract(2)).toThrow(/exhausted/);
|
|
103
|
+
expect(discarded).toEqual([]);
|
|
104
|
+
expect(tiles.assets.isResident("shared")).toBe(true);
|
|
105
|
+
expect(tiles.assets.refCountOf("shared")).toBe(1);
|
|
106
|
+
expect(pool.bytesInUse).toBe(100);
|
|
107
|
+
});
|
|
108
|
+
test("double extract and unmatched release are errors", () => {
|
|
109
|
+
const { source } = mockSource({ 1: [{ assetID: "a", byteSize: 10 }] });
|
|
110
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
111
|
+
tiles.extract(1);
|
|
112
|
+
expect(() => tiles.extract(1)).toThrow(/already extracted/);
|
|
113
|
+
expect(() => tiles.release(2)).toThrow(/un-extracted/);
|
|
114
|
+
});
|
|
115
|
+
test("a malformed byte size throws before any state changes", () => {
|
|
116
|
+
// A buggy source returning a negative size must fail at zero state —
|
|
117
|
+
// no references taken, nothing materialised, pool untouched.
|
|
118
|
+
const { source, materialized } = mockSource({
|
|
119
|
+
1: [{ assetID: "ok", byteSize: 100 }, { assetID: "bad", byteSize: -5 }],
|
|
120
|
+
});
|
|
121
|
+
const pool = new ChunkedPool(1000, 100);
|
|
122
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
123
|
+
expect(() => tiles.extract(1)).toThrow(/Invalid byte size/);
|
|
124
|
+
expect(materialized).toEqual([]);
|
|
125
|
+
expect(pool.bytesInUse).toBe(0);
|
|
126
|
+
expect(tiles.assets.isResident("ok")).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
test("composed with the demand queue: budget holds, shared reps survive eviction", () => {
|
|
129
|
+
// 20 products all sharing one heavy rep, each with a light unique rep —
|
|
130
|
+
// the mapped-item shape (fixtures, repeated windows).
|
|
131
|
+
const composition = {};
|
|
132
|
+
for (let id = 0; id < 20; ++id) {
|
|
133
|
+
composition[id] = [
|
|
134
|
+
{ assetID: "shared-heavy", byteSize: 400 },
|
|
135
|
+
{ assetID: `unique-${id}`, byteSize: 100 },
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
const { source, materialized, discarded } = mockSource(composition);
|
|
139
|
+
const pool = new ChunkedPool(4000, 100);
|
|
140
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
141
|
+
// Queue budget == pool budget: the safety invariant under test.
|
|
142
|
+
const queue = new DemandGeometryQueue(tiles, 4000);
|
|
143
|
+
// Stream demand across all 20 products with rising priority.
|
|
144
|
+
for (let id = 0; id < 20; ++id) {
|
|
145
|
+
queue.request(id, id);
|
|
146
|
+
queue.pump();
|
|
147
|
+
// Physical use never exceeds the logical budget the queue enforces.
|
|
148
|
+
expect(pool.bytesInUse).toBeLessThanOrEqual(queue.stats.residentBytes);
|
|
149
|
+
expect(queue.stats.residentBytes).toBeLessThanOrEqual(4000);
|
|
150
|
+
}
|
|
151
|
+
// The shared rep was materialised exactly once across all that churn...
|
|
152
|
+
expect(materialized.filter((a) => a === "shared-heavy").length).toBe(1);
|
|
153
|
+
// ...and never discarded, because some product always held it.
|
|
154
|
+
expect(discarded).not.toContain("shared-heavy");
|
|
155
|
+
// Evicting everything finally discards it and empties the pool.
|
|
156
|
+
queue.evictAll();
|
|
157
|
+
expect(discarded).toContain("shared-heavy");
|
|
158
|
+
expect(pool.bytesInUse).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
test("composed queue at exact pool budget never exhausts the pool", () => {
|
|
161
|
+
// Every product unique (worst case for the invariant: no sharing slack).
|
|
162
|
+
const composition = {};
|
|
163
|
+
for (let id = 0; id < 50; ++id) {
|
|
164
|
+
composition[id] = [{ assetID: `u-${id}`, byteSize: 190 }];
|
|
165
|
+
}
|
|
166
|
+
const { source } = mockSource(composition);
|
|
167
|
+
const pool = new ChunkedPool(1000, 100);
|
|
168
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
169
|
+
const queue = new DemandGeometryQueue(tiles, 1000);
|
|
170
|
+
// 190 rounds to 200 → 5 tiles fit. Churn all 50 through; extract must
|
|
171
|
+
// never throw pool-exhausted because logical charges cover physical.
|
|
172
|
+
for (let id = 0; id < 50; ++id) {
|
|
173
|
+
queue.request(id, id);
|
|
174
|
+
queue.pump();
|
|
175
|
+
}
|
|
176
|
+
expect(queue.stats.residentCount).toBe(5);
|
|
177
|
+
expect(pool.bytesInUse).toBe(1000);
|
|
178
|
+
expect(pool.stats.failedAcquires).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|