@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.
Files changed (36) hide show
  1. package/dist/cargo/index.js +1 -1
  2. package/dist/{chunk-GAG7QOVO.js → chunk-6BVER473.js} +2 -2
  3. package/dist/{chunk-3737A6EA.js → chunk-BGQ63MCT.js} +74 -17
  4. package/dist/chunk-BGQ63MCT.js.map +1 -0
  5. package/dist/{chunk-BEFVYAR5.js → chunk-CBXHHCZT.js} +2 -2
  6. package/dist/{chunk-PPMBY7YS.js → chunk-CCVYQFLU.js} +2 -2
  7. package/dist/{chunk-XEJA6K76.js → chunk-GU7A7MQA.js} +2 -2
  8. package/dist/{chunk-E6427JU2.js → chunk-H4YQSDOI.js} +17 -1
  9. package/dist/chunk-H4YQSDOI.js.map +1 -0
  10. package/dist/{chunk-N3BI6P3J.js → chunk-UV4O7FAL.js} +8 -8
  11. package/dist/{dispatch-FZEAWKVM.js → dispatch-23SKSHPD.js} +5 -5
  12. package/dist/{executor-YIQB2S6N.js → executor-FKZURXVV.js} +3 -3
  13. package/dist/index.js +7 -7
  14. package/dist/kernel/query/builder.d.ts +15 -2
  15. package/dist/kernel/query/join.d.ts +27 -0
  16. package/dist/materialized-views/index.js +3 -3
  17. package/dist/{noydb-SNZONAYA.js → noydb-FYTCH2GD.js} +4 -4
  18. package/dist/pod/index.js +4 -4
  19. package/dist/port/to/index.d.ts +2 -2
  20. package/dist/port/to/locator.d.ts +54 -8
  21. package/dist/query/index.js +2 -2
  22. package/dist/{stale-ARHFVL5U.js → stale-4PUTWI2H.js} +2 -2
  23. package/dist/to/index.js +16 -6
  24. package/dist/to/index.js.map +1 -1
  25. package/package.json +3 -3
  26. package/dist/chunk-3737A6EA.js.map +0 -1
  27. package/dist/chunk-E6427JU2.js.map +0 -1
  28. /package/dist/{chunk-GAG7QOVO.js.map → chunk-6BVER473.js.map} +0 -0
  29. /package/dist/{chunk-BEFVYAR5.js.map → chunk-CBXHHCZT.js.map} +0 -0
  30. /package/dist/{chunk-PPMBY7YS.js.map → chunk-CCVYQFLU.js.map} +0 -0
  31. /package/dist/{chunk-XEJA6K76.js.map → chunk-GU7A7MQA.js.map} +0 -0
  32. /package/dist/{chunk-N3BI6P3J.js.map → chunk-UV4O7FAL.js.map} +0 -0
  33. /package/dist/{dispatch-FZEAWKVM.js.map → dispatch-23SKSHPD.js.map} +0 -0
  34. /package/dist/{executor-YIQB2S6N.js.map → executor-FKZURXVV.js.map} +0 -0
  35. /package/dist/{noydb-SNZONAYA.js.map → noydb-FYTCH2GD.js.map} +0 -0
  36. /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-3737A6EA.js";
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-ARHFVL5U.js");
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-FZEAWKVM.js");
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-FZEAWKVM.js");
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-ARHFVL5U.js");
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-YIQB2S6N.js");
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-ARHFVL5U.js");
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-N3BI6P3J.js.map
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-YIQB2S6N.js"));
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-ARHFVL5U.js");
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-YIQB2S6N.js"));
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-ARHFVL5U.js");
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-FZEAWKVM.js.map
72
+ //# sourceMappingURL=dispatch-23SKSHPD.js.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  MaterializedViewExecutor
3
- } from "./chunk-GAG7QOVO.js";
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-E6427JU2.js";
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-YIQB2S6N.js.map
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-XEJA6K76.js";
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-BEFVYAR5.js";
94
- import "./chunk-GAG7QOVO.js";
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-PPMBY7YS.js";
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-N3BI6P3J.js";
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-3737A6EA.js";
318
+ } from "./chunk-BGQ63MCT.js";
319
319
  import {
320
320
  DEFAULT_JOIN_MAX_ROWS,
321
321
  applyJoins
322
- } from "./chunk-E6427JU2.js";
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 in are projection-only
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-BEFVYAR5.js";
9
+ } from "../chunk-CBXHHCZT.js";
10
10
  import {
11
11
  MaterializedViewExecutor
12
- } from "../chunk-GAG7QOVO.js";
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-E6427JU2.js";
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-N3BI6P3J.js";
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-3737A6EA.js";
40
- import "./chunk-E6427JU2.js";
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-SNZONAYA.js.map
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-PPMBY7YS.js";
11
- import "../chunk-N3BI6P3J.js";
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-3737A6EA.js";
82
- import "../chunk-E6427JU2.js";
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";
@@ -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
- * Reconstructs a live `NoydbStore` from a `StoreDescriptor`. Registered
55
- * against a `StoreLocator` under the descriptor's `kind`.
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 StoreFactory = (descriptor: StoreDescriptor, opts: {
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
- }) => NoydbStore | Promise<NoydbStore>;
84
+ }) => S | Promise<S>;
61
85
  /** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */
