@_linked/core 2.26.0 → 2.28.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +33 -0
  3. package/lib/esm/index.d.ts +9 -0
  4. package/lib/esm/index.js +8 -0
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/interfaces/IDataset.d.ts +15 -0
  7. package/lib/esm/interfaces/IDataset.js.map +1 -1
  8. package/lib/esm/interfaces/IFileStore.d.ts +6 -0
  9. package/lib/esm/interfaces/IFileStore.js.map +1 -1
  10. package/lib/esm/live/LiveQueryStore.d.ts +240 -0
  11. package/lib/esm/live/LiveQueryStore.js +729 -0
  12. package/lib/esm/live/LiveQueryStore.js.map +1 -0
  13. package/lib/esm/live/changes.d.ts +30 -0
  14. package/lib/esm/live/changes.js +30 -0
  15. package/lib/esm/live/changes.js.map +1 -0
  16. package/lib/esm/live/keys.d.ts +54 -0
  17. package/lib/esm/live/keys.js +112 -0
  18. package/lib/esm/live/keys.js.map +1 -0
  19. package/lib/esm/live/matcher.d.ts +43 -0
  20. package/lib/esm/live/matcher.js +115 -0
  21. package/lib/esm/live/matcher.js.map +1 -0
  22. package/lib/esm/live/registry.d.ts +24 -0
  23. package/lib/esm/live/registry.js +41 -0
  24. package/lib/esm/live/registry.js.map +1 -0
  25. package/lib/esm/live.d.ts +12 -0
  26. package/lib/esm/live.js +15 -0
  27. package/lib/esm/live.js.map +1 -0
  28. package/lib/esm/queries/AskBuilder.d.ts +10 -0
  29. package/lib/esm/queries/AskBuilder.js +14 -0
  30. package/lib/esm/queries/AskBuilder.js.map +1 -1
  31. package/lib/esm/queries/CountBuilder.d.ts +10 -0
  32. package/lib/esm/queries/CountBuilder.js +14 -0
  33. package/lib/esm/queries/CountBuilder.js.map +1 -1
  34. package/lib/esm/queries/QueryBuilder.d.ts +10 -0
  35. package/lib/esm/queries/QueryBuilder.js +14 -0
  36. package/lib/esm/queries/QueryBuilder.js.map +1 -1
  37. package/lib/esm/queries/QueryContext.js +9 -2
  38. package/lib/esm/queries/QueryContext.js.map +1 -1
  39. package/lib/esm/queries/SelectQuery.d.ts +8 -8
  40. package/lib/esm/queries/SelectQuery.js.map +1 -1
  41. package/lib/esm/queries/queryDependencies.d.ts +58 -0
  42. package/lib/esm/queries/queryDependencies.js +494 -0
  43. package/lib/esm/queries/queryDependencies.js.map +1 -0
  44. package/lib/esm/queries/queryDispatch.d.ts +46 -0
  45. package/lib/esm/queries/queryDispatch.js +88 -3
  46. package/lib/esm/queries/queryDispatch.js.map +1 -1
  47. package/lib/esm/shapes/nodeShapeData.d.ts +16 -0
  48. package/lib/esm/shapes/nodeShapeData.js +41 -1
  49. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  50. package/lib/esm/sparql/irToAlgebra.js +1 -32
  51. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  52. package/lib/esm/utils/LinkedStorage.d.ts +6 -0
  53. package/lib/esm/utils/LinkedStorage.js +32 -6
  54. package/lib/esm/utils/LinkedStorage.js.map +1 -1
  55. package/package.json +6 -1
