@noy-db/hub 0.6.0-pre.10 → 0.6.0-pre.11
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/dist/cargo/index.js +1 -1
- package/dist/{chunk-GAG7QOVO.js → chunk-6BVER473.js} +2 -2
- package/dist/{chunk-3737A6EA.js → chunk-BGQ63MCT.js} +74 -17
- package/dist/chunk-BGQ63MCT.js.map +1 -0
- package/dist/{chunk-BEFVYAR5.js → chunk-CBXHHCZT.js} +2 -2
- package/dist/{chunk-PPMBY7YS.js → chunk-CCVYQFLU.js} +2 -2
- package/dist/{chunk-XEJA6K76.js → chunk-GU7A7MQA.js} +2 -2
- package/dist/{chunk-E6427JU2.js → chunk-H4YQSDOI.js} +17 -1
- package/dist/chunk-H4YQSDOI.js.map +1 -0
- package/dist/{chunk-N3BI6P3J.js → chunk-UV4O7FAL.js} +8 -8
- package/dist/{dispatch-FZEAWKVM.js → dispatch-23SKSHPD.js} +5 -5
- package/dist/{executor-YIQB2S6N.js → executor-FKZURXVV.js} +3 -3
- package/dist/index.js +7 -7
- package/dist/kernel/query/builder.d.ts +15 -2
- package/dist/kernel/query/join.d.ts +27 -0
- package/dist/materialized-views/index.js +3 -3
- package/dist/{noydb-SNZONAYA.js → noydb-FYTCH2GD.js} +4 -4
- package/dist/pod/index.js +4 -4
- package/dist/port/to/index.d.ts +2 -2
- package/dist/port/to/locator.d.ts +54 -8
- package/dist/query/index.js +2 -2
- package/dist/{stale-ARHFVL5U.js → stale-4PUTWI2H.js} +2 -2
- package/dist/to/index.js +16 -6
- package/dist/to/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-3737A6EA.js.map +0 -1
- package/dist/chunk-E6427JU2.js.map +0 -1
- /package/dist/{chunk-GAG7QOVO.js.map → chunk-6BVER473.js.map} +0 -0
- /package/dist/{chunk-BEFVYAR5.js.map → chunk-CBXHHCZT.js.map} +0 -0
- /package/dist/{chunk-PPMBY7YS.js.map → chunk-CCVYQFLU.js.map} +0 -0
- /package/dist/{chunk-XEJA6K76.js.map → chunk-GU7A7MQA.js.map} +0 -0
- /package/dist/{chunk-N3BI6P3J.js.map → chunk-UV4O7FAL.js.map} +0 -0
- /package/dist/{dispatch-FZEAWKVM.js.map → dispatch-23SKSHPD.js.map} +0 -0
- /package/dist/{executor-YIQB2S6N.js.map → executor-FKZURXVV.js.map} +0 -0
- /package/dist/{noydb-SNZONAYA.js.map → noydb-FYTCH2GD.js.map} +0 -0
- /package/dist/{stale-ARHFVL5U.js.map → stale-4PUTWI2H.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/kernel/query/join.ts"],"sourcesContent":["/**\n * Query DSL `.join()` — eager, single-FK, intra-vault joins.\n *\n * resolves a ref()-declared foreign key into an attached\n * right-side record under an alias, using one of two planner paths\n * selected automatically:\n *\n * - **nested-loop** — right-side source exposes `lookupById`, so\n * each left row costs O(1). This is the common path for joins\n * against a Collection, which backs `lookupById` with a Map\n * lookup.\n * - **hash** — right-side has only `snapshot()`. Build a\n * `Map<id, record>` once, probe per left row. Same asymptotic\n * cost for our collections, but the path exists as a fallback\n * for custom QuerySource implementations and as an explicit\n * test-only override via `{ strategy: 'hash' }`.\n *\n * Scope:\n *\n * - Equi-joins on declared `ref()` fields only. Joins on\n * undeclared fields throw at plan time with an actionable error\n * naming the field and collection.\n * - Same-vault only. Cross-vault correlation goes\n * through `queryAcross`; this is an architectural\n * invariant, not a limitation we plan to lift.\n * - Hard row ceiling via `JoinTooLargeError` — default 50k per\n * side, override via `{ maxRows }`. Warns at 80% of the ceiling\n * on the existing warn channel.\n * - Three ref-mode behaviors on dangling refs:\n * strict → `DanglingReferenceError`,\n * warn → attach `null` with a one-shot warning,\n * cascade → attach `null` silently (cascade is a delete-time\n * mode; any dangling refs still present at read time are\n * mid-flight cascades or orphans from earlier, not a DSL error).\n *\n * Partition-awareness seam:\n *\n * Every `JoinLeg` carries a `partitionScope` field that is always\n * `'all'` in. The executor never reads this field.\n * partition-aware joins will start populating it from `where()`\n * predicates on the partition key without changing the planner's\n * external shape — this is the whole reason it exists now.\n *\n * Joins stay OUT of the ledger: reads don't touch `_ledger/`,\n * including joined reads.\n */\n\nimport type { RefDescriptor, RefMode } from '../refs.js'\nimport type { Clause } from './predicate.js'\nimport { readPath } from './predicate.js'\nimport { JoinTooLargeError, DanglingReferenceError } from '../errors.js'\n\n/** Planner strategy for a single join leg. Auto-selected unless overridden. */\nexport type JoinStrategy = 'hash' | 'nested'\n\n/** Default per-side row ceiling before `.join()` throws `JoinTooLargeError`. */\nexport const DEFAULT_JOIN_MAX_ROWS = 50_000\n\n/**\n * Fraction of the row ceiling at which a one-shot warning is emitted.\n * At 80% we warn; at 100% we throw. The warn gives consumers a\n * heads-up before the hard error so they can raise the ceiling or\n * filter further without first hitting a broken query.\n */\nconst JOIN_WARN_FRACTION = 0.8\n\n/**\n * Internal representation of a single join leg in the query plan.\n *\n * This is the primary place where constraint #1 is honored:\n * every leg carries a `partitionScope` field that is always `'all'`\n * in and is never read by the executor. partition-aware\n * joins will start populating it from `where()` predicates on the\n * partition key without changing the planner's external shape.\n */\nexport interface JoinLeg {\n /** Field on the left-side record holding the foreign key value. */\n readonly field: string\n /** Alias key under which the joined right-side record attaches. */\n readonly as: string\n /** Target collection name, resolved from the `ref()` declaration. */\n readonly target: string\n /** Ref mode controlling behavior on dangling refs at read time. */\n readonly mode: RefMode\n /** Manual planner strategy override. `undefined` → auto-select. */\n readonly strategy: JoinStrategy | undefined\n /** Per-side row ceiling override. `undefined` → DEFAULT_JOIN_MAX_ROWS. */\n readonly maxRows: number | undefined\n /**\n * Partition scope for future partition-aware joins. Always `'all'`\n * today — the executor never reads this field. Future versions will\n * populate it from `where()` predicates without breaking the\n * planner's external shape. Do not remove even though it looks\n * unused today — that's the whole point of having it.\n */\n readonly partitionScope: 'all' | readonly string[]\n /**\n * When `true`, this is a dictionary join. The executor\n * resolves the left-field value against the dict snapshot and\n * attaches `{ ...labels, key }` rather than a right-side record.\n * `target` holds the dictionary name (not a collection name).\n */\n readonly isDictJoin?: true\n}\n\n/**\n * Minimal shape of a joinable right-side record source.\n *\n * Collections implement this structurally via their `QuerySource`;\n * sources without `lookupById` force the hash-join fallback. Kept as\n * a thin interface so tests can wire up plain-object sources without\n * pulling in the full Collection class.\n *\n * The optional `subscribe` is used by `Query.live()` to merge\n * right-side change streams into the live re-run trigger. Sources\n * that omit `subscribe` still work for live joins — they just\n * don't drive re-fires when their right side mutates. Collection\n * implements `subscribe` by hooking into the existing per-\n * vault event emitter.\n */\nexport interface JoinableSource {\n snapshot(): readonly unknown[]\n lookupById?(id: string): unknown\n /**\n * Default locale a label-resolving query falls back to when the query\n * itself is locale-less. Set by a `staticDict()`-backed source from its\n * `displayLocale` so `{ by: 'label' }` resolves under a locale-less read.\n * Plain `_dict_*`-backed sources omit it.\n */\n readonly displayLocale?: string\n /**\n * Sync present-for-join dressing (#626 retirement, #650 Task 6) — when\n * present and the query carries a locale, each joined right-side record\n * is passed through this hook (built by the right-side `Collection` from\n * its own i18n-text + lookup-label bindings) BEFORE it is attached under\n * the leg's alias — so a joined `i18nText` field resolves to a string\n * (not a raw `{ locale }` map) and a joined lookup field gains its\n * `<field>Label`. Locale-less queries leave joined fields raw\n * (consistent with a locale-less read). Replaces the old `i18nFields`\n * data field — the join executor no longer resolves i18n locale itself,\n * it just calls this hook.\n */\n readonly presentForJoin?: (record: unknown, locale: string) => unknown\n /**\n * Subscribe to mutations on this source. The callback fires\n * AFTER the underlying record set has been updated. Returns an\n * unsubscribe function. Optional — sources without this method\n * cannot trigger live-join re-fires from their side.\n */\n subscribe?(cb: () => void): () => void\n}\n\n/**\n * Join resolution context attached to a `Query` when it's constructed\n * from a `Collection`. Holds everything the `.join()` method needs to\n * translate a field name into a target collection + ref mode, and\n * everything the executor needs to read the right side.\n *\n * Kept as a structural interface so `Vault` can implement it\n * without `Query` needing to import `Vault` (circular-import\n * avoid). The Collection wires this up in its `query()` method using\n * the `joinResolver` back-reference the Vault passes in.\n */\nexport interface JoinContext {\n /** Name of the left-side (owning) collection. */\n readonly leftCollection: string\n /**\n * The owning collection's default locale. Used to resolve joined\n * i18n fields at the `join` layer when a terminal call doesn't pass an\n * explicit locale — so `openVault({ locale })` flows to joins like it does\n * to `get`/`list`. A per-call `toArray({ locale })` overrides it.\n */\n readonly defaultLocale?: string\n /** Look up a `RefDescriptor` by field name on the left collection. */\n resolveRef(field: string): RefDescriptor | null\n /** Resolve a right-side source by target collection name. */\n resolveSource(collectionName: string): JoinableSource | null\n /**\n * Resolve a dictKey join source. Returns a `JoinableSource`\n * whose snapshot exposes `{ key, ...labels }` records, keyed by the\n * stable dictionary key. `null` when the field is not a dictKey.\n *\n * The source is built from the compartment's in-memory dictionary\n * snapshot — same data as `DictionaryHandle.list()`, O(1) per lookup.\n */\n resolveDictSource?(field: string): JoinableSource | null\n}\n\n/**\n * Does this clause address a field that only exists once join legs are\n * attached — i.e. is its path rooted at a join alias?\n *\n * Only `FieldClause` is inspectable. A `FilterClause` carries an opaque\n * function and a `WherePredicateClause` a named one, so neither can be\n * classified; both stay on the pre-join side, which is where they have\n * always run. See `splitAroundJoins`.\n */\nfunction referencesJoinAlias(clause: Clause, aliases: ReadonlySet<string>): boolean {\n if (clause.type !== 'field') return false\n // `where('client.name', …)` addresses the alias; so does the anti-join\n // form `where('client', '==', null)`, where the path IS the alias.\n return aliases.has(clause.field.split('.')[0]!)\n}\n\n/**\n * Split a plan's clauses into those evaluable before the join legs run and\n * those that need the joined shape (#1030).\n *\n * Join legs attach after `where` so the left set can be narrowed (and\n * index-driven) first. That is the right default, but it silently broke any\n * predicate addressing a joined alias: the field did not exist yet, so\n * `readPath` returned `undefined`, nothing matched, and the query returned an\n * empty result with no error.\n *\n * The split is deliberately narrow. When no clause addresses an alias — every\n * query written against the previous behaviour — `postJoin` is empty and the\n * caller takes its original path unchanged. The reordered pipeline therefore\n * only ever runs for queries that match nothing today.\n *\n * KNOWN RESIDUAL: `.filter(r => r.client?.name === 'Ann')` cannot be\n * classified (the predicate is a closure), so it still runs pre-join and still\n * sees no alias. Prefer `.where()` for anything addressing a joined field.\n *\n * Shared by the eager `Query` and the streaming `ScanBuilder` so the two\n * cannot drift on which side a clause belongs to.\n */\nexport function splitAroundJoins(\n clauses: readonly Clause[],\n joins: readonly JoinLeg[],\n): { readonly preJoin: readonly Clause[]; readonly postJoin: readonly Clause[] } {\n if (joins.length === 0 || clauses.length === 0) return { preJoin: clauses, postJoin: [] }\n const aliases = new Set(joins.map(leg => leg.as))\n const preJoin: Clause[] = []\n const postJoin: Clause[] = []\n for (const clause of clauses) {\n ;(referencesJoinAlias(clause, aliases) ? postJoin : preJoin).push(clause)\n }\n return { preJoin, postJoin }\n}\n\n/**\n * Coerce an unknown FK value into a lookup key string.\n *\n * Legitimate ref values are strings or numbers — the same narrowing\n * the write-time `enforceRefsOnPut` path applies. Anything else\n * (objects, arrays, booleans, null, undefined) is treated as \"no\n * ref\" and returns `null`, so the join attaches `null` instead of\n * running `String({})` and producing `'[object Object]'` as a\n * bucket key. This matches the lint rule guidance and keeps\n * bizarre FK values from producing silently-wrong lookups.\n */\nfunction coerceRefKey(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'bigint') return String(value)\n return null\n}\n\n/**\n * Warn-channel deduplication for dangling-ref `'warn'` mode. Keyed\n * by `field → target:refId` so the same dangling ref only produces\n * one warning even across many rows or repeated queries.\n */\nconst warnedDanglingKeys = new Set<string>()\nfunction warnOnceDangling(field: string, target: string, refId: string): void {\n const key = `${field}→${target}:${refId}`\n if (warnedDanglingKeys.has(key)) return\n warnedDanglingKeys.add(key)\n console.warn(\n `[noy-db] .join() encountered dangling ref in 'warn' mode: ` +\n `field \"${field}\" → \"${target}:${refId}\" not found. Attaching null.`,\n )\n}\n\n/**\n * Track row-ceiling warnings to fire only once per (target, side).\n * Prevents per-query spam when a consumer is running the same query\n * repeatedly (e.g. in a reactive loop).\n */\nconst warnedCeilingKeys = new Set<string>()\nfunction warnCeilingApproaching(\n target: string,\n side: 'left' | 'right',\n rows: number,\n maxRows: number,\n): void {\n const key = `${target}:${side}`\n if (warnedCeilingKeys.has(key)) return\n warnedCeilingKeys.add(key)\n const pct = Math.round((rows / maxRows) * 100)\n console.warn(\n `[noy-db] .join() ${side} side is at ${pct}% of the ${maxRows}-row ` +\n `ceiling for target \"${target}\" (${rows} rows). Streaming joins over ` +\n `scan() are not yet supported for collections that need to exceed this.`,\n )\n}\n\n/**\n * Apply every join leg in the plan against a base set of left-side\n * rows. Called by the query executor after `where` / `orderBy` /\n * `offset` / `limit` have narrowed the left set.\n *\n * Each leg attaches a `leg.as` field to every row. Returns a new\n * array of plain objects — the original left rows are not mutated\n * (structural sharing is fine for the inner fields, but the\n * top-level object is a fresh clone so consumers can further mutate\n * safely).\n *\n * **Ordering:** joins run AFTER orderBy / limit / offset in v1.\n * This keeps the planner simple and means queries like \"top 10\n * invoices with client\" sort and paginate the left side first, then\n * join. Sorting *by* a joined field is out of scope for — users\n * can post-sort the result array in userland or wait for \n * (multi-FK chaining) which can be layered on top.\n *\n * **Multi-FK chaining:** each leg's `maxRows` is enforced\n * against the current left-row count independently. Because\n * joins are equi-joins on the target's primary key (one-to-one or\n * one-to-null), the left row count is constant across legs — no\n * cartesian blowup. The per-leg left-side check is still necessary\n * so that a later leg with a tighter ceiling correctly fires on a\n * query like `.join('a', { maxRows: 100_000 }).join('b', { maxRows: 50 })`,\n * which should throw on the second leg if the left set exceeds 50.\n */\nexport function applyJoins(\n rows: readonly unknown[],\n joins: readonly JoinLeg[],\n context: JoinContext,\n locale?: string,\n): unknown[] {\n if (joins.length === 0) return [...rows]\n\n let result: unknown[] = [...rows]\n for (const leg of joins) {\n result = applyOneJoin(result, leg, context, locale)\n }\n return result\n}\n\nfunction applyOneJoin(\n leftRows: readonly unknown[],\n leg: JoinLeg,\n context: JoinContext,\n locale?: string,\n): unknown[] {\n // Dict join path — resolve left-field value against the\n // dictionary snapshot and attach { key, ...labels } under leg.as.\n if (leg.isDictJoin) {\n const dictSource = context.resolveDictSource?.(leg.field)\n if (!dictSource) {\n throw new Error(\n `.join() field \"${leg.field}\" on \"${context.leftCollection}\" is declared as a ` +\n `dictKey join but the dict source could not be resolved. ` +\n `Ensure the dictionary has at least one entry.`,\n )\n }\n const out: unknown[] = []\n const snapshot = dictSource.snapshot()\n const dictMap = new Map<string, unknown>()\n for (const entry of snapshot) {\n const k = readPath(entry, 'key')\n if (typeof k === 'string') dictMap.set(k, entry)\n }\n for (const left of leftRows) {\n const rawId = readPath(left, leg.field)\n const key = coerceRefKey(rawId)\n const dictEntry = key === null ? undefined : dictMap.get(key)\n out.push({ ...(left as Record<string, unknown>), [leg.as]: dictEntry ?? null })\n }\n return out\n }\n\n const source = context.resolveSource(leg.target)\n if (!source) {\n throw new Error(\n `.join() cannot resolve target collection \"${leg.target}\" ` +\n `(referenced from field \"${leg.field}\" on \"${context.leftCollection}\"). ` +\n `Make sure the target collection has been opened via vault.collection() ` +\n `at least once before running the query.`,\n )\n }\n\n const maxRows = leg.maxRows ?? DEFAULT_JOIN_MAX_ROWS\n\n // Per-leg left-side ceiling check. In a\n // multi-FK chain, each leg's `maxRows` is enforced independently\n // against the current left-row count, so\n // `.join('a', { maxRows: 100_000 }).join('b', { maxRows: 50 })`\n // correctly throws on the second leg if the left set exceeds 50.\n if (leftRows.length > maxRows) {\n throw new JoinTooLargeError({\n leftRows: leftRows.length,\n rightRows: -1,\n maxRows,\n side: 'left',\n message:\n `.join() left side has ${leftRows.length} rows, exceeding the ${maxRows}-row ` +\n `ceiling for target \"${leg.target}\". Filter the left side further with ` +\n `where()/limit() before joining, or raise the ceiling via { maxRows }. ` +\n `Streaming joins over scan() are not yet supported.`,\n })\n }\n if (leftRows.length > maxRows * JOIN_WARN_FRACTION) {\n warnCeilingApproaching(leg.target, 'left', leftRows.length, maxRows)\n }\n\n const rightSnapshot = source.snapshot()\n if (rightSnapshot.length > maxRows) {\n throw new JoinTooLargeError({\n leftRows: leftRows.length,\n rightRows: rightSnapshot.length,\n maxRows,\n side: 'right',\n message:\n `.join() right side \"${leg.target}\" has ${rightSnapshot.length} rows, ` +\n `exceeding the ${maxRows}-row ceiling. Raise the ceiling via { maxRows } ` +\n `if the data genuinely fits in memory, or track for streaming joins.`,\n })\n }\n if (rightSnapshot.length > maxRows * JOIN_WARN_FRACTION) {\n warnCeilingApproaching(leg.target, 'right', rightSnapshot.length, maxRows)\n }\n\n // `join`-layer dressing (#650 Task 6, #626 retirement). When the query\n // carries a locale (per-call or the owning collection's default) and the\n // right side declares a `presentForJoin` hook, resolve each matched right\n // record through it before it's attached. Locale-less → leave raw.\n const effLocale = locale ?? context.defaultLocale\n const presentResolve: ((right: unknown) => unknown) | undefined =\n effLocale !== undefined && source.presentForJoin !== undefined\n ? (right) =>\n right !== null && typeof right === 'object'\n ? source.presentForJoin!(right, effLocale)\n : right\n : undefined\n\n // Strategy selection: explicit override wins; otherwise prefer\n // nested-loop when the source exposes lookupById (O(1) per row),\n // falling back to hash join when it doesn't.\n const strategy: JoinStrategy =\n leg.strategy ?? (source.lookupById ? 'nested' : 'hash')\n\n if (strategy === 'nested' && source.lookupById) {\n // Bind through an arrow so the `this` context of lookupById\n // doesn't drift — same pattern as the existing candidateRecords\n // helper in builder.ts.\n const lookup = (id: string): unknown => source.lookupById?.(id)\n return nestedLoopJoin(leftRows, leg, lookup, presentResolve)\n }\n return hashJoin(leftRows, leg, rightSnapshot, presentResolve)\n}\n\nfunction nestedLoopJoin(\n leftRows: readonly unknown[],\n leg: JoinLeg,\n lookupById: (id: string) => unknown,\n presentResolve?: (right: unknown) => unknown,\n): unknown[] {\n const out: unknown[] = []\n for (const left of leftRows) {\n const rawId = readPath(left, leg.field)\n const key = coerceRefKey(rawId)\n let right = key === null ? undefined : lookupById(key)\n if (presentResolve && right !== undefined) right = presentResolve(right)\n out.push(attachJoin(left, leg, right, rawId))\n }\n return out\n}\n\nfunction hashJoin(\n leftRows: readonly unknown[],\n leg: JoinLeg,\n rightSnapshot: readonly unknown[],\n presentResolve?: (right: unknown) => unknown,\n): unknown[] {\n // Build the right-side hash once per query execution. We key on\n // the `id` field because ref() always points to a target's primary\n // key — non-equi and non-id joins are out of scope for.\n const rightMap = new Map<string, unknown>()\n for (const record of rightSnapshot) {\n const rawId = readPath(record, 'id')\n const key = coerceRefKey(rawId)\n if (key !== null) {\n rightMap.set(key, record)\n }\n }\n const out: unknown[] = []\n for (const left of leftRows) {\n const rawId = readPath(left, leg.field)\n const key = coerceRefKey(rawId)\n let right = key === null ? undefined : rightMap.get(key)\n if (presentResolve && right !== undefined) right = presentResolve(right)\n out.push(attachJoin(left, leg, right, rawId))\n }\n return out\n}\n\n/**\n * Attach the resolved right-side record (or null) to the left row\n * under the alias, applying ref-mode semantics for the dangling\n * case.\n *\n * A left-side record whose FK field is null/undefined is NOT a\n * dangling ref — it's \"no reference at all\", which is always\n * allowed regardless of mode. This matches the write-time\n * `enforceRefsOnPut` behavior: \"Nullish ref values are allowed —\n * treat them as 'no reference'.\"\n *\n * Only non-null FKs pointing at non-existent targets trigger the\n * mode behavior.\n */\nfunction attachJoin(\n left: unknown,\n leg: JoinLeg,\n right: unknown,\n rawId: unknown,\n): unknown {\n if (left === null || typeof left !== 'object') {\n // Pathological input — return as-is. Shouldn't happen in\n // practice because QuerySource yields objects, but defensive\n // because plan execution is untyped at this layer.\n return left\n }\n const merged: Record<string, unknown> = { ...(left as Record<string, unknown>) }\n\n // \"No ref at all\" — null/undefined FK value, or a non-string/non-\n // number FK that coerceRefKey treated as no-ref. Never throws\n // regardless of mode; matches the write-time policy that nullish\n // refs are allowed.\n const refKey = coerceRefKey(rawId)\n if (right === undefined) {\n if (refKey !== null && leg.mode === 'strict') {\n throw new DanglingReferenceError({\n field: leg.field,\n target: leg.target,\n refId: refKey,\n message:\n `.join() strict dangling: record references \"${leg.target}:${refKey}\" ` +\n `via field \"${leg.field}\", but no such record exists. Use ref() mode 'warn' ` +\n `or 'cascade' if dangling refs are acceptable, or run ` +\n `vault.checkIntegrity() to find and fix the orphans.`,\n })\n }\n if (refKey !== null && leg.mode === 'warn') {\n warnOnceDangling(leg.field, leg.target, refKey)\n }\n // For 'cascade' and null refs we attach null silently. Cascade\n // is a delete-time mode; any dangling refs visible at read time\n // are either mid-flight or pre-existing orphans, not a DSL error.\n merged[leg.as] = null\n } else {\n merged[leg.as] = right\n }\n return merged\n}\n\n/**\n * Test-only: reset the join warning deduplication state between\n * tests. Production code never calls this — the dedup state is\n * intentionally process-scoped so a noisy query doesn't spam the\n * console once per component render.\n */\nexport function resetJoinWarnings(): void {\n warnedDanglingKeys.clear()\n warnedCeilingKeys.clear()\n}\n"],"mappings":"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","names":[]}
|
|
@@ -150,7 +150,7 @@ import {
|
|
|
150
150
|
import {
|
|
151
151
|
Query,
|
|
152
152
|
ScanBuilder
|
|
153
|
-
} from "./chunk-
|
|
153
|
+
} from "./chunk-BGQ63MCT.js";
|
|
154
154
|
import {
|
|
155
155
|
EXPORT_AUDIT_COLLECTION,
|
|
156
156
|
NO_BLOBS,
|
|
@@ -3409,7 +3409,7 @@ var Collection = class {
|
|
|
3409
3409
|
}
|
|
3410
3410
|
}
|
|
3411
3411
|
if (this.materializedViewSource !== void 0) {
|
|
3412
|
-
const { resolveStaleMVOnRead } = await import("./stale-
|
|
3412
|
+
const { resolveStaleMVOnRead } = await import("./stale-4PUTWI2H.js");
|
|
3413
3413
|
await resolveStaleMVOnRead(this.materializedViewSource, this.name, this.#dispatchCtx({ collection: this.name, id: "resolve-on-read" }));
|
|
3414
3414
|
}
|
|
3415
3415
|
let record;
|
|
@@ -3939,7 +3939,7 @@ var Collection = class {
|
|
|
3939
3939
|
*/
|
|
3940
3940
|
async dispatchMaterializedViews(id, record, wave) {
|
|
3941
3941
|
if (this.materializedViewSource === void 0) return;
|
|
3942
|
-
const { dispatchMaterializedViews } = await import("./dispatch-
|
|
3942
|
+
const { dispatchMaterializedViews } = await import("./dispatch-23SKSHPD.js");
|
|
3943
3943
|
return dispatchMaterializedViews(this.#mvDispatchCtx(this.materializedViewSource), id, record, wave);
|
|
3944
3944
|
}
|
|
3945
3945
|
/**
|
|
@@ -4441,7 +4441,7 @@ var Collection = class {
|
|
|
4441
4441
|
*/
|
|
4442
4442
|
async dispatchMaterializedViewsOnDelete(id) {
|
|
4443
4443
|
if (this.materializedViewSource === void 0) return { deleted: 0, residueUndecodable: [], residueDeclined: [] };
|
|
4444
|
-
const { dispatchMaterializedViewsOnDelete } = await import("./dispatch-
|
|
4444
|
+
const { dispatchMaterializedViewsOnDelete } = await import("./dispatch-23SKSHPD.js");
|
|
4445
4445
|
return dispatchMaterializedViewsOnDelete(this.#mvDispatchCtx(this.materializedViewSource), id);
|
|
4446
4446
|
}
|
|
4447
4447
|
/**
|
|
@@ -4461,7 +4461,7 @@ var Collection = class {
|
|
|
4461
4461
|
);
|
|
4462
4462
|
}
|
|
4463
4463
|
if (this.materializedViewSource !== void 0) {
|
|
4464
|
-
const { resolveStaleMVOnRead } = await import("./stale-
|
|
4464
|
+
const { resolveStaleMVOnRead } = await import("./stale-4PUTWI2H.js");
|
|
4465
4465
|
await resolveStaleMVOnRead(this.materializedViewSource, this.name, this.#dispatchCtx({ collection: this.name, id: "resolve-on-read" }));
|
|
4466
4466
|
}
|
|
4467
4467
|
await this.ensureHydrated();
|
|
@@ -10300,14 +10300,14 @@ var Vault = class {
|
|
|
10300
10300
|
if (!reg) {
|
|
10301
10301
|
throw new Error(`refreshView: no MV registered with name "${name}"`);
|
|
10302
10302
|
}
|
|
10303
|
-
const { MaterializedViewExecutor } = await import("./executor-
|
|
10303
|
+
const { MaterializedViewExecutor } = await import("./executor-FKZURXVV.js");
|
|
10304
10304
|
const result = await MaterializedViewExecutor.refresh(reg, {
|
|
10305
10305
|
getCollection: (n) => this.collection(n),
|
|
10306
10306
|
getActiveTxContext: () => this.noydb._activeTxContextOrNull,
|
|
10307
10307
|
getQueryContext: () => this,
|
|
10308
10308
|
dispatchCtx: this._dispatchCtx({ collection: name, id: "refreshView" })
|
|
10309
10309
|
});
|
|
10310
|
-
const { clearMVStaleFully } = await import("./stale-
|
|
10310
|
+
const { clearMVStaleFully } = await import("./stale-4PUTWI2H.js");
|
|
10311
10311
|
await clearMVStaleFully(this.adapter, this.name, registry, name);
|
|
10312
10312
|
return result;
|
|
10313
10313
|
}
|
|
@@ -14549,4 +14549,4 @@ export {
|
|
|
14549
14549
|
Noydb,
|
|
14550
14550
|
createNoydb
|
|
14551
14551
|
};
|
|
14552
|
-
//# sourceMappingURL=chunk-
|
|
14552
|
+
//# sourceMappingURL=chunk-UV4O7FAL.js.map
|
|
@@ -15,7 +15,7 @@ async function dispatchMaterializedViews(ctx, id, record, wave) {
|
|
|
15
15
|
if (wave?.seen(`mv\0${reg.spec.name}`)) continue;
|
|
16
16
|
if (executor === null) {
|
|
17
17
|
;
|
|
18
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
18
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-FKZURXVV.js"));
|
|
19
19
|
}
|
|
20
20
|
await executor.refresh(reg, {
|
|
21
21
|
getCollection: (name) => source.getCollection(name),
|
|
@@ -24,7 +24,7 @@ async function dispatchMaterializedViews(ctx, id, record, wave) {
|
|
|
24
24
|
dispatchCtx: dispatchCtx({ collection: collectionName, id })
|
|
25
25
|
});
|
|
26
26
|
} else if (mode === "lazy") {
|
|
27
|
-
if (staleHelpers === null) staleHelpers = await import("./stale-
|
|
27
|
+
if (staleHelpers === null) staleHelpers = await import("./stale-4PUTWI2H.js");
|
|
28
28
|
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -44,7 +44,7 @@ async function dispatchMaterializedViewsOnDelete(ctx, id) {
|
|
|
44
44
|
if (mode === "eager") {
|
|
45
45
|
if (executor === null) {
|
|
46
46
|
;
|
|
47
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
47
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-FKZURXVV.js"));
|
|
48
48
|
}
|
|
49
49
|
const rr = await executor.refresh(reg, {
|
|
50
50
|
getCollection: (name) => source.getCollection(name),
|
|
@@ -57,7 +57,7 @@ async function dispatchMaterializedViewsOnDelete(ctx, id) {
|
|
|
57
57
|
residueDeclined.push(...rr.residueDeclined);
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
|
-
if (staleHelpers === null) staleHelpers = await import("./stale-
|
|
60
|
+
if (staleHelpers === null) staleHelpers = await import("./stale-4PUTWI2H.js");
|
|
61
61
|
const inv = await staleHelpers.invalidateMVAtRest(source, reg, mode);
|
|
62
62
|
deleted += inv.deleted;
|
|
63
63
|
residueUndecodable.push(...inv.residueUndecodable.map((rid) => `${reg.outputCollection}:${rid}`));
|
|
@@ -69,4 +69,4 @@ export {
|
|
|
69
69
|
dispatchMaterializedViews,
|
|
70
70
|
dispatchMaterializedViewsOnDelete
|
|
71
71
|
};
|
|
72
|
-
//# sourceMappingURL=dispatch-
|
|
72
|
+
//# sourceMappingURL=dispatch-23SKSHPD.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MaterializedViewExecutor
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6BVER473.js";
|
|
4
4
|
import "./chunk-7F4DBNSP.js";
|
|
5
5
|
import "./chunk-RXYYFSBB.js";
|
|
6
6
|
import "./chunk-CCZJJ4I2.js";
|
|
7
7
|
import "./chunk-5ZKS6ZE2.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-H4YQSDOI.js";
|
|
9
9
|
import "./chunk-XZHEOSQ2.js";
|
|
10
10
|
import "./chunk-CADUYBQD.js";
|
|
11
11
|
import "./chunk-IGJCYUT5.js";
|
|
@@ -26,4 +26,4 @@ import "./chunk-PZ5AY32C.js";
|
|
|
26
26
|
export {
|
|
27
27
|
MaterializedViewExecutor
|
|
28
28
|
};
|
|
29
|
-
//# sourceMappingURL=executor-
|
|
29
|
+
//# sourceMappingURL=executor-FKZURXVV.js.map
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
decryptExtractedPartition,
|
|
55
55
|
diffVault,
|
|
56
56
|
withCargo
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-GU7A7MQA.js";
|
|
58
58
|
import "./chunk-4GZ6XOVW.js";
|
|
59
59
|
import {
|
|
60
60
|
createDeedOwner,
|
|
@@ -90,8 +90,8 @@ import "./chunk-YAWK6AWF.js";
|
|
|
90
90
|
import {
|
|
91
91
|
withMaterializedView
|
|
92
92
|
} from "./chunk-CEJWMBXO.js";
|
|
93
|
-
import "./chunk-
|
|
94
|
-
import "./chunk-
|
|
93
|
+
import "./chunk-CBXHHCZT.js";
|
|
94
|
+
import "./chunk-6BVER473.js";
|
|
95
95
|
import "./chunk-7F4DBNSP.js";
|
|
96
96
|
import {
|
|
97
97
|
MoneyCurrencyError,
|
|
@@ -167,7 +167,7 @@ import {
|
|
|
167
167
|
verifyRedirect,
|
|
168
168
|
wrapBundleStore,
|
|
169
169
|
wrapPodStore
|
|
170
|
-
} from "./chunk-
|
|
170
|
+
} from "./chunk-CCVYQFLU.js";
|
|
171
171
|
import {
|
|
172
172
|
Collection,
|
|
173
173
|
ELEVATION_AUDIT_COLLECTION,
|
|
@@ -186,7 +186,7 @@ import {
|
|
|
186
186
|
refArray,
|
|
187
187
|
via,
|
|
188
188
|
withArchive
|
|
189
|
-
} from "./chunk-
|
|
189
|
+
} from "./chunk-UV4O7FAL.js";
|
|
190
190
|
import "./chunk-HVQGCKY4.js";
|
|
191
191
|
import "./chunk-4HDYQL3J.js";
|
|
192
192
|
import {
|
|
@@ -315,11 +315,11 @@ import {
|
|
|
315
315
|
ScanBuilder,
|
|
316
316
|
buildLiveQuery,
|
|
317
317
|
executePlan
|
|
318
|
-
} from "./chunk-
|
|
318
|
+
} from "./chunk-BGQ63MCT.js";
|
|
319
319
|
import {
|
|
320
320
|
DEFAULT_JOIN_MAX_ROWS,
|
|
321
321
|
applyJoins
|
|
322
|
-
} from "./chunk-
|
|
322
|
+
} from "./chunk-H4YQSDOI.js";
|
|
323
323
|
import "./chunk-WASHZV34.js";
|
|
324
324
|
import "./chunk-JIUVJ4M7.js";
|
|
325
325
|
import {
|
|
@@ -309,6 +309,12 @@ export declare class Query<T, S extends keyof T = never, Q extends keyof T & str
|
|
|
309
309
|
toArray(opts?: {
|
|
310
310
|
locale?: string;
|
|
311
311
|
}): T[];
|
|
312
|
+
/**
|
|
313
|
+
* Joins need a `JoinContext`. Unreachable in practice — `.join()` throws
|
|
314
|
+
* when one is missing — but belt-and-braces for a plan built through the
|
|
315
|
+
* raw `Query` constructor with joins pre-populated.
|
|
316
|
+
*/
|
|
317
|
+
private requireJoinContext;
|
|
312
318
|
/**
|
|
313
319
|
* Decode this source's Via-covered fields on read (e.g. money: stored
|
|
314
320
|
* scaled-int → canonical decimal), so `query().toArray()` agrees with
|
|
@@ -330,12 +336,19 @@ export declare class Query<T, S extends keyof T = never, Q extends keyof T & str
|
|
|
330
336
|
}): T | null;
|
|
331
337
|
/**
|
|
332
338
|
* Return the number of matching records (after where/filter,
|
|
333
|
-
* before limit). **Joins are NOT applied** — count() reports the
|
|
334
|
-
* left-side cardinality, because joins
|
|
339
|
+
* before limit). **Joins are normally NOT applied** — count() reports the
|
|
340
|
+
* left-side cardinality, because joins are projection-only
|
|
335
341
|
* (they attach an aliased field; they never filter). Running joins
|
|
336
342
|
* here just to discard the aliases would be wasteful, and in strict
|
|
337
343
|
* mode it could throw `DanglingReferenceError` for a call whose
|
|
338
344
|
* intent is purely to count.
|
|
345
|
+
*
|
|
346
|
+
* The exception (#1030) is a `where` clause addressing a join alias. Then
|
|
347
|
+
* the legs must run, because the predicate is part of what is being
|
|
348
|
+
* counted — skipping them would report the unfiltered left cardinality.
|
|
349
|
+
* A strict-mode `DanglingReferenceError` becomes reachable from `count()`
|
|
350
|
+
* in exactly that case, which is acceptable: the caller asked to filter on
|
|
351
|
+
* the joined side, so they asked for the join.
|
|
339
352
|
*/
|
|
340
353
|
count(): number;
|
|
341
354
|
/**
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* including joined reads.
|
|
46
46
|
*/
|
|
47
47
|
import type { RefDescriptor, RefMode } from '../refs.js';
|
|
48
|
+
import type { Clause } from './predicate.js';
|
|
48
49
|
/** Planner strategy for a single join leg. Auto-selected unless overridden. */
|
|
49
50
|
export type JoinStrategy = 'hash' | 'nested';
|
|
50
51
|
/** Default per-side row ceiling before `.join()` throws `JoinTooLargeError`. */
|
|
@@ -168,6 +169,32 @@ export interface JoinContext {
|
|
|
168
169
|
*/
|
|
169
170
|
resolveDictSource?(field: string): JoinableSource | null;
|
|
170
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Split a plan's clauses into those evaluable before the join legs run and
|
|
174
|
+
* those that need the joined shape (#1030).
|
|
175
|
+
*
|
|
176
|
+
* Join legs attach after `where` so the left set can be narrowed (and
|
|
177
|
+
* index-driven) first. That is the right default, but it silently broke any
|
|
178
|
+
* predicate addressing a joined alias: the field did not exist yet, so
|
|
179
|
+
* `readPath` returned `undefined`, nothing matched, and the query returned an
|
|
180
|
+
* empty result with no error.
|
|
181
|
+
*
|
|
182
|
+
* The split is deliberately narrow. When no clause addresses an alias — every
|
|
183
|
+
* query written against the previous behaviour — `postJoin` is empty and the
|
|
184
|
+
* caller takes its original path unchanged. The reordered pipeline therefore
|
|
185
|
+
* only ever runs for queries that match nothing today.
|
|
186
|
+
*
|
|
187
|
+
* KNOWN RESIDUAL: `.filter(r => r.client?.name === 'Ann')` cannot be
|
|
188
|
+
* classified (the predicate is a closure), so it still runs pre-join and still
|
|
189
|
+
* sees no alias. Prefer `.where()` for anything addressing a joined field.
|
|
190
|
+
*
|
|
191
|
+
* Shared by the eager `Query` and the streaming `ScanBuilder` so the two
|
|
192
|
+
* cannot drift on which side a clause belongs to.
|
|
193
|
+
*/
|
|
194
|
+
export declare function splitAroundJoins(clauses: readonly Clause[], joins: readonly JoinLeg[]): {
|
|
195
|
+
readonly preJoin: readonly Clause[];
|
|
196
|
+
readonly postJoin: readonly Clause[];
|
|
197
|
+
};
|
|
171
198
|
/**
|
|
172
199
|
* Apply every join leg in the plan against a base set of left-side
|
|
173
200
|
* rows. Called by the query executor after `where` / `orderBy` /
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
isMVStale,
|
|
7
7
|
markMVStale,
|
|
8
8
|
resolveStaleMVOnRead
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-CBXHHCZT.js";
|
|
10
10
|
import {
|
|
11
11
|
MaterializedViewExecutor
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-6BVER473.js";
|
|
13
13
|
import {
|
|
14
14
|
MaterializedViewRegistry,
|
|
15
15
|
analyzeDependencies,
|
|
@@ -21,7 +21,7 @@ import "../chunk-RXYYFSBB.js";
|
|
|
21
21
|
import "../chunk-CCZJJ4I2.js";
|
|
22
22
|
import "../chunk-5ZKS6ZE2.js";
|
|
23
23
|
import "../chunk-5D2ALSM5.js";
|
|
24
|
-
import "../chunk-
|
|
24
|
+
import "../chunk-H4YQSDOI.js";
|
|
25
25
|
import "../chunk-XZHEOSQ2.js";
|
|
26
26
|
import "../chunk-CADUYBQD.js";
|
|
27
27
|
import "../chunk-IGJCYUT5.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Noydb,
|
|
3
3
|
createNoydb
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-UV4O7FAL.js";
|
|
5
5
|
import "./chunk-HVQGCKY4.js";
|
|
6
6
|
import "./chunk-4HDYQL3J.js";
|
|
7
7
|
import "./chunk-EPFF7LOX.js";
|
|
@@ -36,8 +36,8 @@ import "./chunk-5D2ALSM5.js";
|
|
|
36
36
|
import "./chunk-3JVXVJ7L.js";
|
|
37
37
|
import "./chunk-AURFOK3D.js";
|
|
38
38
|
import "./chunk-7MNBICB5.js";
|
|
39
|
-
import "./chunk-
|
|
40
|
-
import "./chunk-
|
|
39
|
+
import "./chunk-BGQ63MCT.js";
|
|
40
|
+
import "./chunk-H4YQSDOI.js";
|
|
41
41
|
import "./chunk-WASHZV34.js";
|
|
42
42
|
import "./chunk-JIUVJ4M7.js";
|
|
43
43
|
import "./chunk-GQ3BX5EA.js";
|
|
@@ -80,4 +80,4 @@ export {
|
|
|
80
80
|
Noydb,
|
|
81
81
|
createNoydb
|
|
82
82
|
};
|
|
83
|
-
//# sourceMappingURL=noydb-
|
|
83
|
+
//# sourceMappingURL=noydb-FYTCH2GD.js.map
|
package/dist/pod/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
verifyRedirect,
|
|
8
8
|
wrapBundleStore,
|
|
9
9
|
wrapPodStore
|
|
10
|
-
} from "../chunk-
|
|
11
|
-
import "../chunk-
|
|
10
|
+
} from "../chunk-CCVYQFLU.js";
|
|
11
|
+
import "../chunk-UV4O7FAL.js";
|
|
12
12
|
import "../chunk-HVQGCKY4.js";
|
|
13
13
|
import "../chunk-4HDYQL3J.js";
|
|
14
14
|
import {
|
|
@@ -78,8 +78,8 @@ import "../chunk-5D2ALSM5.js";
|
|
|
78
78
|
import "../chunk-3JVXVJ7L.js";
|
|
79
79
|
import "../chunk-AURFOK3D.js";
|
|
80
80
|
import "../chunk-7MNBICB5.js";
|
|
81
|
-
import "../chunk-
|
|
82
|
-
import "../chunk-
|
|
81
|
+
import "../chunk-BGQ63MCT.js";
|
|
82
|
+
import "../chunk-H4YQSDOI.js";
|
|
83
83
|
import "../chunk-WASHZV34.js";
|
|
84
84
|
import "../chunk-JIUVJ4M7.js";
|
|
85
85
|
import "../chunk-GQ3BX5EA.js";
|
package/dist/port/to/index.d.ts
CHANGED
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export type { NoydbStore, NoydbBundleStore, NoydbPodStore, EncryptedEnvelope, VaultSnapshot, TxOp, StoreCapabilities, StoreCredentials, StoreCredentialSource, StoreTime, ListPageResult, } from '../../kernel/types.js';
|
|
19
19
|
export { ConflictError, NetworkError, StoreCapabilityError, BundleVersionConflictError, PodVersionConflictError, UnknownStoreKindError, DuplicateStoreKindError, } from '../../kernel/errors.js';
|
|
20
|
-
export type { StoreClass, StoreDescriptor, StoreBinding, StoreFactory, StoreLocator } from './locator.js';
|
|
21
|
-
export { createStoreLocator } from './locator.js';
|
|
20
|
+
export type { StoreClass, StoreDescriptor, StoreBinding, StoreFactory, StoreLocator, AnyNoydbStore, } from './locator.js';
|
|
21
|
+
export { createStoreLocator, isPodStore } from './locator.js';
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* and errors, no Node built-ins, no crypto. It adds zero runtime
|
|
18
18
|
* dependencies to `@noy-db/hub/to`.
|
|
19
19
|
*/
|
|
20
|
-
import type { NoydbStore, StoreCredentialSource } from '../../kernel/types.js';
|
|
20
|
+
import type { NoydbStore, NoydbPodStore, StoreCredentialSource } from '../../kernel/types.js';
|
|
21
21
|
/**
|
|
22
22
|
* The broad topology bucket a store descriptor's kind falls into —
|
|
23
23
|
* used by callers/UI to group or filter descriptors without knowing every
|
|
@@ -51,22 +51,68 @@ export interface StoreDescriptor {
|
|
|
51
51
|
*/
|
|
52
52
|
export type StoreBinding = unknown;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Either store shape a factory may produce: the 6-method KV `NoydbStore`, or
|
|
55
|
+
* the whole-vault `NoydbPodStore` implemented by `to-drive` / `to-icloud`.
|
|
56
|
+
*
|
|
57
|
+
* The two are DISJOINT, not sub/supertypes — a pod store has none of the six
|
|
58
|
+
* KV methods — which is why moving between them needs a double cast and why
|
|
59
|
+
* `isPodStore()` exists to narrow instead.
|
|
56
60
|
*/
|
|
57
|
-
export type
|
|
61
|
+
export type AnyNoydbStore = NoydbStore | NoydbPodStore;
|
|
62
|
+
/**
|
|
63
|
+
* Narrow an `AnyNoydbStore` to the pod shape. Discriminates on the
|
|
64
|
+
* `kind: 'bundle'` tag `NoydbPodStore` carries for exactly this purpose; a
|
|
65
|
+
* `NoydbStore` has no `kind` field at all.
|
|
66
|
+
*
|
|
67
|
+
* Use this at a `resolve()` boundary instead of casting — a store resolved
|
|
68
|
+
* from a descriptor read out of a pod is only known to be one of the two
|
|
69
|
+
* shapes at runtime.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isPodStore(store: AnyNoydbStore): store is NoydbPodStore;
|
|
72
|
+
/**
|
|
73
|
+
* Reconstructs a live store from a `StoreDescriptor`. Registered against a
|
|
74
|
+
* `StoreLocator` under the descriptor's `kind`.
|
|
75
|
+
*
|
|
76
|
+
* The type parameter says WHICH shape this factory builds. It defaults to
|
|
77
|
+
* `NoydbStore`, so a bare `StoreFactory` means exactly what it always has;
|
|
78
|
+
* a pod-store factory declares `StoreFactory<NoydbPodStore>` and registers
|
|
79
|
+
* without a cast (#988).
|
|
80
|
+
*/
|
|
81
|
+
export type StoreFactory<S extends AnyNoydbStore = NoydbStore> = (descriptor: StoreDescriptor, opts: {
|
|
58
82
|
binding?: StoreBinding;
|
|
59
83
|
credentials?: StoreCredentialSource;
|
|
60
|
-
}) =>
|
|
84
|
+
}) => S | Promise<S>;
|
|
61
85
|
/** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */
|
|
62
86
|
export interface StoreLocator {
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Register a factory for `kind`. Throws if `kind` is already registered.
|
|
89
|
+
*
|
|
90
|
+
* Accepts a factory building EITHER store shape — `S` is inferred from the
|
|
91
|
+
* factory's own return type, so a `NoydbPodStore` factory needs no cast.
|
|
92
|
+
*/
|
|
93
|
+
register<S extends AnyNoydbStore>(kind: string, factory: StoreFactory<S>): void;
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `descriptor` to a live store via its kind's registered factory.
|
|
96
|
+
*
|
|
97
|
+
* Returns `NoydbStore`. If the `kind` was registered with a pod-store
|
|
98
|
+
* factory, the resolved value is really a `NoydbPodStore`: this signature
|
|
99
|
+
* is the seam's remaining unsoundness, kept because widening it to
|
|
100
|
+
* `AnyNoydbStore` would break every existing caller. Where the descriptor's
|
|
101
|
+
* kind is not statically known, prefer `resolveAny()` and `isPodStore()`.
|
|
102
|
+
*/
|
|
66
103
|
resolve(descriptor: StoreDescriptor, opts?: {
|
|
67
104
|
binding?: StoreBinding;
|
|
68
105
|
credentials?: StoreCredentialSource;
|
|
69
106
|
}): NoydbStore | Promise<NoydbStore>;
|
|
107
|
+
/**
|
|
108
|
+
* As `resolve()`, but typed honestly: the registry is keyed by a runtime
|
|
109
|
+
* `kind` string, so which of the two shapes comes back is not knowable
|
|
110
|
+
* statically. Narrow the result with `isPodStore()`.
|
|
111
|
+
*/
|
|
112
|
+
resolveAny(descriptor: StoreDescriptor, opts?: {
|
|
113
|
+
binding?: StoreBinding;
|
|
114
|
+
credentials?: StoreCredentialSource;
|
|
115
|
+
}): AnyNoydbStore | Promise<AnyNoydbStore>;
|
|
70
116
|
}
|
|
71
117
|
/**
|
|
72
118
|
* Creates a fresh, empty `StoreLocator`.
|
package/dist/query/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
ScanBuilder,
|
|
5
5
|
buildLiveQuery,
|
|
6
6
|
executePlan
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-BGQ63MCT.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_JOIN_MAX_ROWS,
|
|
10
10
|
applyJoins,
|
|
11
11
|
resetJoinWarnings
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-H4YQSDOI.js";
|
|
13
13
|
import {
|
|
14
14
|
CollectionIndexes
|
|
15
15
|
} from "../chunk-GQ3BX5EA.js";
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isMVStale,
|
|
7
7
|
markMVStale,
|
|
8
8
|
resolveStaleMVOnRead
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-CBXHHCZT.js";
|
|
10
10
|
import "./chunk-5D2ALSM5.js";
|
|
11
11
|
import "./chunk-3URQC7EH.js";
|
|
12
12
|
import "./chunk-7H73RIE3.js";
|
|
@@ -27,4 +27,4 @@ export {
|
|
|
27
27
|
markMVStale,
|
|
28
28
|
resolveStaleMVOnRead
|
|
29
29
|
};
|
|
30
|
-
//# sourceMappingURL=stale-
|
|
30
|
+
//# sourceMappingURL=stale-4PUTWI2H.js.map
|
package/dist/to/index.js
CHANGED
|
@@ -10,8 +10,18 @@ import {
|
|
|
10
10
|
import "../chunk-PZ5AY32C.js";
|
|
11
11
|
|
|
12
12
|
// src/port/to/locator.ts
|
|
13
|
+
function isPodStore(store) {
|
|
14
|
+
return store.kind === "bundle";
|
|
15
|
+
}
|
|
13
16
|
function createStoreLocator() {
|
|
14
17
|
const factories = /* @__PURE__ */ new Map();
|
|
18
|
+
function lookup(kind) {
|
|
19
|
+
const factory = factories.get(kind);
|
|
20
|
+
if (!factory) {
|
|
21
|
+
throw new UnknownStoreKindError(kind, [...factories.keys()].sort());
|
|
22
|
+
}
|
|
23
|
+
return factory;
|
|
24
|
+
}
|
|
15
25
|
return {
|
|
16
26
|
register(kind, factory) {
|
|
17
27
|
if (factories.has(kind)) {
|
|
@@ -20,11 +30,10 @@ function createStoreLocator() {
|
|
|
20
30
|
factories.set(kind, factory);
|
|
21
31
|
},
|
|
22
32
|
resolve(descriptor, opts = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return factory(descriptor, opts);
|
|
33
|
+
return lookup(descriptor.kind)(descriptor, opts);
|
|
34
|
+
},
|
|
35
|
+
resolveAny(descriptor, opts = {}) {
|
|
36
|
+
return lookup(descriptor.kind)(descriptor, opts);
|
|
28
37
|
}
|
|
29
38
|
};
|
|
30
39
|
}
|
|
@@ -36,6 +45,7 @@ export {
|
|
|
36
45
|
PodVersionConflictError,
|
|
37
46
|
StoreCapabilityError,
|
|
38
47
|
UnknownStoreKindError,
|
|
39
|
-
createStoreLocator
|
|
48
|
+
createStoreLocator,
|
|
49
|
+
isPodStore
|
|
40
50
|
};
|
|
41
51
|
//# sourceMappingURL=index.js.map
|
package/dist/to/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/port/to/locator.ts"],"sourcesContent":["/**\n * The Locator seam (#945 Task 1): a serializable, CREDENTIALLESS store\n * descriptor plus a factory registry that reconstructs a `NoydbStore` from\n * data.\n *\n * A `StoreDescriptor` is the pod-portable \"what store and where\" — kind,\n * broad topology class, and a kind-specific serializable address/options\n * bag. It is deliberately dumb data: no field on it is ever typed as a\n * function, a `StoreCredentialSource`, or a `StoreCredentials` value, so a\n * descriptor can be written into a pod / manifest / sync payload without an\n * audit for leaked secrets. Anything that must resolve device-side or\n * user-side — a short-lived credential, a device-local directory override,\n * a mounted drive handle — travels out-of-band via `resolve()`'s `opts`,\n * never through the descriptor itself.\n *\n * This module is pure TypeScript: no imports beyond the kernel's own types\n * and errors, no Node built-ins, no crypto. It adds zero runtime\n * dependencies to `@noy-db/hub/to`.\n */\nimport type { NoydbStore, StoreCredentialSource } from '../../kernel/types.js'\nimport { DuplicateStoreKindError, UnknownStoreKindError } from '../../kernel/errors.js'\n\n/**\n * The broad topology bucket a store descriptor's kind falls into —\n * used by callers/UI to group or filter descriptors without knowing every\n * concrete `kind` string.\n */\nexport type StoreClass = 'local' | 'browser' | 'lan' | 'cloud'\n\n/**\n * A serializable, CREDENTIALLESS description of a store instance: enough\n * data to reconstruct a `NoydbStore` via a registered `StoreFactory`, and\n * safe to persist or transmit as-is.\n *\n * CREDENTIALLESS BY CONSTRUCTION: no field here may be typed as a function,\n * a `StoreCredentialSource`, or a `StoreCredentials` value. Credentials\n * NEVER ride the descriptor — they are supplied separately (as a\n * `StoreCredentialSource`) to `StoreLocator.resolve()` at resolve time.\n */\nexport interface StoreDescriptor {\n /** The registered factory key (e.g. `'file'`, `'aws-s3'`, `'webdav'`). */\n readonly kind: string\n /** The broad topology bucket this kind falls into. */\n readonly class: StoreClass\n /** Kind-specific serializable location (e.g. `{ dir }`, `{ bucket, region }`). */\n readonly address: unknown\n /** Kind-specific serializable tuning options. */\n readonly options?: unknown\n}\n\n/**\n * A kind-specific, device-local supplement to a `StoreDescriptor` — a\n * directory override, a mount point, a drive handle — that is resolved\n * device-side and never carried inside a pod alongside the descriptor.\n */\nexport type StoreBinding = unknown\n\n/**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/port/to/locator.ts"],"sourcesContent":["/**\n * The Locator seam (#945 Task 1): a serializable, CREDENTIALLESS store\n * descriptor plus a factory registry that reconstructs a `NoydbStore` from\n * data.\n *\n * A `StoreDescriptor` is the pod-portable \"what store and where\" — kind,\n * broad topology class, and a kind-specific serializable address/options\n * bag. It is deliberately dumb data: no field on it is ever typed as a\n * function, a `StoreCredentialSource`, or a `StoreCredentials` value, so a\n * descriptor can be written into a pod / manifest / sync payload without an\n * audit for leaked secrets. Anything that must resolve device-side or\n * user-side — a short-lived credential, a device-local directory override,\n * a mounted drive handle — travels out-of-band via `resolve()`'s `opts`,\n * never through the descriptor itself.\n *\n * This module is pure TypeScript: no imports beyond the kernel's own types\n * and errors, no Node built-ins, no crypto. It adds zero runtime\n * dependencies to `@noy-db/hub/to`.\n */\nimport type { NoydbStore, NoydbPodStore, StoreCredentialSource } from '../../kernel/types.js'\nimport { DuplicateStoreKindError, UnknownStoreKindError } from '../../kernel/errors.js'\n\n/**\n * The broad topology bucket a store descriptor's kind falls into —\n * used by callers/UI to group or filter descriptors without knowing every\n * concrete `kind` string.\n */\nexport type StoreClass = 'local' | 'browser' | 'lan' | 'cloud'\n\n/**\n * A serializable, CREDENTIALLESS description of a store instance: enough\n * data to reconstruct a `NoydbStore` via a registered `StoreFactory`, and\n * safe to persist or transmit as-is.\n *\n * CREDENTIALLESS BY CONSTRUCTION: no field here may be typed as a function,\n * a `StoreCredentialSource`, or a `StoreCredentials` value. Credentials\n * NEVER ride the descriptor — they are supplied separately (as a\n * `StoreCredentialSource`) to `StoreLocator.resolve()` at resolve time.\n */\nexport interface StoreDescriptor {\n /** The registered factory key (e.g. `'file'`, `'aws-s3'`, `'webdav'`). */\n readonly kind: string\n /** The broad topology bucket this kind falls into. */\n readonly class: StoreClass\n /** Kind-specific serializable location (e.g. `{ dir }`, `{ bucket, region }`). */\n readonly address: unknown\n /** Kind-specific serializable tuning options. */\n readonly options?: unknown\n}\n\n/**\n * A kind-specific, device-local supplement to a `StoreDescriptor` — a\n * directory override, a mount point, a drive handle — that is resolved\n * device-side and never carried inside a pod alongside the descriptor.\n */\nexport type StoreBinding = unknown\n\n/**\n * Either store shape a factory may produce: the 6-method KV `NoydbStore`, or\n * the whole-vault `NoydbPodStore` implemented by `to-drive` / `to-icloud`.\n *\n * The two are DISJOINT, not sub/supertypes — a pod store has none of the six\n * KV methods — which is why moving between them needs a double cast and why\n * `isPodStore()` exists to narrow instead.\n */\nexport type AnyNoydbStore = NoydbStore | NoydbPodStore\n\n/**\n * Narrow an `AnyNoydbStore` to the pod shape. Discriminates on the\n * `kind: 'bundle'` tag `NoydbPodStore` carries for exactly this purpose; a\n * `NoydbStore` has no `kind` field at all.\n *\n * Use this at a `resolve()` boundary instead of casting — a store resolved\n * from a descriptor read out of a pod is only known to be one of the two\n * shapes at runtime.\n */\nexport function isPodStore(store: AnyNoydbStore): store is NoydbPodStore {\n return (store as NoydbPodStore).kind === 'bundle'\n}\n\n/**\n * Reconstructs a live store from a `StoreDescriptor`. Registered against a\n * `StoreLocator` under the descriptor's `kind`.\n *\n * The type parameter says WHICH shape this factory builds. It defaults to\n * `NoydbStore`, so a bare `StoreFactory` means exactly what it always has;\n * a pod-store factory declares `StoreFactory<NoydbPodStore>` and registers\n * without a cast (#988).\n */\nexport type StoreFactory<S extends AnyNoydbStore = NoydbStore> = (\n descriptor: StoreDescriptor,\n opts: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n) => S | Promise<S>\n\n/** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */\nexport interface StoreLocator {\n /**\n * Register a factory for `kind`. Throws if `kind` is already registered.\n *\n * Accepts a factory building EITHER store shape — `S` is inferred from the\n * factory's own return type, so a `NoydbPodStore` factory needs no cast.\n */\n register<S extends AnyNoydbStore>(kind: string, factory: StoreFactory<S>): void\n /**\n * Resolve `descriptor` to a live store via its kind's registered factory.\n *\n * Returns `NoydbStore`. If the `kind` was registered with a pod-store\n * factory, the resolved value is really a `NoydbPodStore`: this signature\n * is the seam's remaining unsoundness, kept because widening it to\n * `AnyNoydbStore` would break every existing caller. Where the descriptor's\n * kind is not statically known, prefer `resolveAny()` and `isPodStore()`.\n */\n resolve(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): NoydbStore | Promise<NoydbStore>\n /**\n * As `resolve()`, but typed honestly: the registry is keyed by a runtime\n * `kind` string, so which of the two shapes comes back is not knowable\n * statically. Narrow the result with `isPodStore()`.\n */\n resolveAny(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): AnyNoydbStore | Promise<AnyNoydbStore>\n}\n\n/**\n * Creates a fresh, empty `StoreLocator`.\n *\n * DUPLICATE REGISTRATION: `register()` throws if `kind` is already\n * registered, rather than silently overwriting it (last-wins). A\n * composition root that registers the same kind twice is almost always a\n * mistake — a copy-pasted setup block, two satellite packages fighting\n * over the same kind string — and last-wins would hide it behind whichever\n * registration happened to run last. This mirrors the\n * `DuplicateBehaviorNameError` philosophy (#947): fail loudly at the\n * mistake's source instead of silently at some unrelated `resolve()` call\n * later.\n */\nexport function createStoreLocator(): StoreLocator {\n const factories = new Map<string, StoreFactory<AnyNoydbStore>>()\n\n function lookup(kind: string): StoreFactory<AnyNoydbStore> {\n const factory = factories.get(kind)\n if (!factory) {\n throw new UnknownStoreKindError(kind, [...factories.keys()].sort())\n }\n return factory\n }\n\n return {\n register(kind, factory) {\n if (factories.has(kind)) {\n throw new DuplicateStoreKindError(kind)\n }\n factories.set(kind, factory)\n },\n resolve(descriptor, opts = {}) {\n // The one place the seam's `resolve(): NoydbStore` signature is paid\n // for. A pod-store `kind` really returns a `NoydbPodStore`; the registry\n // is keyed by a runtime string so nothing here can prove otherwise.\n // Concentrated in the module that owns the invariant rather than\n // duplicated as `as unknown as StoreFactory` in every pod-store package\n // (#988). `resolveAny()` is the honest signature over the same call.\n return lookup(descriptor.kind)(descriptor, opts) as NoydbStore | Promise<NoydbStore>\n },\n resolveAny(descriptor, opts = {}) {\n return lookup(descriptor.kind)(descriptor, opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;AA4EO,SAAS,WAAW,OAA8C;AACvE,SAAQ,MAAwB,SAAS;AAC3C;AA8DO,SAAS,qBAAmC;AACjD,QAAM,YAAY,oBAAI,IAAyC;AAE/D,WAAS,OAAO,MAA2C;AACzD,UAAM,UAAU,UAAU,IAAI,IAAI;AAClC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,sBAAsB,MAAM,CAAC,GAAG,UAAU,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,SAAS;AACtB,UAAI,UAAU,IAAI,IAAI,GAAG;AACvB,cAAM,IAAI,wBAAwB,IAAI;AAAA,MACxC;AACA,gBAAU,IAAI,MAAM,OAAO;AAAA,IAC7B;AAAA,IACA,QAAQ,YAAY,OAAO,CAAC,GAAG;AAO7B,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,IACA,WAAW,YAAY,OAAO,CAAC,GAAG;AAChC,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/hub",
|
|
3
|
-
"version": "0.6.0-pre.
|
|
3
|
+
"version": "0.6.0-pre.11",
|
|
4
4
|
"description": "Zero-knowledge, offline-first, encrypted document store — core library with AES-256-GCM, PBKDF2, multi-user keyring, and sync engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -210,14 +210,14 @@
|
|
|
210
210
|
"node": ">=22.0.0"
|
|
211
211
|
},
|
|
212
212
|
"dependencies": {
|
|
213
|
-
"@noy-db/attestation": "0.6.0-pre.
|
|
213
|
+
"@noy-db/attestation": "0.6.0-pre.11"
|
|
214
214
|
},
|
|
215
215
|
"devDependencies": {
|
|
216
216
|
"@types/node": "^22.0.0",
|
|
217
217
|
"esbuild": "^0.25.0",
|
|
218
218
|
"zod": "^4.0.0",
|
|
219
219
|
"zod-to-json-schema": "^3.25.2",
|
|
220
|
-
"@noy-db/on-shamir": "0.6.0-pre.
|
|
220
|
+
"@noy-db/on-shamir": "0.6.0-pre.11"
|
|
221
221
|
},
|
|
222
222
|
"peerDependencies": {
|
|
223
223
|
"zod-to-json-schema": "^3.25.0"
|