62
86
  export interface StoreLocator {
63
- /** Register a factory for `kind`. Throws if `kind` is already registered. */
64
- register(kind: string, factory: StoreFactory): void;
65
- /** Resolve `descriptor` to a live store via its kind's registered factory. */
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`.
@@ -4,12 +4,12 @@ import {
4
4
  ScanBuilder,
5
5
  buildLiveQuery,
6
6
  executePlan
7
- } from "../chunk-3737A6EA.js";
7
+ } from "../chunk-BGQ63MCT.js";
8
8
  import {
9
9
  DEFAULT_JOIN_MAX_ROWS,
10
10
  applyJoins,
11
11
  resetJoinWarnings
12
- } from "../chunk-E6427JU2.js";
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-BEFVYAR5.js";
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-ARHFVL5U.js.map
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
- const factory = factories.get(descriptor.kind);
24
- if (!factory) {
25
- throw new UnknownStoreKindError(descriptor.kind, [...factories.keys()].sort());
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
@@ -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 * Reconstructs a live `NoydbStore` from a `StoreDescriptor`. Registered\n * against a `StoreLocator` under the descriptor's `kind`.\n */\nexport type StoreFactory = (\n descriptor: StoreDescriptor,\n opts: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n) => NoydbStore | Promise<NoydbStore>\n\n/** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */\nexport interface StoreLocator {\n /** Register a factory for `kind`. Throws if `kind` is already registered. */\n register(kind: string, factory: StoreFactory): void\n /** Resolve `descriptor` to a live store via its kind's registered factory. */\n resolve(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): NoydbStore | Promise<NoydbStore>\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>()\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 const factory = factories.get(descriptor.kind)\n if (!factory) {\n throw new UnknownStoreKindError(descriptor.kind, [...factories.keys()].sort())\n }\n return factory(descriptor, opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;AA0FO,SAAS,qBAAmC;AACjD,QAAM,YAAY,oBAAI,IAA0B;AAEhD,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;AAC7B,YAAM,UAAU,UAAU,IAAI,WAAW,IAAI;AAC7C,UAAI,CAAC,SAAS;AACZ,cAAM,IAAI,sBAAsB,WAAW,MAAM,CAAC,GAAG,UAAU,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,MAC/E;AACA,aAAO,QAAQ,YAAY,IAAI;AAAA,IACjC;AAAA,EACF;AACF;","names":[]}
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.10",
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.10"
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.10"
220
+ "@noy-db/on-shamir": "0.6.0-pre.11"
221
221
  },
222
222
  "peerDependencies": {
223
223
  "zod-to-json-schema": "^3.25.0"