@@ -14,6 +14,12 @@ export declare abstract class LinkedStorage {
14
14
  static getDefaultDataset(): IDataset;
15
15
  /** Set the default IDataset (catch-all for shapes with no explicit mapping). */
16
16
  static setDefaultDataset(dataset: IDataset): void;
17
+ /**
18
+ * Be told whenever the set of datasets changes (`setDefaultDataset`,
19
+ * `setDatasetForShapes`, `unsetDatasetForShape`). The live-query store uses
20
+ * this to pick up a dataset's change feed. Returns the unsubscribe function.
21
+ */
22
+ static onRoutingChanged(listener: () => void): () => void;
17
23
  /** Pin one or more shape classes to a specific IDataset implementer. */
18
24
  static setDatasetForShapes(dataset: IDataset, ...shapeClasses: Function[]): void;
19
25
  /**
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- var _a, _b;
10
+ var _a, _b, _c;
11
11
  import { CoreSet } from '../collections/CoreSet.js';
12
12
  import { setQueryDispatch, resolveExistence } from '../queries/queryDispatch.js';
13
13
  import { getShapeClass } from './ShapeClass.js';
@@ -41,7 +41,21 @@ linkedStorageGlobal.__linkedStorageInstanceCount =
41
41
  const routingState = ((_b = linkedStorageGlobal.__linkedStorageRouting) !== null && _b !== void 0 ? _b : (linkedStorageGlobal.__linkedStorageRouting = {
42
42
  defaultDataset: undefined,
43
43
  shapeToDataset: new Map(),
44
+ listeners: new Set(),
44
45
  }));
46
+ // An older copy of this module may have created the record without listeners.
47
+ (_c = routingState.listeners) !== null && _c !== void 0 ? _c : (routingState.listeners = new Set());
48
+ /** Tell routing-change listeners that the set of datasets may have changed. */
49
+ function notifyRoutingChanged() {
50
+ for (const listener of [...routingState.listeners]) {
51
+ try {
52
+ listener();
53
+ }
54
+ catch (err) {
55
+ console.error('[linked] routing-change listener failed', err);
56
+ }
57
+ }
58
+ }
45
59
  /** A shape class's IRI, or undefined for anything that is not a registered shape. */
46
60
  const shapeUriOf = (shapeClass) => {
47
61
  var _a;
@@ -75,6 +89,16 @@ export class LinkedStorage {
75
89
  updateQuery: (q) => this.updateQuery(q),
76
90
  deleteQuery: (q) => this.deleteQuery(q),
77
91
  });
92
+ notifyRoutingChanged();
93
+ }
94
+ /**
95
+ * Be told whenever the set of datasets changes (`setDefaultDataset`,
96
+ * `setDatasetForShapes`, `unsetDatasetForShape`). The live-query store uses
97
+ * this to pick up a dataset's change feed. Returns the unsubscribe function.
98
+ */
99
+ static onRoutingChanged(listener) {
100
+ routingState.listeners.add(listener);
101
+ return () => routingState.listeners.delete(listener);
78
102
  }
79
103
  /** Pin one or more shape classes to a specific IDataset implementer. */
80
104
  static setDatasetForShapes(dataset, ...shapeClasses) {
@@ -95,6 +119,7 @@ export class LinkedStorage {
95
119
  }
96
120
  routingState.shapeToDataset.set(shapeClass, dataset);
97
121
  });
122
+ notifyRoutingChanged();
98
123
  }
99
124
  /**
100
125
  * Remove the pin for a shape, by class or by IRI.
@@ -107,13 +132,14 @@ export class LinkedStorage {
107
132
  static unsetDatasetForShape(shape) {
108
133
  const uri = typeof shape === 'string' ? shape : shapeUriOf(shape);
109
134
  routingState.shapeToDataset.delete(shape);
110
- if (!uri)
111
- return;
112
- for (const pinned of [...routingState.shapeToDataset.keys()]) {
113
- if (shapeUriOf(pinned) === uri) {
114
- routingState.shapeToDataset.delete(pinned);
135
+ if (uri) {
136
+ for (const pinned of [...routingState.shapeToDataset.keys()]) {
137
+ if (shapeUriOf(pinned) === uri) {
138
+ routingState.shapeToDataset.delete(pinned);
139
+ }
115
140
  }
116
141
  }
142
+ notifyRoutingChanged();
117
143
  }
118
144
  /** Every IDataset known to the primary router (default + all pinned). */
