@typicalday/firegraph 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -90
- package/bin/firegraph.mjs +21 -7
- package/dist/backend-U-MLShlg.d.ts +97 -0
- package/dist/backend-np4gEVhB.d.cts +97 -0
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +7 -6
- package/dist/backend.d.ts +7 -6
- package/dist/backend.js +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/{chunk-EVUM6ORB.js → chunk-6SB34IPQ.js} +76 -8
- package/dist/chunk-6SB34IPQ.js.map +1 -0
- package/dist/{chunk-SU4FNLC3.js → chunk-EEKWRX5E.js} +1 -1
- package/dist/{chunk-SU4FNLC3.js.map → chunk-EEKWRX5E.js.map} +1 -1
- package/dist/{chunk-YLGXLEUE.js → chunk-GJVVRTQT.js} +5 -14
- package/dist/chunk-GJVVRTQT.js.map +1 -0
- package/dist/{chunk-GLOVWKQH.js → chunk-R7CRGYY4.js} +1 -1
- package/dist/{chunk-GLOVWKQH.js.map → chunk-R7CRGYY4.js.map} +1 -1
- package/dist/{do-sqlite.cjs → cloudflare/index.cjs} +1659 -1422
- package/dist/cloudflare/index.cjs.map +1 -0
- package/dist/cloudflare/index.d.cts +529 -0
- package/dist/cloudflare/index.d.ts +529 -0
- package/dist/cloudflare/index.js +934 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/codegen/index.cjs +4 -13
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +1 -1
- package/dist/index.cjs +144 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -27
- package/dist/index.d.ts +116 -27
- package/dist/index.js +77 -123
- package/dist/index.js.map +1 -1
- package/dist/query-client/index.cjs.map +1 -1
- package/dist/query-client/index.js +1 -1
- package/dist/{scope-path-BtajqNK5.d.ts → scope-path-B1G3YiA7.d.cts} +5 -100
- package/dist/{scope-path-D2mNENJ-.d.cts → scope-path-B1G3YiA7.d.ts} +5 -100
- package/dist/{types-DfWVTsMn.d.ts → types-BGWxcpI_.d.cts} +92 -1
- package/dist/{types-DfWVTsMn.d.cts → types-BGWxcpI_.d.ts} +92 -1
- package/package.json +13 -17
- package/dist/chunk-EVUM6ORB.js.map +0 -1
- package/dist/chunk-SZ6W4VAS.js +0 -701
- package/dist/chunk-SZ6W4VAS.js.map +0 -1
- package/dist/chunk-YLGXLEUE.js.map +0 -1
- package/dist/d1.cjs +0 -2421
- package/dist/d1.cjs.map +0 -1
- package/dist/d1.d.cts +0 -54
- package/dist/d1.d.ts +0 -54
- package/dist/d1.js +0 -76
- package/dist/d1.js.map +0 -1
- package/dist/do-sqlite.cjs.map +0 -1
- package/dist/do-sqlite.d.cts +0 -41
- package/dist/do-sqlite.d.ts +0 -41
- package/dist/do-sqlite.js +0 -79
- package/dist/do-sqlite.js.map +0 -1
- package/dist/editor/client/assets/index-Bq2bfzeY.js +0 -411
- package/dist/editor/client/assets/index-CJ4m_EOL.css +0 -1
- package/dist/editor/client/index.html +0 -16
- package/dist/editor/server/index.mjs +0 -51511
package/dist/backend.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/backend.ts","../src/errors.ts","../src/internal/routing-backend.ts","../src/scope-path.ts"],"sourcesContent":["/**\n * Public backend surface — the stable set of types and primitives for code\n * that wants to wrap, substitute, or compose `StorageBackend`s.\n *\n * Most firegraph users only touch `GraphClient`; this module is for the\n * narrower set of users who write their own storage drivers (e.g. an RPC\n * executor that tunnels a `StorageBackend` into a Durable Object) or who\n * need to route `subgraph()` calls across multiple physical backends\n * (see `createRoutingBackend`).\n *\n * Entry point: `firegraph/backend`.\n */\n\nexport { CrossBackendTransactionError } from './errors.js';\nexport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './internal/backend.js';\nexport type { RoutingBackendOptions, RoutingContext } from './internal/routing-backend.js';\nexport { createRoutingBackend } from './internal/routing-backend.js';\nexport type { StorageScopeSegment } from './scope-path.js';\nexport {\n appendStorageScope,\n isAncestorScopeUid,\n parseStorageScope,\n resolveAncestorScope,\n} from './scope-path.js';\n","export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by any of the underlying drivers (D1, DO\n * SQLite, Firestore), so firegraph surfaces this as a typed error instead\n * of silently confining the write to the base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n","/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord): Promise<void> {\n return this.base.setDoc(docId, record);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * const base = createDOSqliteBackend(ctx.storage, 'fg');\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * const stub = env.MEMORIES.get(env.MEMORIES.idFromName(storageScope));\n * return createMyRpcBackend(stub); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n","/**\n * Storage-scope path utilities — materialized-path parsing helpers for the\n * SQLite backend's `storageScope` string and for any custom backend that\n * adopts the same encoding (e.g. a cross-DO routing layer that uses\n * `storageScope` as a Durable Object name).\n *\n * **Storage-scope** (as produced by `SqliteBackendImpl`) interleaves parent\n * UIDs with subgraph names:\n *\n * ```\n * '' // root\n * 'A/memories' // g.subgraph(A, 'memories')\n * 'A/memories/B/context' // .subgraph(B, 'context') on the above\n * ```\n *\n * The structure is the same as a Firestore collection path with the\n * collection/doc segments reordered: each pair is `<uid>/<name>`, where\n * `<uid>` is a node UID in the parent scope and `<name>` is the subgraph\n * name. Use these helpers to decode that structure when building cross-\n * backend routers (see `createRoutingBackend`).\n *\n * For Firestore paths (which begin with a collection segment), use\n * `resolveAncestorCollection` / `isAncestorUid` from `./cross-graph.js`.\n */\n\n/**\n * One segment of a materialized-path storage-scope — a `(uid, name)` pair\n * produced by one `subgraph(uid, name)` call.\n */\nexport interface StorageScopeSegment {\n /** Parent node UID at the enclosing scope. */\n uid: string;\n /** Subgraph name chosen by the caller (e.g. `'memories'`). */\n name: string;\n}\n\n/**\n * Parse a materialized-path storage-scope into its `(uid, name)` pairs.\n *\n * Returns `[]` for the root (`''`). Throws `Error('INVALID_SCOPE_PATH')`\n * when the string has an odd number of segments (a corrupt path — every\n * level contributes exactly two segments) or when any segment is empty.\n *\n * @example\n * ```ts\n * parseStorageScope(''); // []\n * parseStorageScope('A/memories'); // [{ uid: 'A', name: 'memories' }]\n * parseStorageScope('A/memories/B/context'); // [{ uid: 'A', name: 'memories' }, { uid: 'B', name: 'context' }]\n * ```\n */\nexport function parseStorageScope(scope: string): StorageScopeSegment[] {\n if (scope === '') return [];\n const parts = scope.split('/');\n if (parts.length % 2 !== 0) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" has an odd number of segments; ` +\n 'expected interleaved <uid>/<name> pairs.',\n );\n }\n const out: StorageScopeSegment[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n const uid = parts[i];\n const name = parts[i + 1];\n if (!uid || !name) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" contains an empty segment at position ${i}.`,\n );\n }\n out.push({ uid, name });\n }\n return out;\n}\n\n/**\n * Resolve the ancestor **storage-scope** at which a given UID's node lives,\n * by scanning a materialized-path storage-scope for that UID.\n *\n * Mirrors `resolveAncestorCollection()` from `./cross-graph.js` for\n * Firestore paths, but operates on `storageScope` (no leading collection\n * segment — segments are `<uid>/<name>` pairs).\n *\n * @returns The storage-scope at which the UID's node was added via\n * `subgraph(uid, _)`, or `null` if the UID does not appear at a UID\n * position in the path.\n *\n * @example\n * ```ts\n * // Scope: 'A/memories/B/context'\n * resolveAncestorScope('A/memories/B/context', 'A'); // '' (A was added at root)\n * resolveAncestorScope('A/memories/B/context', 'B'); // 'A/memories'\n * resolveAncestorScope('A/memories/B/context', 'X'); // null\n * ```\n */\nexport function resolveAncestorScope(storageScope: string, uid: string): string | null {\n if (!uid) return null;\n if (storageScope === '') return null;\n const parts = storageScope.split('/');\n // UID positions are even indices (0, 2, 4, …); names are at odd indices.\n for (let i = 0; i < parts.length; i += 2) {\n if (parts[i] === uid) {\n return i === 0 ? '' : parts.slice(0, i).join('/');\n }\n }\n return null;\n}\n\n/**\n * Boolean shorthand for `resolveAncestorScope(scope, uid) !== null`.\n */\nexport function isAncestorScopeUid(storageScope: string, uid: string): boolean {\n return resolveAncestorScope(storageScope, uid) !== null;\n}\n\n/**\n * Join a parent storage-scope with a new `(uid, name)` pair, producing the\n * storage-scope that `backend.subgraph(uid, name)` would use internally.\n *\n * This is the inverse of `parseStorageScope`'s per-segment semantics and is\n * useful when computing DO names / shard keys from the router callback.\n */\nexport function appendStorageScope(parentScope: string, uid: string, name: string): string {\n if (!uid || uid.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: uid must be non-empty and must not contain \"/\": got \"${uid}\".`,\n );\n }\n if (!name || name.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: name must be non-empty and must not contain \"/\": got \"${name}\".`,\n );\n }\n return parentScope ? `${parentScope}/${uid}/${name}` : `${uid}/${name}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/backend.ts","../src/errors.ts","../src/internal/routing-backend.ts","../src/scope-path.ts"],"sourcesContent":["/**\n * Public backend surface — the stable set of types and primitives for code\n * that wants to wrap, substitute, or compose `StorageBackend`s.\n *\n * Most firegraph users only touch `GraphClient`; this module is for the\n * narrower set of users who write their own storage drivers (e.g. an RPC\n * executor that tunnels a `StorageBackend` into a Durable Object) or who\n * need to route `subgraph()` calls across multiple physical backends\n * (see `createRoutingBackend`).\n *\n * Entry point: `firegraph/backend`.\n */\n\nexport { CrossBackendTransactionError } from './errors.js';\nexport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './internal/backend.js';\nexport type { RoutingBackendOptions, RoutingContext } from './internal/routing-backend.js';\nexport { createRoutingBackend } from './internal/routing-backend.js';\nexport type { StorageScopeSegment } from './scope-path.js';\nexport {\n appendStorageScope,\n isAncestorScopeUid,\n parseStorageScope,\n resolveAncestorScope,\n} from './scope-path.js';\n","export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by real-world storage engines (Firestore,\n * SQLite drivers over D1/DO/better-sqlite3, etc.), so firegraph surfaces\n * this as a typed error instead of silently confining the write to the\n * base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n","/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord): Promise<void> {\n return this.base.setDoc(docId, record);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * // `base` is any StorageBackend — e.g. a Firestore-backed one, an\n * // in-process SQLite backend, or the DO backend from firegraph/cloudflare.\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * return createMyMemoriesBackend(storageScope); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n","/**\n * Storage-scope path utilities — materialized-path parsing helpers for the\n * SQLite backend's `storageScope` string and for any custom backend that\n * adopts the same encoding (e.g. a cross-DO routing layer that uses\n * `storageScope` as a Durable Object name).\n *\n * **Storage-scope** (as produced by `SqliteBackendImpl`) interleaves parent\n * UIDs with subgraph names:\n *\n * ```\n * '' // root\n * 'A/memories' // g.subgraph(A, 'memories')\n * 'A/memories/B/context' // .subgraph(B, 'context') on the above\n * ```\n *\n * The structure is the same as a Firestore collection path with the\n * collection/doc segments reordered: each pair is `<uid>/<name>`, where\n * `<uid>` is a node UID in the parent scope and `<name>` is the subgraph\n * name. Use these helpers to decode that structure when building cross-\n * backend routers (see `createRoutingBackend`).\n *\n * For Firestore paths (which begin with a collection segment), use\n * `resolveAncestorCollection` / `isAncestorUid` from `./cross-graph.js`.\n */\n\n/**\n * One segment of a materialized-path storage-scope — a `(uid, name)` pair\n * produced by one `subgraph(uid, name)` call.\n */\nexport interface StorageScopeSegment {\n /** Parent node UID at the enclosing scope. */\n uid: string;\n /** Subgraph name chosen by the caller (e.g. `'memories'`). */\n name: string;\n}\n\n/**\n * Parse a materialized-path storage-scope into its `(uid, name)` pairs.\n *\n * Returns `[]` for the root (`''`). Throws `Error('INVALID_SCOPE_PATH')`\n * when the string has an odd number of segments (a corrupt path — every\n * level contributes exactly two segments) or when any segment is empty.\n *\n * @example\n * ```ts\n * parseStorageScope(''); // []\n * parseStorageScope('A/memories'); // [{ uid: 'A', name: 'memories' }]\n * parseStorageScope('A/memories/B/context'); // [{ uid: 'A', name: 'memories' }, { uid: 'B', name: 'context' }]\n * ```\n */\nexport function parseStorageScope(scope: string): StorageScopeSegment[] {\n if (scope === '') return [];\n const parts = scope.split('/');\n if (parts.length % 2 !== 0) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" has an odd number of segments; ` +\n 'expected interleaved <uid>/<name> pairs.',\n );\n }\n const out: StorageScopeSegment[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n const uid = parts[i];\n const name = parts[i + 1];\n if (!uid || !name) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" contains an empty segment at position ${i}.`,\n );\n }\n out.push({ uid, name });\n }\n return out;\n}\n\n/**\n * Resolve the ancestor **storage-scope** at which a given UID's node lives,\n * by scanning a materialized-path storage-scope for that UID.\n *\n * Mirrors `resolveAncestorCollection()` from `./cross-graph.js` for\n * Firestore paths, but operates on `storageScope` (no leading collection\n * segment — segments are `<uid>/<name>` pairs).\n *\n * @returns The storage-scope at which the UID's node was added via\n * `subgraph(uid, _)`, or `null` if the UID does not appear at a UID\n * position in the path.\n *\n * @example\n * ```ts\n * // Scope: 'A/memories/B/context'\n * resolveAncestorScope('A/memories/B/context', 'A'); // '' (A was added at root)\n * resolveAncestorScope('A/memories/B/context', 'B'); // 'A/memories'\n * resolveAncestorScope('A/memories/B/context', 'X'); // null\n * ```\n */\nexport function resolveAncestorScope(storageScope: string, uid: string): string | null {\n if (!uid) return null;\n if (storageScope === '') return null;\n const parts = storageScope.split('/');\n // UID positions are even indices (0, 2, 4, …); names are at odd indices.\n for (let i = 0; i < parts.length; i += 2) {\n if (parts[i] === uid) {\n return i === 0 ? '' : parts.slice(0, i).join('/');\n }\n }\n return null;\n}\n\n/**\n * Boolean shorthand for `resolveAncestorScope(scope, uid) !== null`.\n */\nexport function isAncestorScopeUid(storageScope: string, uid: string): boolean {\n return resolveAncestorScope(storageScope, uid) !== null;\n}\n\n/**\n * Join a parent storage-scope with a new `(uid, name)` pair, producing the\n * storage-scope that `backend.subgraph(uid, name)` would use internally.\n *\n * This is the inverse of `parseStorageScope`'s per-segment semantics and is\n * useful when computing DO names / shard keys from the router callback.\n */\nexport function appendStorageScope(parentScope: string, uid: string, name: string): string {\n if (!uid || uid.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: uid must be non-empty and must not contain \"/\": got \"${uid}\".`,\n );\n }\n if (!name || name.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: name must be non-empty and must not contain \"/\": got \"${name}\".`,\n );\n }\n return parentScope ? `${parentScope}/${uid}/${name}` : `${uid}/${name}`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/backend.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export { C as CrossBackendTransactionError, S as StorageScopeSegment, a as appendStorageScope, i as isAncestorScopeUid, p as parseStorageScope, r as resolveAncestorScope } from './scope-path-B1G3YiA7.cjs';
|
|
2
|
+
import { S as StorageBackend } from './backend-np4gEVhB.cjs';
|
|
3
|
+
export { B as BatchBackend, T as TransactionBackend, U as UpdatePayload, W as WritableRecord } from './backend-np4gEVhB.cjs';
|
|
4
|
+
import './types-BGWxcpI_.cjs';
|
|
4
5
|
import '@google-cloud/firestore';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -105,12 +106,12 @@ interface RoutingBackendOptions {
|
|
|
105
106
|
*
|
|
106
107
|
* @example
|
|
107
108
|
* ```ts
|
|
108
|
-
*
|
|
109
|
+
* // `base` is any StorageBackend — e.g. a Firestore-backed one, an
|
|
110
|
+
* // in-process SQLite backend, or the DO backend from firegraph/cloudflare.
|
|
109
111
|
* const routed = createRoutingBackend(base, {
|
|
110
112
|
* route: ({ subgraphName, storageScope }) => {
|
|
111
113
|
* if (subgraphName !== 'memories') return null;
|
|
112
|
-
*
|
|
113
|
-
* return createMyRpcBackend(stub); // caller-owned
|
|
114
|
+
* return createMyMemoriesBackend(storageScope); // caller-owned
|
|
114
115
|
* },
|
|
115
116
|
* });
|
|
116
117
|
* const client = createGraphClientFromBackend(routed, { registry });
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export { C as CrossBackendTransactionError, S as StorageScopeSegment, a as appendStorageScope, i as isAncestorScopeUid, p as parseStorageScope, r as resolveAncestorScope } from './scope-path-B1G3YiA7.js';
|
|
2
|
+
import { S as StorageBackend } from './backend-U-MLShlg.js';
|
|
3
|
+
export { B as BatchBackend, T as TransactionBackend, U as UpdatePayload, W as WritableRecord } from './backend-U-MLShlg.js';
|
|
4
|
+
import './types-BGWxcpI_.js';
|
|
4
5
|
import '@google-cloud/firestore';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -105,12 +106,12 @@ interface RoutingBackendOptions {
|
|
|
105
106
|
*
|
|
106
107
|
* @example
|
|
107
108
|
* ```ts
|
|
108
|
-
*
|
|
109
|
+
* // `base` is any StorageBackend — e.g. a Firestore-backed one, an
|
|
110
|
+
* // in-process SQLite backend, or the DO backend from firegraph/cloudflare.
|
|
109
111
|
* const routed = createRoutingBackend(base, {
|
|
110
112
|
* route: ({ subgraphName, storageScope }) => {
|
|
111
113
|
* if (subgraphName !== 'memories') return null;
|
|
112
|
-
*
|
|
113
|
-
* return createMyRpcBackend(stub); // caller-owned
|
|
114
|
+
* return createMyMemoriesBackend(storageScope); // caller-owned
|
|
114
115
|
* },
|
|
115
116
|
* });
|
|
116
117
|
* const client = createGraphClientFromBackend(routed, { registry });
|
package/dist/backend.js
CHANGED
package/dist/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/routing-backend.ts"],"sourcesContent":["/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord): Promise<void> {\n return this.base.setDoc(docId, record);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * const base = createDOSqliteBackend(ctx.storage, 'fg');\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * const stub = env.MEMORIES.get(env.MEMORIES.idFromName(storageScope));\n * return createMyRpcBackend(stub); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/routing-backend.ts"],"sourcesContent":["/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord): Promise<void> {\n return this.base.setDoc(docId, record);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * // `base` is any StorageBackend — e.g. a Firestore-backed one, an\n * // in-process SQLite backend, or the DO backend from firegraph/cloudflare.\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * return createMyMemoriesBackend(storageScope); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
RegistryScopeError,
|
|
8
8
|
RegistryViolationError,
|
|
9
9
|
ValidationError
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-R7CRGYY4.js";
|
|
11
11
|
|
|
12
12
|
// src/internal/constants.ts
|
|
13
13
|
var NODE_RELATION = "is";
|
|
@@ -214,9 +214,7 @@ async function migrateRecord(record, registry, globalWriteBack = "off") {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
async function migrateRecords(records, registry, globalWriteBack = "off") {
|
|
217
|
-
return Promise.all(
|
|
218
|
-
records.map((r) => migrateRecord(r, registry, globalWriteBack))
|
|
219
|
-
);
|
|
217
|
+
return Promise.all(records.map((r) => migrateRecord(r, registry, globalWriteBack)));
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
// src/scope.ts
|
|
@@ -298,6 +296,28 @@ function createRegistry(input) {
|
|
|
298
296
|
for (const [key, arr] of axbBuild) {
|
|
299
297
|
axbIndex.set(key, Object.freeze(arr));
|
|
300
298
|
}
|
|
299
|
+
const topologyIndex = /* @__PURE__ */ new Map();
|
|
300
|
+
const topologyBuild = /* @__PURE__ */ new Map();
|
|
301
|
+
const topologySeen = /* @__PURE__ */ new Map();
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!entry.targetGraph) continue;
|
|
304
|
+
let seen = topologySeen.get(entry.aType);
|
|
305
|
+
if (!seen) {
|
|
306
|
+
seen = /* @__PURE__ */ new Set();
|
|
307
|
+
topologySeen.set(entry.aType, seen);
|
|
308
|
+
}
|
|
309
|
+
if (seen.has(entry.targetGraph)) continue;
|
|
310
|
+
seen.add(entry.targetGraph);
|
|
311
|
+
const existing = topologyBuild.get(entry.aType);
|
|
312
|
+
if (existing) {
|
|
313
|
+
existing.push(entry);
|
|
314
|
+
} else {
|
|
315
|
+
topologyBuild.set(entry.aType, [entry]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const [key, arr] of topologyBuild) {
|
|
319
|
+
topologyIndex.set(key, Object.freeze(arr));
|
|
320
|
+
}
|
|
301
321
|
return {
|
|
302
322
|
lookup(aType, axbType, bType) {
|
|
303
323
|
return map.get(tripleKey(aType, axbType, bType))?.entry;
|
|
@@ -305,6 +325,9 @@ function createRegistry(input) {
|
|
|
305
325
|
lookupByAxbType(axbType) {
|
|
306
326
|
return axbIndex.get(axbType) ?? [];
|
|
307
327
|
},
|
|
328
|
+
getSubgraphTopology(aType) {
|
|
329
|
+
return topologyIndex.get(aType) ?? [];
|
|
330
|
+
},
|
|
308
331
|
validate(aType, axbType, bType, data, scopePath) {
|
|
309
332
|
const rec = map.get(tripleKey(aType, axbType, bType));
|
|
310
333
|
if (!rec) {
|
|
@@ -352,6 +375,21 @@ function createMergedRegistry(base, extension) {
|
|
|
352
375
|
}
|
|
353
376
|
return Object.freeze(merged);
|
|
354
377
|
},
|
|
378
|
+
getSubgraphTopology(aType) {
|
|
379
|
+
const baseResults = base.getSubgraphTopology(aType);
|
|
380
|
+
const extResults = extension.getSubgraphTopology(aType);
|
|
381
|
+
if (extResults.length === 0) return baseResults;
|
|
382
|
+
if (baseResults.length === 0) return extResults;
|
|
383
|
+
const seen = new Set(baseResults.map((e) => e.targetGraph));
|
|
384
|
+
const merged = [...baseResults];
|
|
385
|
+
for (const entry of extResults) {
|
|
386
|
+
if (!seen.has(entry.targetGraph)) {
|
|
387
|
+
seen.add(entry.targetGraph);
|
|
388
|
+
merged.push(entry);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return Object.freeze(merged);
|
|
392
|
+
},
|
|
355
393
|
validate(aType, axbType, bType, data, scopePath) {
|
|
356
394
|
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
357
395
|
return base.validate(aType, axbType, bType, data, scopePath);
|
|
@@ -384,7 +422,8 @@ function discoveryToEntries(discovery) {
|
|
|
384
422
|
subtitleField: entity.subtitleField,
|
|
385
423
|
allowedIn: entity.allowedIn,
|
|
386
424
|
migrations: entity.migrations,
|
|
387
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
425
|
+
migrationWriteBack: entity.migrationWriteBack,
|
|
426
|
+
indexes: entity.indexes
|
|
388
427
|
});
|
|
389
428
|
}
|
|
390
429
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -412,7 +451,8 @@ function discoveryToEntries(discovery) {
|
|
|
412
451
|
allowedIn: entity.allowedIn,
|
|
413
452
|
targetGraph: resolvedTargetGraph,
|
|
414
453
|
migrations: entity.migrations,
|
|
415
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
454
|
+
migrationWriteBack: entity.migrationWriteBack,
|
|
455
|
+
indexes: entity.indexes
|
|
416
456
|
});
|
|
417
457
|
}
|
|
418
458
|
}
|
|
@@ -1142,6 +1182,21 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1142
1182
|
getBackend() {
|
|
1143
1183
|
return this.backend;
|
|
1144
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Snapshot of the currently-effective registry. Returns the merged view
|
|
1187
|
+
* used for domain-type validation and migration — in dynamic mode this is
|
|
1188
|
+
* `dynamicRegistry ?? staticRegistry ?? bootstrapRegistry`, so callers see
|
|
1189
|
+
* updates after `reloadRegistry()` without having to re-resolve anything.
|
|
1190
|
+
*
|
|
1191
|
+
* Exposed for backends that need topology access during bulk operations
|
|
1192
|
+
* (e.g. the Cloudflare DO backend's cross-DO cascade). Not part of the
|
|
1193
|
+
* public `GraphClient` surface.
|
|
1194
|
+
*
|
|
1195
|
+
* @internal
|
|
1196
|
+
*/
|
|
1197
|
+
getRegistrySnapshot() {
|
|
1198
|
+
return this.getCombinedRegistry();
|
|
1199
|
+
}
|
|
1145
1200
|
// ---------------------------------------------------------------------------
|
|
1146
1201
|
// Registry routing
|
|
1147
1202
|
// ---------------------------------------------------------------------------
|
|
@@ -1538,6 +1593,18 @@ function createGraphClientFromBackend(backend, options, metaBackend) {
|
|
|
1538
1593
|
return new GraphClientImpl(backend, options, metaBackend);
|
|
1539
1594
|
}
|
|
1540
1595
|
|
|
1596
|
+
// src/default-indexes.ts
|
|
1597
|
+
var DEFAULT_CORE_INDEXES = Object.freeze([
|
|
1598
|
+
{ fields: ["aUid"] },
|
|
1599
|
+
{ fields: ["bUid"] },
|
|
1600
|
+
{ fields: ["aType"] },
|
|
1601
|
+
{ fields: ["bType"] },
|
|
1602
|
+
{ fields: ["aUid", "axbType"] },
|
|
1603
|
+
{ fields: ["axbType", "bUid"] },
|
|
1604
|
+
{ fields: ["aType", "axbType"] },
|
|
1605
|
+
{ fields: ["axbType", "bType"] }
|
|
1606
|
+
]);
|
|
1607
|
+
|
|
1541
1608
|
export {
|
|
1542
1609
|
NODE_RELATION,
|
|
1543
1610
|
DEFAULT_QUERY_LIMIT,
|
|
@@ -1570,6 +1637,7 @@ export {
|
|
|
1570
1637
|
buildNodeQueryPlan,
|
|
1571
1638
|
analyzeQuerySafety,
|
|
1572
1639
|
GraphClientImpl,
|
|
1573
|
-
createGraphClientFromBackend
|
|
1640
|
+
createGraphClientFromBackend,
|
|
1641
|
+
DEFAULT_CORE_INDEXES
|
|
1574
1642
|
};
|
|
1575
|
-
//# sourceMappingURL=chunk-
|
|
1643
|
+
//# sourceMappingURL=chunk-6SB34IPQ.js.map
|