119
145
  static getDatasets() {
@@ -1 +1 @@
1
- {"version":3,"file":"LinkedStorage.js","sourceRoot":"","sources":["../../../src/utils/LinkedStorage.ts"],"names":[],"mappings":"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","sourcesContent":["import {CoreSet} from '../collections/CoreSet.js';\nimport type {IDataset} from '../interfaces/IDataset.js';\nimport type {SelectQuery} from '../queries/SelectQuery.js';\nimport type {AskQuery} from '../queries/AskQuery.js';\nimport type {CountQuery} from '../queries/CountQuery.js';\nimport type {CreateQuery} from '../queries/CreateQuery.js';\nimport type {UpdateQuery} from '../queries/UpdateQuery.js';\nimport type {DeleteQuery, DeleteResponse} from '../queries/DeleteQuery.js';\nimport {setQueryDispatch, resolveExistence} from '../queries/queryDispatch.js';\nimport {getShapeClass} from './ShapeClass.js';\nimport type {NodeShapeData} from '../shapes/SHACL.js';\n\n// plan-011 — count physical evaluations of THIS module on the one shared\n// global object. With the single-loader fix there should be exactly one copy;\n// the single-instance guard in backend.ts reports this count if storage config\n// ever lands on a different copy. Deliberately avoids Date/Math.random\n// (unavailable / non-deterministic per repo constraints).\nconst linkedStorageGlobal: any =\n typeof globalThis !== 'undefined' ? globalThis : ({} as any);\nlinkedStorageGlobal.__linkedStorageInstanceCount =\n (linkedStorageGlobal.__linkedStorageInstanceCount ?? 0) + 1;\n\n/**\n * Primary routing layer (arch-04 §The IDataset abstraction).\n *\n * Resolves an incoming Linked Query to the IDataset that should handle it,\n * based on the query's target shape. Composite IDatasets (gateways, routers,\n * forwarders, resolvers) plug in here just like storage IDatasets do.\n *\n * The \"Dataset\" naming aligns with the IDataset contract. Earlier \"Store\"\n * names were renamed in phase-1 — see docs/plans/002-phase-1-create-user-project-flow.md.\n */\n/**\n * The routing table lives on the shared global, beside the shape registries.\n *\n * It has to, for the same reason they do: a module can evaluate more than once in a\n * process, and a class static then exists once per copy. Leaving these two here while\n * the registries were shared was worse than either extreme — a second copy answered\n * `isInitialised()` with false and `getDatasetForShapeClass()` with the default, while\n * reporting a fully populated shape registry.\n */\nconst routingState: {\n defaultDataset?: IDataset;\n shapeToDataset: Map<Function, IDataset>;\n} = (linkedStorageGlobal.__linkedStorageRouting ??= {\n defaultDataset: undefined,\n shapeToDataset: new Map(),\n});\n\n/** A shape class's IRI, or undefined for anything that is not a registered shape. */\nconst shapeUriOf = (shapeClass?: Function | null): string | undefined => {\n const uri = (shapeClass as {shape?: {id?: unknown}} | null | undefined)?.shape?.id;\n return typeof uri === 'string' && uri ? uri : undefined;\n};\n\nexport abstract class LinkedStorage {\n\n /** plan-011 — how many physical copies of this module have evaluated. */\n static getLoadedInstanceCount(): number {\n return linkedStorageGlobal.__linkedStorageInstanceCount ?? 0;\n }\n\n static isInitialised() {\n return !!routingState.defaultDataset;\n }\n\n /** The catch-all IDataset for shapes with no explicit mapping. */\n static getDefaultDataset() {\n return routingState.defaultDataset;\n }\n\n /** Set the default IDataset (catch-all for shapes with no explicit mapping). */\n static setDefaultDataset(dataset: IDataset) {\n routingState.defaultDataset = dataset;\n if (routingState.defaultDataset?.init) {\n routingState.defaultDataset.init();\n }\n setQueryDispatch({\n selectQuery: (q) => this.selectQuery(q),\n askQuery: (q) => this.askQuery(q),\n createQuery: (q) => this.createQuery(q),\n updateQuery: (q) => this.updateQuery(q),\n deleteQuery: (q) => this.deleteQuery(q),\n });\n }\n\n /** Pin one or more shape classes to a specific IDataset implementer. */\n static setDatasetForShapes(dataset: IDataset, ...shapeClasses: Function[]) {\n shapeClasses.forEach((shapeClass) => {\n // A shape's IRI is its identity, so two classes claiming the same IRI are the\n // same shape and must not route to different datasets. That situation is a\n // duplicate-registration bug elsewhere; say so here rather than resolving it by\n // Map insertion order, which is silent and arbitrary.\n const uri = shapeUriOf(shapeClass);\n if (uri) {\n for (const [pinned, existing] of routingState.shapeToDataset) {\n if (pinned !== shapeClass && shapeUriOf(pinned) === uri && existing !== dataset) {\n console.warn(\n `[linked] Two different classes are pinned to '${uri}' with different ` +\n `datasets. They are the same shape by identity, so one of these pins ` +\n `will be ignored. This usually means the shape class was registered twice.`,\n );\n }\n }\n }\n routingState.shapeToDataset.set(shapeClass, dataset);\n });\n }\n\n /**\n * Remove the pin for a shape, by class or by IRI.\n *\n * Removes **every** entry sharing that IRI, not just the one class object handed in.\n * Deleting by class identity alone is not enough: resolution falls back to matching\n * on IRI, so another class claiming the same shape would silently resurrect the pin\n * the caller believed they had removed.\n */\n static unsetDatasetForShape(shape: Function | string): void {\n const uri = typeof shape === 'string' ? shape : shapeUriOf(shape);\n routingState.shapeToDataset.delete(shape as Function);\n if (!uri) return;\n for (const pinned of [...routingState.shapeToDataset.keys()]) {\n if (shapeUriOf(pinned) === uri) {\n routingState.shapeToDataset.delete(pinned);\n }\n }\n }\n\n /** Every IDataset known to the primary router (default + all pinned). */\n static getDatasets(): CoreSet<IDataset> {\n const datasets = new CoreSet<IDataset>();\n if (routingState.defaultDataset) {\n datasets.add(routingState.defaultDataset);\n }\n routingState.shapeToDataset.forEach((dataset) => datasets.add(dataset));\n return datasets;\n }\n\n /**\n * The live shape→IDataset map — not a copy.\n *\n * Mutating it works but is not the supported way to remove a pin: deleting a key\n * here removes one class object, and resolution falls back to matching on shape IRI,\n * so another class claiming the same shape resurrects the pin. Use\n * `unsetDatasetForShape`, which removes every entry for that identity.\n */\n static getShapeToDatasetMap(): Map<Function, IDataset> {\n return routingState.shapeToDataset;\n }\n\n /** Resolve the IDataset for a given shape class. Walks the prototype chain. */\n static getDatasetForShapeClass(shapeClass?: Function | null): IDataset | undefined {\n let current: Function | null = shapeClass ?? null;\n while (typeof current === 'function') {\n const dataset = routingState.shapeToDataset.get(current);\n if (dataset) {\n return dataset;\n }\n const parent = Object.getPrototypeOf(current);\n if (parent === Function.prototype || parent === null) break;\n current = parent;\n }\n // Only once the identity walk has failed. `static shape` is inherited, so running\n // this at every level of the chain repeated the same scan for the same IRI.\n return this.findPinByShapeUri(shapeClass) ?? routingState.defaultDataset;\n }\n\n /**\n * The same pins, matched on shape IRI rather than on class identity.\n *\n * A class object is only a usable key while exactly one copy of the module\n * that declares it has evaluated, and in a built app there is not: the\n * backend runs from `lib/`, while `linked.backend.storage` is loaded from the\n * app root and imports the app's shapes from `src/`. The pin lands on one\n * copy of a shape class and the query resolves the other. The two are `!==`,\n * the lookup misses, and the query silently falls through to the default\n * dataset — which is how a shape that IS pinned reaches the app-data router\n * and fails there. Dev loads both halves from `src`, so it only breaks in\n * production.\n *\n * A shape's IRI does not have that problem: it is the shape's identity, it is\n * what the query already carries on the wire, and every copy derives the same\n * one. So it is the key that survives duplication.\n *\n * Deliberately a scan of the one map rather than a second map kept alongside it:\n * one source of truth, over a map holding a few dozen entries, on a path only\n * reached after an identity lookup has already failed.\n */\n private static findPinByShapeUri(shapeClass?: Function | null): IDataset | undefined {\n const uri = shapeUriOf(shapeClass);\n if (!uri) return undefined;\n for (const [pinned, dataset] of routingState.shapeToDataset) {\n if (shapeUriOf(pinned) === uri) {\n return dataset;\n }\n }\n return undefined;\n }\n\n private static resolveDatasetForQueryShape(\n shape?: string | Function | NodeShapeData | null,\n ): IDataset | undefined {\n if (!shape) {\n return routingState.defaultDataset;\n }\n if (typeof shape === 'function') {\n return this.getDatasetForShapeClass(shape);\n }\n if (typeof shape === 'string') {\n const shapeClass = getShapeClass(shape);\n return this.getDatasetForShapeClass(shapeClass);\n }\n // NodeShapeData (the closed query's `shape` accessor) — resolve via its IRI.\n if (typeof shape === 'object' && 'id' in shape) {\n const shapeClass = getShapeClass((shape as {id: string}).id);\n return this.getDatasetForShapeClass(shapeClass);\n }\n return routingState.defaultDataset;\n }\n\n /**\n * Route a select query — **and a count**, which is a select.\n *\n * A count is not a separate query form (an ask is: `ASK WHERE { … }`); it is\n * `SELECT (COUNT(DISTINCT ?s) AS ?count) WHERE { … }`, routed by the same shape to\n * the same dataset and answered over the same channel. So it needs no arm of its\n * own here — which is the point: when it had one, this router was missing it, and\n * `.count()` failed on every path that went through the router while passing\n * against a store held directly.\n *\n * The count contract — a real, non-negative integer, a failure never flattened\n * into `0` — belongs to `resolveCount`, which the builder goes through. None of it\n * is re-implemented here, exactly as none of `resolveExistence` is.\n */\n static selectQuery<ResultType>(\n query: SelectQuery | CountQuery,\n ): Promise<ResultType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid select query passed to LinkedStorage.selectQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.selectQuery) {\n return Promise.reject(\n new Error('No query dataset configured. Call LinkedStorage.setDefaultDataset().'),\n );\n }\n return dataset.selectQuery(query) as Promise<ResultType>;\n }\n\n /**\n * Route an ask query.\n *\n * A **shaped** ask routes like every other query — by its shape, to one dataset.\n *\n * A **shapeless** ask (\"does a node with this IRI exist at all\") has no shape,\n * and therefore no routing key. Asking only the default dataset would answer\n * `false` for a node that exists in a pinned one — a wrong answer, quietly. So a\n * router must ask every dataset it knows and OR the results. That is cheap\n * precisely because the answers are booleans: the fan-out short-circuits on the\n * first `true`, where the same sweep for rows could not.\n *\n * Any other router implementing `IDataset` inherits this obligation: a shapeless\n * ask means \"anywhere I can reach\", not \"in my default store\".\n *\n * The contract itself — a real boolean, errors never flattened to `false` —\n * belongs to `resolveExistence`, which the builder also goes through. None of it\n * is re-implemented here.\n */\n static async askQuery(query: AskQuery): Promise<boolean> {\n if (!query?.shape) {\n return this.askAnyDataset(query);\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset) {\n throw new Error(\n 'No query dataset configured. Call LinkedStorage.setDefaultDataset().',\n );\n }\n return resolveExistence(dataset, query);\n }\n\n /**\n * Fan a shapeless ask out across every known dataset, resolving `true` as soon\n * as one answers `true`.\n *\n * Sequential rather than parallel: the common case is a single dataset, and an\n * early `true` should not have already cost a query against every other store.\n * A failure from any dataset propagates — an unreachable store makes the answer\n * unknown, and \"unknown\" is not `false`.\n */\n private static async askAnyDataset(query: AskQuery): Promise<boolean> {\n const datasets = this.getDatasets();\n if (datasets.size === 0) {\n throw new Error(\n 'No query dataset configured. Call LinkedStorage.setDefaultDataset().',\n );\n }\n for (const dataset of datasets) {\n if (await resolveExistence(dataset, query)) return true;\n }\n return false;\n }\n\n static updateQuery<ResponseType>(query: UpdateQuery): Promise<ResponseType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid update query passed to LinkedStorage.updateQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.updateQuery) {\n return Promise.reject(\n new Error('No update handler configured on the query dataset.'),\n );\n }\n return dataset.updateQuery(query) as Promise<ResponseType>;\n }\n\n static createQuery<ResponseType>(query: CreateQuery): Promise<ResponseType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid create query passed to LinkedStorage.createQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.createQuery) {\n return Promise.reject(\n new Error('No create handler configured on the query dataset.'),\n );\n }\n return dataset.createQuery(query) as Promise<ResponseType>;\n }\n\n static deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid delete query passed to LinkedStorage.deleteQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.deleteQuery) {\n return Promise.reject(\n new Error('No delete handler configured on the query dataset.'),\n );\n }\n return dataset.deleteQuery(query);\n }\n}\n"]}
1
+ {"version":3,"file":"LinkedStorage.js","sourceRoot":"","sources":["../../../src/utils/LinkedStorage.ts"],"names":[],"mappings":"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","sourcesContent":["import {CoreSet} from '../collections/CoreSet.js';\nimport type {IDataset} from '../interfaces/IDataset.js';\nimport type {SelectQuery} from '../queries/SelectQuery.js';\nimport type {AskQuery} from '../queries/AskQuery.js';\nimport type {CountQuery} from '../queries/CountQuery.js';\nimport type {CreateQuery} from '../queries/CreateQuery.js';\nimport type {UpdateQuery} from '../queries/UpdateQuery.js';\nimport type {DeleteQuery, DeleteResponse} from '../queries/DeleteQuery.js';\nimport {setQueryDispatch, resolveExistence} from '../queries/queryDispatch.js';\nimport {getShapeClass} from './ShapeClass.js';\nimport type {NodeShapeData} from '../shapes/SHACL.js';\n\n// plan-011 — count physical evaluations of THIS module on the one shared\n// global object. With the single-loader fix there should be exactly one copy;\n// the single-instance guard in backend.ts reports this count if storage config\n// ever lands on a different copy. Deliberately avoids Date/Math.random\n// (unavailable / non-deterministic per repo constraints).\nconst linkedStorageGlobal: any =\n typeof globalThis !== 'undefined' ? globalThis : ({} as any);\nlinkedStorageGlobal.__linkedStorageInstanceCount =\n (linkedStorageGlobal.__linkedStorageInstanceCount ?? 0) + 1;\n\n/**\n * Primary routing layer (arch-04 §The IDataset abstraction).\n *\n * Resolves an incoming Linked Query to the IDataset that should handle it,\n * based on the query's target shape. Composite IDatasets (gateways, routers,\n * forwarders, resolvers) plug in here just like storage IDatasets do.\n *\n * The \"Dataset\" naming aligns with the IDataset contract. Earlier \"Store\"\n * names were renamed in phase-1 — see docs/plans/002-phase-1-create-user-project-flow.md.\n */\n/**\n * The routing table lives on the shared global, beside the shape registries.\n *\n * It has to, for the same reason they do: a module can evaluate more than once in a\n * process, and a class static then exists once per copy. Leaving these two here while\n * the registries were shared was worse than either extreme — a second copy answered\n * `isInitialised()` with false and `getDatasetForShapeClass()` with the default, while\n * reporting a fully populated shape registry.\n */\nconst routingState: {\n defaultDataset?: IDataset;\n shapeToDataset: Map<Function, IDataset>;\n listeners?: Set<() => void>;\n} = (linkedStorageGlobal.__linkedStorageRouting ??= {\n defaultDataset: undefined,\n shapeToDataset: new Map(),\n listeners: new Set(),\n});\n// An older copy of this module may have created the record without listeners.\nroutingState.listeners ??= new Set();\n\n/** Tell routing-change listeners that the set of datasets may have changed. */\nfunction notifyRoutingChanged(): void {\n for (const listener of [...routingState.listeners!]) {\n try {\n listener();\n } catch (err) {\n console.error('[linked] routing-change listener failed', err);\n }\n }\n}\n\n/** A shape class's IRI, or undefined for anything that is not a registered shape. */\nconst shapeUriOf = (shapeClass?: Function | null): string | undefined => {\n const uri = (shapeClass as {shape?: {id?: unknown}} | null | undefined)?.shape?.id;\n return typeof uri === 'string' && uri ? uri : undefined;\n};\n\nexport abstract class LinkedStorage {\n\n /** plan-011 — how many physical copies of this module have evaluated. */\n static getLoadedInstanceCount(): number {\n return linkedStorageGlobal.__linkedStorageInstanceCount ?? 0;\n }\n\n static isInitialised() {\n return !!routingState.defaultDataset;\n }\n\n /** The catch-all IDataset for shapes with no explicit mapping. */\n static getDefaultDataset() {\n return routingState.defaultDataset;\n }\n\n /** Set the default IDataset (catch-all for shapes with no explicit mapping). */\n static setDefaultDataset(dataset: IDataset) {\n routingState.defaultDataset = dataset;\n if (routingState.defaultDataset?.init) {\n routingState.defaultDataset.init();\n }\n setQueryDispatch({\n selectQuery: (q) => this.selectQuery(q),\n askQuery: (q) => this.askQuery(q),\n createQuery: (q) => this.createQuery(q),\n updateQuery: (q) => this.updateQuery(q),\n deleteQuery: (q) => this.deleteQuery(q),\n });\n notifyRoutingChanged();\n }\n\n /**\n * Be told whenever the set of datasets changes (`setDefaultDataset`,\n * `setDatasetForShapes`, `unsetDatasetForShape`). The live-query store uses\n * this to pick up a dataset's change feed. Returns the unsubscribe function.\n */\n static onRoutingChanged(listener: () => void): () => void {\n routingState.listeners!.add(listener);\n return () => routingState.listeners!.delete(listener);\n }\n\n /** Pin one or more shape classes to a specific IDataset implementer. */\n static setDatasetForShapes(dataset: IDataset, ...shapeClasses: Function[]) {\n shapeClasses.forEach((shapeClass) => {\n // A shape's IRI is its identity, so two classes claiming the same IRI are the\n // same shape and must not route to different datasets. That situation is a\n // duplicate-registration bug elsewhere; say so here rather than resolving it by\n // Map insertion order, which is silent and arbitrary.\n const uri = shapeUriOf(shapeClass);\n if (uri) {\n for (const [pinned, existing] of routingState.shapeToDataset) {\n if (pinned !== shapeClass && shapeUriOf(pinned) === uri && existing !== dataset) {\n console.warn(\n `[linked] Two different classes are pinned to '${uri}' with different ` +\n `datasets. They are the same shape by identity, so one of these pins ` +\n `will be ignored. This usually means the shape class was registered twice.`,\n );\n }\n }\n }\n routingState.shapeToDataset.set(shapeClass, dataset);\n });\n notifyRoutingChanged();\n }\n\n /**\n * Remove the pin for a shape, by class or by IRI.\n *\n * Removes **every** entry sharing that IRI, not just the one class object handed in.\n * Deleting by class identity alone is not enough: resolution falls back to matching\n * on IRI, so another class claiming the same shape would silently resurrect the pin\n * the caller believed they had removed.\n */\n static unsetDatasetForShape(shape: Function | string): void {\n const uri = typeof shape === 'string' ? shape : shapeUriOf(shape);\n routingState.shapeToDataset.delete(shape as Function);\n if (uri) {\n for (const pinned of [...routingState.shapeToDataset.keys()]) {\n if (shapeUriOf(pinned) === uri) {\n routingState.shapeToDataset.delete(pinned);\n }\n }\n }\n notifyRoutingChanged();\n }\n\n /** Every IDataset known to the primary router (default + all pinned). */\n static getDatasets(): CoreSet<IDataset> {\n const datasets = new CoreSet<IDataset>();\n if (routingState.defaultDataset) {\n datasets.add(routingState.defaultDataset);\n }\n routingState.shapeToDataset.forEach((dataset) => datasets.add(dataset));\n return datasets;\n }\n\n /**\n * The live shape→IDataset map — not a copy.\n *\n * Mutating it works but is not the supported way to remove a pin: deleting a key\n * here removes one class object, and resolution falls back to matching on shape IRI,\n * so another class claiming the same shape resurrects the pin. Use\n * `unsetDatasetForShape`, which removes every entry for that identity.\n */\n static getShapeToDatasetMap(): Map<Function, IDataset> {\n return routingState.shapeToDataset;\n }\n\n /** Resolve the IDataset for a given shape class. Walks the prototype chain. */\n static getDatasetForShapeClass(shapeClass?: Function | null): IDataset | undefined {\n let current: Function | null = shapeClass ?? null;\n while (typeof current === 'function') {\n const dataset = routingState.shapeToDataset.get(current);\n if (dataset) {\n return dataset;\n }\n const parent = Object.getPrototypeOf(current);\n if (parent === Function.prototype || parent === null) break;\n current = parent;\n }\n // Only once the identity walk has failed. `static shape` is inherited, so running\n // this at every level of the chain repeated the same scan for the same IRI.\n return this.findPinByShapeUri(shapeClass) ?? routingState.defaultDataset;\n }\n\n /**\n * The same pins, matched on shape IRI rather than on class identity.\n *\n * A class object is only a usable key while exactly one copy of the module\n * that declares it has evaluated, and in a built app there is not: the\n * backend runs from `lib/`, while `linked.backend.storage` is loaded from the\n * app root and imports the app's shapes from `src/`. The pin lands on one\n * copy of a shape class and the query resolves the other. The two are `!==`,\n * the lookup misses, and the query silently falls through to the default\n * dataset — which is how a shape that IS pinned reaches the app-data router\n * and fails there. Dev loads both halves from `src`, so it only breaks in\n * production.\n *\n * A shape's IRI does not have that problem: it is the shape's identity, it is\n * what the query already carries on the wire, and every copy derives the same\n * one. So it is the key that survives duplication.\n *\n * Deliberately a scan of the one map rather than a second map kept alongside it:\n * one source of truth, over a map holding a few dozen entries, on a path only\n * reached after an identity lookup has already failed.\n */\n private static findPinByShapeUri(shapeClass?: Function | null): IDataset | undefined {\n const uri = shapeUriOf(shapeClass);\n if (!uri) return undefined;\n for (const [pinned, dataset] of routingState.shapeToDataset) {\n if (shapeUriOf(pinned) === uri) {\n return dataset;\n }\n }\n return undefined;\n }\n\n private static resolveDatasetForQueryShape(\n shape?: string | Function | NodeShapeData | null,\n ): IDataset | undefined {\n if (!shape) {\n return routingState.defaultDataset;\n }\n if (typeof shape === 'function') {\n return this.getDatasetForShapeClass(shape);\n }\n if (typeof shape === 'string') {\n const shapeClass = getShapeClass(shape);\n return this.getDatasetForShapeClass(shapeClass);\n }\n // NodeShapeData (the closed query's `shape` accessor) — resolve via its IRI.\n if (typeof shape === 'object' && 'id' in shape) {\n const shapeClass = getShapeClass((shape as {id: string}).id);\n return this.getDatasetForShapeClass(shapeClass);\n }\n return routingState.defaultDataset;\n }\n\n /**\n * Route a select query — **and a count**, which is a select.\n *\n * A count is not a separate query form (an ask is: `ASK WHERE { … }`); it is\n * `SELECT (COUNT(DISTINCT ?s) AS ?count) WHERE { … }`, routed by the same shape to\n * the same dataset and answered over the same channel. So it needs no arm of its\n * own here — which is the point: when it had one, this router was missing it, and\n * `.count()` failed on every path that went through the router while passing\n * against a store held directly.\n *\n * The count contract — a real, non-negative integer, a failure never flattened\n * into `0` — belongs to `resolveCount`, which the builder goes through. None of it\n * is re-implemented here, exactly as none of `resolveExistence` is.\n */\n static selectQuery<ResultType>(\n query: SelectQuery | CountQuery,\n ): Promise<ResultType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid select query passed to LinkedStorage.selectQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.selectQuery) {\n return Promise.reject(\n new Error('No query dataset configured. Call LinkedStorage.setDefaultDataset().'),\n );\n }\n return dataset.selectQuery(query) as Promise<ResultType>;\n }\n\n /**\n * Route an ask query.\n *\n * A **shaped** ask routes like every other query — by its shape, to one dataset.\n *\n * A **shapeless** ask (\"does a node with this IRI exist at all\") has no shape,\n * and therefore no routing key. Asking only the default dataset would answer\n * `false` for a node that exists in a pinned one — a wrong answer, quietly. So a\n * router must ask every dataset it knows and OR the results. That is cheap\n * precisely because the answers are booleans: the fan-out short-circuits on the\n * first `true`, where the same sweep for rows could not.\n *\n * Any other router implementing `IDataset` inherits this obligation: a shapeless\n * ask means \"anywhere I can reach\", not \"in my default store\".\n *\n * The contract itself — a real boolean, errors never flattened to `false` —\n * belongs to `resolveExistence`, which the builder also goes through. None of it\n * is re-implemented here.\n */\n static async askQuery(query: AskQuery): Promise<boolean> {\n if (!query?.shape) {\n return this.askAnyDataset(query);\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset) {\n throw new Error(\n 'No query dataset configured. Call LinkedStorage.setDefaultDataset().',\n );\n }\n return resolveExistence(dataset, query);\n }\n\n /**\n * Fan a shapeless ask out across every known dataset, resolving `true` as soon\n * as one answers `true`.\n *\n * Sequential rather than parallel: the common case is a single dataset, and an\n * early `true` should not have already cost a query against every other store.\n * A failure from any dataset propagates — an unreachable store makes the answer\n * unknown, and \"unknown\" is not `false`.\n */\n private static async askAnyDataset(query: AskQuery): Promise<boolean> {\n const datasets = this.getDatasets();\n if (datasets.size === 0) {\n throw new Error(\n 'No query dataset configured. Call LinkedStorage.setDefaultDataset().',\n );\n }\n for (const dataset of datasets) {\n if (await resolveExistence(dataset, query)) return true;\n }\n return false;\n }\n\n static updateQuery<ResponseType>(query: UpdateQuery): Promise<ResponseType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid update query passed to LinkedStorage.updateQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.updateQuery) {\n return Promise.reject(\n new Error('No update handler configured on the query dataset.'),\n );\n }\n return dataset.updateQuery(query) as Promise<ResponseType>;\n }\n\n static createQuery<ResponseType>(query: CreateQuery): Promise<ResponseType> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid create query passed to LinkedStorage.createQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.createQuery) {\n return Promise.reject(\n new Error('No create handler configured on the query dataset.'),\n );\n }\n return dataset.createQuery(query) as Promise<ResponseType>;\n }\n\n static deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n if (!query?.shape) {\n return Promise.reject(\n new Error(\n 'Invalid delete query passed to LinkedStorage.deleteQuery(): missing shape.',\n ),\n );\n }\n const dataset = this.resolveDatasetForQueryShape(query.shape);\n if (!dataset?.deleteQuery) {\n return Promise.reject(\n new Error('No delete handler configured on the query dataset.'),\n );\n }\n return dataset.deleteQuery(query);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_linked/core",
3
- "version": "2.26.0",
3
+ "version": "2.28.0",
4
4
  "packageManager": "npm@12.1.0",
5
5
  "license": "MIT",
6
6
  "linkedPackage": true,
@@ -19,6 +19,10 @@
19
19
  "**/utils/Package.ts",
20
20
  "**/lib/esm/index.js",
21
21
  "**/src/index.ts",
22
+ "**/lib/esm/live.js",
23
+ "**/src/live.ts",
24
+ "**/lib/esm/live/LiveQueryStore.js",
25
+ "**/src/live/LiveQueryStore.ts",
22
26
  "**/shapes/index.js",
23
27
  "**/shapes/index.ts",
24
28
  "**/shapes/*.js",
@@ -58,6 +62,7 @@
58
62
  "sync:agents": "node packages/skills/sync.mjs",
59
63
  "setup": "npx semantu-agents",
60
64
  "typecheck": "tsc -p tsconfig-tests.json",
65
+ "check:dts": "tsc -p tsconfig-dts-check.json",
61
66
  "test": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs && npm run typecheck",
62
67
  "test:fuseki": "docker compose -f src/tests/docker-compose.test.yml up -d --wait && NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --runInBand --testPathPattern='sparql-fuseki' --verbose; EXIT=$?; docker compose -f src/tests/docker-compose.test.yml down; exit $EXIT"
63
68
  },