@spooky-sync/client-solid2 0.0.1-canary.208 → 0.0.1-canary.209

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/index.cjs CHANGED
@@ -142,6 +142,25 @@ function usePendingMutations() {
142
142
  * inserted as-is and trailing rows are dropped, so add / remove / reorder all
143
143
  * reach coarse readers.
144
144
  */
145
+ /**
146
+ * Field-level equality for the merge. `===` is not enough: the decoder hands
147
+ * out a fresh RecordId / Date instance for every record-link and datetime
148
+ * column on every emission, so by identity every one of those fields "changed"
149
+ * on every update - and a store write on an unchanged `id` re-ran everything a
150
+ * page keyed on that row (a thread's anchor, its composer, its whole subtree).
151
+ * Compare those wrappers by value; everything else stays identity (nested
152
+ * objects and arrays are reconciled by their own writes).
153
+ */
154
+ function sameValue(a, b) {
155
+ if (a === b) return true;
156
+ if (a == null || b == null) return false;
157
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
158
+ if (typeof a === "object" && typeof b === "object") {
159
+ const ctor = a.constructor;
160
+ if (ctor && ctor === b.constructor && ctor !== Object && ctor !== Array) return String(a) === String(b);
161
+ }
162
+ return false;
163
+ }
145
164
  function mergeRows(draft, next, key = "id") {
146
165
  const byKey = /* @__PURE__ */ new Map();
147
166
  for (const row of draft) {
@@ -153,7 +172,7 @@ function mergeRows(draft, next, key = "id") {
153
172
  const k = incoming?.[key];
154
173
  const reuse = k !== void 0 ? byKey.get(String(k)) : void 0;
155
174
  if (reuse) {
156
- for (const field of Object.keys(incoming)) if (reuse[field] !== incoming[field]) reuse[field] = incoming[field];
175
+ for (const field of Object.keys(incoming)) if (!sameValue(reuse[field], incoming[field])) reuse[field] = incoming[field];
157
176
  for (const field of Object.keys((0, solid_js.snapshot)(reuse))) if (!(field in incoming)) delete reuse[field];
158
177
  if (draft[i] !== reuse) draft[i] = reuse;
159
178
  } else if (draft[i] !== incoming) draft[i] = incoming;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Sp00kyClient","RecordId"],"sources":["../src/lib/conflate.ts","../src/lib/from-subscription.ts","../src/lib/context.ts","../src/lib/merge-rows.ts","../src/lib/create-query.ts","../src/lib/create-preload.ts","../src/lib/use-sync-status.ts","../src/lib/use-storage-status.ts","../src/lib/use-crdt-field.ts","../src/lib/use-feature-flag.ts","../src/lib/use-app-release.ts","../src/lib/use-file-upload.ts","../src/lib/use-download-file.ts","../src/lib/use-blurhash.ts","../src/lib/use-bucket-image.ts","../src/lib/Blurhash.ts","../src/lib/BucketImage.ts","../src/lib/Sp00kyProvider.ts","../src/lib/create-submission.ts","../src/index.ts"],"sourcesContent":["/**\n * Latest-wins async iterable over a subscribe-callback source.\n *\n * Bridges spooky's push-callback subscriptions into the AsyncIterable shape\n * Solid 2 computations consume natively. Each spooky emission is a full result\n * set, so intermediate values are droppable: only the newest unconsumed value\n * is buffered, and a pending pull resolves with it immediately.\n *\n * Teardown contract (probed in rc-semantics.test.ts): Solid 2 does NOT\n * terminate a superseded/disposed computation's async generator — no\n * `return()`, no `finally`. Consumers MUST call `it.return()` themselves from\n * an `onCleanup` registered synchronously in the compute scope. `return()`\n * unsubscribes (awaiting the unsubscribe if the subscribe returned a promise,\n * as `sp00ky.subscribe` does) and resolves any parked pull as done.\n */\nexport function conflate<T>(\n subscribe: (cb: (v: T) => void) => (() => void) | Promise<() => void>\n): AsyncIterable<T> {\n return {\n [Symbol.asyncIterator](): AsyncIterator<T> {\n let buffered: { v: T } | undefined;\n let resolveNext: ((r: IteratorResult<T>) => void) | undefined;\n let done = false;\n\n const unsubMaybe = subscribe((v) => {\n if (done) return;\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = undefined;\n r({ value: v, done: false });\n } else {\n buffered = { v };\n }\n });\n\n const finish = () => {\n if (done) return;\n done = true;\n buffered = undefined;\n // Unsubscribe may still be in flight (async registration); chain it.\n Promise.resolve(unsubMaybe)\n .then((unsub) => unsub())\n .catch(() => {\n // Registration failed — there is nothing to unsubscribe.\n });\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = undefined;\n r({ value: undefined as never, done: true });\n }\n };\n\n return {\n next(): Promise<IteratorResult<T>> {\n if (done) return Promise.resolve({ value: undefined as never, done: true });\n if (buffered) {\n const v = buffered.v;\n buffered = undefined;\n return Promise.resolve({ value: v, done: false });\n }\n return new Promise<IteratorResult<T>>((r) => (resolveNext = r));\n },\n return(): Promise<IteratorResult<T>> {\n finish();\n return Promise.resolve({ value: undefined as never, done: true });\n },\n throw(e: unknown): Promise<IteratorResult<T>> {\n finish();\n return Promise.reject(e);\n },\n };\n },\n };\n}\n","import { createMemo, onCleanup, type Accessor } from 'solid-js';\nimport { conflate } from './conflate';\n\n/**\n * Reactive view over a spooky subscribe-callback API.\n *\n * The memo's async generator pulls from a conflated (latest-wins) iterator;\n * `initial` is committed as the memo's `loadingValue`, so the accessor is\n * readable synchronously from birth and never suspends. Spooky's subscribe\n * APIs fire immediately with the current value, so the real value lands within\n * a tick of the first read.\n *\n * Teardown is manual by contract (see conflate.ts): onCleanup terminates the\n * iterator, which unsubscribes.\n */\nexport function fromSubscription<T>(\n subscribe: (cb: (v: T) => void) => (() => void) | Promise<() => void>,\n initial: T\n): Accessor<T> {\n return createMemo(\n async function* (): AsyncGenerator<T> {\n const it = conflate(subscribe)[Symbol.asyncIterator]();\n onCleanup(() => void it.return?.());\n while (true) {\n const r = await it.next();\n if (r.done) break;\n yield r.value;\n }\n },\n { loadingValue: initial }\n );\n}\n","import { createContext, useContext, type Accessor } from 'solid-js';\nimport type { SchemaStructure } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { fromSubscription } from './from-subscription';\n\n// Solid 2: the context object doubles as its provider component —\n// <Sp00kyContext value={db}>{children}</Sp00kyContext>.\nexport const Sp00kyContext = createContext<SyncedDb<any>>();\n\nexport function useDb<S extends SchemaStructure>(): SyncedDb<S> {\n try {\n return useContext(Sp00kyContext) as SyncedDb<S>;\n } catch {\n // Solid 2 throws ContextNotFoundError; rethrow with actionable guidance.\n throw new Error(\n 'useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.'\n );\n }\n}\n\n/**\n * Count of locally-committed mutations not yet acknowledged by the server.\n * Drive an \"unsaved changes\" indicator off this.\n */\nexport function usePendingMutations(): Accessor<number> {\n const db = useDb();\n return fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);\n}\n","import { snapshot } from 'solid-js';\n\n/**\n * Keyed in-place merge of a fresh row list into a store draft array.\n *\n * Why not `reconcile(rows, 'id')`: in Solid 2 rc.1 a reconcile REPLACES the row\n * objects, so a component that captured `data()[0]` sees a different object\n * after the next emission, and anything keyed on row identity (`<For>` without\n * an explicit key, a memo comparing rows) re-creates its subtree on every live\n * update. Mutating the draft in place keeps the identity AND keeps updates\n * fine-grained: only the fields that actually changed are written, so a row\n * whose data is unchanged notifies nobody.\n *\n * Rows are matched by `key` (default `id`); unmatched incoming rows are\n * inserted as-is and trailing rows are dropped, so add / remove / reorder all\n * reach coarse readers.\n */\nexport function mergeRows(draft: any[], next: any[], key = 'id'): void {\n const byKey = new Map<any, any>();\n for (const row of draft) {\n const k = row?.[key];\n if (k !== undefined) byKey.set(String(k), row);\n }\n\n for (let i = 0; i < next.length; i++) {\n const incoming = next[i];\n const k = incoming?.[key];\n const reuse = k !== undefined ? byKey.get(String(k)) : undefined;\n\n if (reuse) {\n for (const field of Object.keys(incoming)) {\n if (reuse[field] !== incoming[field]) reuse[field] = incoming[field];\n }\n // `snapshot` first: iterating the proxy's own keys while deleting from it\n // is not safe, and the raw object is what carries the stale fields.\n for (const field of Object.keys(snapshot(reuse))) {\n if (!(field in incoming)) delete reuse[field];\n }\n if (draft[i] !== reuse) draft[i] = reuse;\n } else if (draft[i] !== incoming) {\n draft[i] = incoming;\n }\n }\n\n if (draft.length > next.length) draft.splice(next.length);\n}\n","import type {\n ColumnSchema,\n FinalQuery,\n SchemaStructure,\n TableNames,\n QueryResult,\n} from '@spooky-sync/query-builder';\nimport {\n createEffect,\n createMemo,\n createSignal,\n createStore,\n onCleanup,\n type Accessor,\n} from 'solid-js';\nimport { SyncedDb } from '..';\nimport type { Sp00kyQueryResultPromise } from '@spooky-sync/core';\nimport { useDb } from './context';\nimport { mergeRows } from './merge-rows';\n\ntype QueryArg<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n> =\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | (() =>\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | null\n | undefined);\n\nexport type QueryOptions = {\n enabled?: () => boolean;\n /**\n * Tear down the query (remote `_00_query` view + local WASM view) when this\n * hook is disposed and no other subscriber remains, instead of keeping it\n * resident for cheap re-subscription. Use for viewport-windowed lists that\n * mount/unmount a query per scroll window and want off-screen windows\n * cancelled. Trade-off: scrolling back to a torn-down window re-registers it.\n */\n deregisterOnCleanup?: boolean;\n};\n\nexport type CreateQueryResult<TData> = {\n /**\n * Reactive result. Never suspends and never throws: born as an empty\n * committed value (`[]` / `null`) and reconciled in place (keyed by `id`) on\n * every live emission — unchanged rows keep identity, and coarse readers\n * (`<For>`) are notified on add/remove/reorder.\n */\n data: Accessor<TData>;\n /**\n * Suspending read of the same result for `<Loading>` users: throws Solid's\n * not-ready protocol until the query has delivered its first real result\n * (or errored, in which case it returns the empty value and `error()` is\n * set). Read this inside a `<Loading>` boundary.\n */\n ready: Accessor<TData>;\n error: Accessor<Error | undefined>;\n isLoading: Accessor<boolean>;\n isFetching: Accessor<boolean>;\n /**\n * True once the query has delivered a result AND no fetch cycle is in\n * flight (registration + initial sync included). While settled, results are\n * authoritative: a windowed query returning fewer rows than its LIMIT\n * really is the end of the list. Resets when the query identity changes.\n */\n isSettled: Accessor<boolean>;\n};\n\n// Overload: context-based (no explicit db)\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions\n): CreateQueryResult<TData>;\n\n// Overload: explicit db\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n db: SyncedDb<S>,\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions\n): CreateQueryResult<TData>;\n\n// Implementation\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends {\n columns: Record<string, ColumnSchema>;\n },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n dbOrQuery: SyncedDb<S> | QueryArg<S, TableName, T, RelatedFields, IsOne>,\n queryOrOptions?: QueryArg<S, TableName, T, RelatedFields, IsOne> | QueryOptions,\n maybeOptions?: QueryOptions\n): CreateQueryResult<TData> {\n let db: SyncedDb<S>;\n let finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>;\n let options: QueryOptions | undefined;\n\n if (dbOrQuery instanceof SyncedDb) {\n db = dbOrQuery;\n finalQuery = queryOrOptions as QueryArg<S, TableName, T, RelatedFields, IsOne>;\n options = maybeOptions;\n } else {\n db = useDb<S>();\n finalQuery = dbOrQuery;\n options = queryOrOptions as QueryOptions | undefined;\n }\n\n const sp00ky = db.getSp00ky();\n\n // Status channel. Written from subscription callbacks and generator\n // continuations, which run outside any tracking scope — `ownedWrite` opts\n // these signals out of Solid 2's owned-scope write guard.\n const [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true });\n const [isFetched, setIsFetched] = createSignal(false, { ownedWrite: true });\n const [isFetching, setIsFetching] = createSignal(false, { ownedWrite: true });\n\n // The hash of the currently-installed subscription, for opt-in deregister on\n // dispose (see `deregisterOnCleanup`).\n let activeHash: string | undefined;\n\n // Results live in a plain store, written from the engine's subscription\n // callback and reconciled in place keyed by `id`, so unchanged rows keep\n // identity and coarse readers (`<For>`) are notified on add/remove/reorder.\n //\n // NOT an async-generator projection. A live query's generator never returns:\n // it awaits the next emission forever, which leaves its node permanently\n // PENDING. Solid 2 holds a navigation transition until every pending node in\n // the new tree settles, so with a generator here the first client-side\n // navigation into a screen that opens a query never commits: the URL and\n // effects update while the old DOM stays on screen, with no error and no\n // <Loading> fallback that can rescue it. Subscription writes settle\n // immediately, which is what the Solid 1 binding did too.\n //\n // Wrapped in an object so `one()` queries (row object or null) and list\n // queries share one store shape; `mergeRows` keys `value`'s contents by id.\n const [store, setStore] = createStore<{ value: TData }>({ value: null as TData });\n\n // Identity of the installed subscription, so a superseded run cannot write\n // results for a query the caller has already moved off.\n let runId = 0;\n\n // Woken by the first result or error, for the suspending `ready()` read.\n let readyWaiters: (() => void)[] = [];\n const wakeReady = () => {\n const waiters = readyWaiters;\n readyWaiters = [];\n for (const w of waiters) w();\n };\n\n // Tracked: the query identity and the `enabled` gate. Untracked apply: the\n // registration + subscription, whose teardown is the returned cleanup.\n createEffect(\n () => {\n const enabled = options?.enabled?.() ?? true;\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n return { enabled, query };\n },\n ({ enabled, query }) => {\n const myRun = ++runId;\n // A new identity starts clean: a previous identity's failure must not\n // keep this one out of its loading state.\n setIsFetched(false);\n setError(undefined);\n if (!enabled || !query) return;\n\n const cleanups: (() => void)[] = [];\n let disposed = false;\n // `sp00ky.subscribe` resolves its unsubscribe asynchronously; the status\n // subscription returns one directly. Accept both, and if teardown already\n // happened while the promise was in flight, unsubscribe immediately.\n const addCleanup = (c: (() => void) | Promise<(() => void) | undefined> | undefined) => {\n if (!c) return;\n if (typeof c === 'function') {\n if (disposed) c();\n else cleanups.push(c);\n return;\n }\n void Promise.resolve(c).then((fn) => {\n if (typeof fn !== 'function') return;\n if (disposed) fn();\n else cleanups.push(fn);\n });\n };\n\n /**\n * Registration can fail — the canonical case is the SSP answering 503\n * NOT_READY while it bootstraps. Surface it as `error()` instead of\n * throwing into the graph: the sync scheduler retries the registration\n * underneath, so a transient failure still recovers, and a spinner\n * driven by `isLoading()` resolves via `error()`.\n */\n query\n .run()\n .then(({ hash }: { hash: string }) => {\n if (disposed || myRun !== runId) return;\n activeHash = hash;\n\n // Mirror the query's fetch status so the UI can show a \"loading\n // more\" state while the sync engine pulls records in the background.\n addCleanup(\n sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === 'fetching'), {\n immediate: true,\n })\n );\n\n let isFirstCall = true;\n addCleanup(\n sp00ky.subscribe(\n hash,\n (rows: Record<string, any>[]) => {\n if (disposed || myRun !== runId) return;\n const queryData = (query.isOne ? (rows[0] ?? null) : rows) as TData;\n // The first (immediate) callback with no data likely means the\n // local DB has not synced yet — don't mark as fetched, so the\n // UI keeps showing its loading state.\n const hasData = query.isOne\n ? queryData !== null && queryData !== undefined\n : rows.length > 0;\n if (!isFirstCall || hasData) {\n setIsFetched(true);\n wakeReady();\n }\n isFirstCall = false;\n\n const t0 = performance.now();\n setStore((s) => {\n if (query.isOne || queryData === null || !Array.isArray(s.value)) {\n s.value = queryData;\n } else {\n mergeRows(s.value as any[], queryData as any[]);\n }\n });\n sp00ky.reportFrontendTiming(hash, performance.now() - t0);\n },\n { immediate: true }\n )\n );\n })\n .catch((err: unknown) => {\n if (disposed || myRun !== runId) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n wakeReady();\n });\n\n return () => {\n disposed = true;\n for (const c of cleanups) c();\n };\n }\n );\n\n // Fallback empty value served before the first emission of a list query.\n const emptyList = [] as unknown as TData;\n\n const data: Accessor<TData> = () => {\n const v = store.value;\n if (v === null || v === undefined) {\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n if (query && !query.isOne) return emptyList;\n }\n return v as TData;\n };\n\n // Suspending read: pends until the first real result (or error). Reading it\n // inside <Loading> integrates with Solid 2's boundary protocol; `data` stays\n // non-throwing.\n //\n // `lazy` matters, and so does the promise actually resolving: an unresolved\n // async node stays PENDING, every queue flush carries pending nodes forward,\n // and a navigation transition waits on them. Lazy means only a query whose\n // `ready()` is read creates the node at all, and the deferred below is\n // settled by the same emission that flips `isFetched`.\n const readyGate = createMemo(\n async (): Promise<true> => {\n if (isFetched() || error()) return true;\n await new Promise<void>((resolve) => readyWaiters.push(resolve));\n return true;\n },\n { lazy: true }\n );\n const ready: Accessor<TData> = () => {\n readyGate();\n return data();\n };\n\n // Tear down the live subscription when the hook's owner is disposed. The\n // projection's own onCleanup (inside the compute) already unsubscribes; this\n // hook-scope cleanup only handles the opt-in query deregistration.\n onCleanup(() => {\n // Opt-in: cancel the query once this hook (its last subscriber) is gone.\n // The compute's cleanup removed this hook's callback, so deregisterQuery's\n // refcount guard sees the true remaining-subscriber count.\n if (options?.deregisterOnCleanup && activeHash) {\n sp00ky.deregisterQuery(activeHash);\n }\n });\n\n const isLoading = () => !isFetched() && error() === undefined;\n const isSettled = () => isFetched() && !isFetching();\n\n return {\n data,\n ready,\n error,\n isLoading,\n isFetching,\n isSettled,\n };\n}\n\n/** @deprecated Renamed `createQuery` in the Solid 2 binding. */\nexport const useQuery = createQuery;\n","import type {\n ColumnSchema,\n FinalQuery,\n SchemaStructure,\n TableNames,\n} from '@spooky-sync/query-builder';\nimport { createEffect } from 'solid-js';\nimport { SyncedDb } from '..';\nimport type {\n Sp00kyQueryResultPromise,\n PreloadOptions as CorePreloadOptions,\n} from '@spooky-sync/core';\nimport { useDb } from './context';\n\ntype PreloadArg<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n> =\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | (() =>\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | null\n | undefined);\n\ntype PreloadOptions = CorePreloadOptions & {\n /** Only preload while this returns true (defaults to always). */\n enabled?: () => boolean;\n};\n\n// Overload: context-based (no explicit db)\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n options?: PreloadOptions\n): void;\n\n// Overload: explicit db\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n db: SyncedDb<S>,\n finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n options?: PreloadOptions\n): void;\n\n/**\n * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a\n * function so it tracks reactive deps), dedupes on the query's stable identity\n * hash, and warms it into the local cache via `db.preload`. No subscription and\n * no cleanup: preload registers nothing that needs tearing down.\n *\n * Typical use: inside a list row, preload the detail query the user is likely\n * to open next, so navigation paints from cache instead of the network.\n */\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n dbOrQuery: SyncedDb<S> | PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n queryOrOptions?: PreloadArg<S, TableName, T, RelatedFields, IsOne> | PreloadOptions,\n maybeOptions?: PreloadOptions\n): void {\n let db: SyncedDb<S>;\n let finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>;\n let options: PreloadOptions | undefined;\n\n if (dbOrQuery instanceof SyncedDb) {\n db = dbOrQuery;\n finalQuery = queryOrOptions as PreloadArg<S, TableName, T, RelatedFields, IsOne>;\n options = maybeOptions;\n } else {\n db = useDb<S>();\n finalQuery = dbOrQuery;\n options = queryOrOptions as PreloadOptions | undefined;\n }\n\n let prevHash: number | undefined;\n\n // Two-arg Solid 2 effect: compute resolves the query (tracking its reactive\n // deps) and dedupes on the identity hash; the untracked apply fires the\n // preload. Returning `undefined` from compute skips nothing — apply guards.\n createEffect(\n () => {\n if (!(options?.enabled?.() ?? true)) return undefined;\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n if (!query) return undefined;\n // Dedupe on the query's stable identity hash so a reactive re-run with\n // an unchanged query doesn't refetch (the core also dedupes per session).\n if (query.hash === prevHash) return undefined;\n prevHash = query.hash;\n return query;\n },\n (query) => {\n if (!query) return;\n void db\n .getSp00ky()\n .preload(query, { refresh: options?.refresh, staleTime: options?.staleTime });\n }\n );\n}\n","import type { Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { ConnectionState, SyncHealth, SyncHealthStatus } from '@spooky-sync/core';\n\nexport interface UseSyncStatus {\n /** Full health snapshot; updates reactively on every transition. */\n health: Accessor<SyncHealth>;\n /** `'healthy'` | `'degraded'`. */\n status: Accessor<SyncHealthStatus>;\n isHealthy: Accessor<boolean>;\n /** `true` once sync has failed for a sustained run — drive a banner off this. */\n isDegraded: Accessor<boolean>;\n /** `true` once at least one sync round has succeeded this session. */\n everConnected: Accessor<boolean>;\n /**\n * `true` only for a real lost connection: degraded AFTER a first successful\n * sync. Stays `false` during the initial \"connecting\" phase (degraded but\n * never reached the server yet), so an indicator can show nothing until the\n * app has actually connected once.\n */\n isOffline: Accessor<boolean>;\n /**\n * Transport state of the remote WebSocket. Flips the instant the socket\n * drops, unlike `status`, which only degrades after a sustained run of failed\n * sync rounds — so this is what to drive a \"reconnecting…\" affordance off.\n */\n connection: Accessor<ConnectionState>;\n /**\n * `true` while the connection is being re-established. Usually still\n * `isHealthy()`: a short reconnect is invisible to sync, and writes made\n * during it are queued locally and pushed once the socket is back.\n */\n isReconnecting: Accessor<boolean>;\n}\n\n/**\n * Observe sync health for a \"can't reach the server\" banner / indicator.\n *\n * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient\n * remote 500 on query registration, a dropped socket) are absorbed by the\n * retry and never flip this; `isDegraded()` only goes true once failures\n * persist for the configured number of consecutive rounds (sp00ky core config\n * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on\n * the next successful round. Must be used within a `<Sp00kyProvider>`.\n */\nexport function useSyncStatus(): UseSyncStatus {\n const db = useDb();\n // The subscription fires synchronously with the current status; the initial\n // value (loadingValue) just avoids a flash before it lands.\n const health = fromSubscription<SyncHealth>((cb) => db.subscribeToSyncHealth(cb), db.syncHealth);\n\n return {\n health,\n status: () => health().status,\n isHealthy: () => health().status === 'healthy',\n isDegraded: () => health().status === 'degraded',\n everConnected: () => health().everConnected,\n isOffline: () => health().status === 'degraded' && health().everConnected,\n connection: () => health().connection,\n isReconnecting: () => health().connection === 'reconnecting',\n };\n}\n","import type { Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';\n\nexport interface UseStorageStatus {\n /** Full durability snapshot; updates reactively. */\n health: Accessor<StorageHealth>;\n /** `'unknown'` | `'persistent'` | `'memory'`. */\n status: Accessor<StorageHealthStatus>;\n /** `true` when the local store survives a reload. */\n isPersistent: Accessor<boolean>;\n /**\n * `true` only when durable storage was requested and could NOT be opened, so\n * the dataset is sitting in RAM and local writes die on reload. Drive a\n * warning off this, not off `status`: a store configured as in-memory reports\n * `'memory'` too, and that is a choice rather than a problem.\n */\n isMemoryFallback: Accessor<boolean>;\n}\n\n/**\n * Observe how durable the LOCAL cache is, for a \"no local storage\" warning.\n *\n * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is\n * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a\n * second tab of the same app cannot get it and runs in memory instead (the\n * engine retries first, so a closing tab's lock is usually waited out). Must be\n * used within a `<Sp00kyProvider>`.\n */\nexport function useStorageStatus(): UseStorageStatus {\n const db = useDb();\n const health = fromSubscription<StorageHealth>(\n (cb) => db.subscribeToStorageHealth(cb),\n db.storageHealth\n );\n\n return {\n health,\n status: () => health().status,\n isPersistent: () => health().status === 'persistent',\n isMemoryFallback: () => health().fallback,\n };\n}\n","import { createEffect, createSignal, type Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport type { CrdtField } from '@spooky-sync/core';\n\nexport function useCrdtField(\n table: string,\n recordId: () => string | undefined,\n field: string,\n fallbackText?: () => string | undefined\n): Accessor<CrdtField | null> {\n const db = useDb();\n const [crdtField, setCrdtField] = createSignal<CrdtField | null>(null, { ownedWrite: true });\n\n // Two-arg Solid 2 effect: the compute tracks `recordId`, the apply owns the\n // open/close lifecycle and returns the cleanup — which runs both when the id\n // changes (before the next apply) and on unmount. That replaces the Solid 1\n // version's manual currentId/initialized bookkeeping.\n createEffect(\n () => recordId(),\n (id) => {\n if (!id) {\n setCrdtField(null);\n return;\n }\n const sp00ky = db.getSp00ky();\n let superseded = false;\n const text = fallbackText?.();\n sp00ky\n .openCrdtField(table, id, field, text)\n .then((cf) => {\n if (!superseded) {\n setCrdtField(cf);\n } else {\n sp00ky.closeCrdtField(table, id, field);\n }\n })\n .catch((err) => {\n // Silent rejections here leave the consumer's `Show when={field()}`\n // permanently stuck on its fallback (typically a static `<p>` with\n // no editing UI), with no error trail. Surface the failure so the\n // root cause (missing `@crdt` annotation, schema codegen drift,\n // local DB query failure, etc.) is visible in the console instead\n // of silently breaking collaborative fields.\n console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);\n });\n return () => {\n superseded = true;\n if (crdtField()) {\n sp00ky.closeCrdtField(table, id, field);\n setCrdtField(null);\n }\n };\n }\n );\n\n return crdtField;\n}\n","import type { Accessor } from 'solid-js';\nimport { onCleanup } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { FeatureFlagOptions } from '@spooky-sync/core';\n\nexport interface UseFeatureFlag {\n variant: Accessor<string | undefined>;\n payload: Accessor<unknown | undefined>;\n enabled: Accessor<boolean>;\n}\n\n/**\n * Subscribe to a feature flag for the currently authenticated user.\n *\n * Returns three Solid accessors that update reactively whenever the\n * server-materialized assignment in `_00_user_feature` changes. Backed by\n * the same SSP + sync pipeline that powers `createQuery`, so toggling a flag\n * via `spky flag enable <key>` propagates to the UI without a refresh.\n *\n * `enabled()` is `true` when the resolved variant exists and is not 'off'.\n * For multi-variant flags, prefer `variant()` directly.\n */\nexport function useFeatureFlag(key: string, options?: FeatureFlagOptions): UseFeatureFlag {\n const db = useDb();\n const handle = db.getSp00ky().feature(key, options);\n onCleanup(() => handle.close());\n\n const state = fromSubscription<{ variant: string | undefined; payload: unknown }>(\n (cb) =>\n handle.subscribe((s) => cb({ variant: s.variant ?? options?.fallback, payload: s.payload })),\n { variant: handle.variant(), payload: handle.payload() }\n );\n\n return {\n variant: () => state().variant,\n payload: () => state().payload,\n enabled: () => {\n const v = state().variant;\n return v !== undefined && v !== 'off';\n },\n };\n}\n","import type { Accessor } from 'solid-js';\nimport { onCleanup } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport { semverGt, type AppReleaseOptions, type AppReleaseSnapshot } from '@spooky-sync/core';\n\nexport interface UseAppReleaseOptions extends AppReleaseOptions {\n /** App name from sp00ky.yml, e.g. `web`. */\n app: string;\n /**\n * The running build's version (X.Y.Z), typically baked in at build time\n * (e.g. a vite `define` from package.json). `updateAvailable()` is true when\n * the announced release is semver-newer than this.\n */\n currentVersion: string;\n}\n\nexport interface UseAppRelease {\n /** Latest announced version for the app, or undefined when no row exists. */\n latestVersion: Accessor<string | undefined>;\n /** Announced version is semver-newer than the running build. */\n updateAvailable: Accessor<boolean>;\n /** The newer release asks clients to update/reload without prompting. */\n mandatory: Accessor<boolean>;\n /** The newer release asks reloads to clear service-worker caches first. */\n cacheBust: Accessor<boolean>;\n /**\n * Reload onto the announced release. Plain `location.reload()` normally;\n * when the release is flagged cache-bust, CacheStorage is cleared, the\n * service-worker registration is nudged to update, and navigation carries a\n * `?cb=` token to punch through intermediary caches. The service worker is\n * deliberately NOT unregistered: navigating while still controlled by a\n * just-unregistered worker strands subresource fetches on the dead worker\n * and the page hangs until a manual reload.\n */\n reload: () => Promise<void>;\n}\n\nasync function reloadForSnapshot(snapshot: AppReleaseSnapshot): Promise<void> {\n if (typeof window === 'undefined') return;\n if (snapshot.cacheBust) {\n try {\n if (window.caches) {\n const keys = await window.caches.keys();\n await Promise.all(keys.map((k) => window.caches.delete(k)));\n }\n if (navigator.serviceWorker) {\n const regs = await navigator.serviceWorker.getRegistrations();\n for (const r of regs) r.update().catch(() => {});\n }\n window.location.href = window.location.pathname + '?cb=' + Date.now();\n return;\n } catch {\n /* fall through to a plain reload */\n }\n }\n window.location.reload();\n}\n\n/**\n * Observe the app's announced release (`_00_app_release:<app>`, written by\n * `spky deploy` / `spky release`) and compare it against the running build.\n *\n * Typical use: mount a small \"new version available — Reload\" notification\n * gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`\n * (guard the auto path against reload loops with a per-version marker, since\n * a client can reload while the deploy is still rolling out and land on the\n * old bundle again).\n */\nexport function useAppRelease(options: UseAppReleaseOptions): UseAppRelease {\n const db = useDb();\n const handle = db.getSp00ky().appRelease(options.app, { ttl: options.ttl });\n onCleanup(() => handle.close());\n\n const snapshot = fromSubscription<AppReleaseSnapshot>(\n (cb) => handle.subscribe(cb),\n handle.snapshot()\n );\n\n const updateAvailable = () => semverGt(snapshot().version, options.currentVersion);\n\n return {\n latestVersion: () => snapshot().version,\n updateAvailable,\n mandatory: () => updateAvailable() && snapshot().mandatory,\n cacheBust: () => snapshot().cacheBust,\n reload: () => reloadForSnapshot(snapshot()),\n };\n}\n","import { createSignal, onCleanup } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport { fileToUint8Array } from '@spooky-sync/core';\nimport type { BucketPutOptions, BucketPutResult } from '@spooky-sync/core';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface FileUploadResult {\n isUploading: () => boolean;\n error: () => Error | null;\n clearError: () => void;\n upload: (\n path: string,\n file: File | Blob,\n options?: BucketPutOptions\n ) => Promise<BucketPutResult | void>;\n download: (path: string) => Promise<string | null>;\n remove: (path: string) => Promise<void>;\n exists: (path: string) => Promise<boolean>;\n}\n\nexport function useFileUpload<S extends SchemaStructure>(\n bucketName: BucketNames<S>\n): FileUploadResult;\nexport function useFileUpload<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>\n): FileUploadResult;\nexport function useFileUpload<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n maybeBucketName?: BucketNames<S>\n): FileUploadResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n // oxlint-disable-next-line no-non-null-assertion\n bucketName = maybeBucketName!;\n }\n\n // Written from async continuations — outside any tracking scope.\n const [isUploading, setIsUploading] = createSignal(false, { ownedWrite: true });\n const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });\n\n const objectUrls: string[] = [];\n onCleanup(() => {\n for (const url of objectUrls) {\n URL.revokeObjectURL(url);\n }\n });\n\n const clearError = () => setError(null);\n\n const validate = (file: File | Blob): void => {\n const config = db.getBucketConfig(bucketName as string);\n if (!config) return;\n\n if (config.maxSize !== null && config.maxSize !== undefined && file.size > config.maxSize) {\n const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);\n throw new Error(`File exceeds maximum size of ${maxMB} MB.`);\n }\n\n if (config.allowedExtensions && config.allowedExtensions.length > 0) {\n const fileName = (file as File).name;\n if (fileName) {\n const ext = fileName.split('.').pop()?.toLowerCase();\n if (!ext || !config.allowedExtensions.includes(ext)) {\n throw new Error(\n `File type not allowed. Accepted: ${config.allowedExtensions.join(', ')}.`\n );\n }\n }\n }\n };\n\n const upload = async (\n path: string,\n file: File | Blob,\n options?: BucketPutOptions\n ): Promise<BucketPutResult | void> => {\n setError(null);\n try {\n validate(file);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n\n setIsUploading(true);\n try {\n const bytes = await fileToUint8Array(file);\n return await db.bucket(bucketName).put(path, bytes, options);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n setIsUploading(false);\n }\n };\n\n const download = async (path: string): Promise<string | null> => {\n setError(null);\n try {\n const content = await db.bucket(bucketName).get(path);\n if (!content) return null;\n const objectUrl = URL.createObjectURL(new Blob([content as BlobPart]));\n objectUrls.push(objectUrl);\n return objectUrl;\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return null;\n }\n };\n\n const remove = async (path: string): Promise<void> => {\n setError(null);\n try {\n await db.bucket(bucketName).delete(path);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n }\n };\n\n const exists = async (path: string): Promise<boolean> => {\n setError(null);\n try {\n return await db.bucket(bucketName).exists(path);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return false;\n }\n };\n\n return {\n isUploading,\n error,\n clearError,\n upload,\n download,\n remove,\n exists,\n };\n}\n","import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { BlobUrlLease } from '@spooky-sync/core';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface UseDownloadFileOptions {\n /**\n * Master switch, default `true`. `false` gives every hook instance its own\n * private object URL fetched fresh from the bucket and revoked on unmount —\n * no sharing, no persistence, no reuse.\n */\n cache?: boolean;\n /**\n * Keep the bytes in OPFS so they survive a reload and are available offline.\n * Default `true`. Turn off for one-shot or sensitive files; the in-tab object\n * URL is still shared between components rendering the same path.\n */\n persist?: boolean;\n /** Exempt this file from pressure eviction. Pinned bytes never expire. */\n pin?: boolean;\n /**\n * `'never'` (default) treats a bucket path as immutable, which is how paths\n * are written (`crypto.randomUUID() + ext`). `'head'` spends a remote `head()`\n * to compare sizes before trusting the cached copy — for paths the app\n * overwrites in place.\n */\n revalidate?: 'never' | 'head';\n}\n\nexport interface UseDownloadFileResult {\n url: Accessor<string | null>;\n isLoading: Accessor<boolean>;\n error: Accessor<Error | null>;\n refetch: () => void;\n}\n\nexport function useDownloadFile<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseDownloadFileOptions\n): UseDownloadFileResult;\nexport function useDownloadFile<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseDownloadFileOptions\n): UseDownloadFileResult;\nexport function useDownloadFile<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n pathOrOptions?: Accessor<string | null | undefined> | UseDownloadFileOptions,\n maybeOptions?: UseDownloadFileOptions\n): UseDownloadFileResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n let options: UseDownloadFileOptions;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n options = (pathOrOptions as UseDownloadFileOptions) ?? {};\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = pathOrOptions as Accessor<string | null | undefined>;\n options = maybeOptions ?? {};\n }\n\n const useCache = options.cache !== false;\n\n // Written from fetch continuations — outside any tracking scope.\n const [url, setUrl] = createSignal<string | null>(null, { ownedWrite: true });\n const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });\n const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });\n\n // Exactly one of these is held at a time: a refcounted lease on the shared\n // cache entry, or a private URL this instance minted and must revoke itself.\n let lease: BlobUrlLease | null = null;\n let privateUrl: string | null = null;\n\n const [refetchSignal, setRefetchSignal] = createSignal(0);\n /** Consumed by the next effect run, so `refetch()` bypasses every layer once. */\n let reloadOnce = false;\n\n function releaseCurrent() {\n lease?.release();\n lease = null;\n if (privateUrl) {\n URL.revokeObjectURL(privateUrl);\n privateUrl = null;\n }\n }\n\n // Two-arg Solid 2 effect: compute tracks path + refetch tick; apply runs the\n // fetch and returns the cancel/release cleanup, which runs before the next\n // apply and on unmount.\n createEffect(\n () => {\n refetchSignal();\n return path();\n },\n (filePath) => {\n releaseCurrent();\n\n if (!filePath) {\n setUrl(null);\n setIsLoading(false);\n setError(null);\n return;\n }\n\n const reload = reloadOnce;\n reloadOnce = false;\n\n let cancelled = false;\n setIsLoading(true);\n setError(null);\n\n const bucket = db.bucket(bucketName);\n const resolve = useCache\n ? bucket\n .url(filePath, {\n persist: options.persist !== false,\n pin: options.pin,\n revalidate: options.revalidate,\n reload,\n })\n .then((acquired) => {\n if (!acquired) return null;\n if (cancelled) {\n // Unmounted or the path changed mid-flight — hand the reference\n // straight back, or the entry never drops to zero and its object\n // URL leaks for the life of the tab.\n acquired.release();\n return null;\n }\n lease = acquired;\n return acquired.url;\n })\n : bucket.read(filePath, { persist: false, reload: true }).then((blob) => {\n if (!blob || cancelled) return null;\n privateUrl = URL.createObjectURL(blob);\n return privateUrl;\n });\n\n resolve.then(\n (result) => {\n if (!cancelled) {\n setUrl(result);\n setIsLoading(false);\n }\n return undefined;\n },\n (err) => {\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsLoading(false);\n }\n }\n );\n\n return () => {\n cancelled = true;\n };\n }\n );\n\n onCleanup(() => {\n releaseCurrent();\n });\n\n const refetch = () => {\n reloadOnce = true;\n setRefetchSignal((n) => n + 1);\n };\n\n return { url, isLoading, error, refetch };\n}\n","import { createSignal, createEffect, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface UseBlurhashResult {\n /** The stored blurhash for the path, or null while loading / when none exists. */\n hash: Accessor<string | null>;\n isLoading: Accessor<boolean>;\n}\n\n/**\n * The blurhash sidecar for a bucket image (written automatically by\n * `bucket.put`, see `Sp00kyConfig.blurhash`). Resolves from OPFS instantly on\n * warm clients; a miss is remembered per tab. Use this directly when the hash\n * belongs to a different rendition than the displayed image; otherwise\n * `useBucketImage` / `BucketImage` bundle it with the download.\n */\nexport function useBlurhash<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>\n): UseBlurhashResult;\nexport function useBlurhash<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>\n): UseBlurhashResult;\nexport function useBlurhash<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n maybePath?: Accessor<string | null | undefined>\n): UseBlurhashResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = maybePath as Accessor<string | null | undefined>;\n }\n\n // Written from the lookup continuation, outside any tracking scope.\n const [hash, setHash] = createSignal<string | null>(null, { ownedWrite: true });\n const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });\n\n // Compute tracks the path; apply runs the lookup and returns the cancel\n // cleanup, which runs before the next apply and on unmount.\n createEffect(\n () => path(),\n (filePath) => {\n if (!filePath) {\n setHash(null);\n setIsLoading(false);\n return;\n }\n let cancelled = false;\n setIsLoading(true);\n db.bucket(bucketName)\n .blurhash(filePath)\n .then((result) => {\n if (cancelled) return;\n setHash(result);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setHash(null);\n setIsLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }\n );\n\n return { hash, isLoading };\n}\n","import { createSignal, createEffect, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\nimport {\n useDownloadFile,\n type UseDownloadFileOptions,\n type UseDownloadFileResult,\n} from './use-download-file';\nimport { useBlurhash } from './use-blurhash';\n\nexport interface UseBucketImageOptions extends UseDownloadFileOptions {\n /**\n * Also resolve the image's blurhash sidecar (see `Sp00kyConfig.blurhash`).\n * Default `true`; the read is registered before the image bytes so the tiny\n * sidecar tends to land first on the serialized remote chain.\n */\n blurhash?: boolean;\n}\n\nexport interface UseBucketImageResult extends UseDownloadFileResult {\n /** Blurhash for the same path, or null (off, missing, still loading). */\n blurhash: Accessor<string | null>;\n /** True once the current `url()` has been decoded and is safe to paint. */\n ready: Accessor<boolean>;\n /**\n * Ref callback for the `<img>` rendering `url()`: flips `ready` when the\n * bitmap is decoded (resolves on failure too, so a broken blob degrades to\n * paint-on-load instead of hiding the image forever). Re-arms itself when\n * the url changes.\n */\n gate: (img: HTMLImageElement) => void;\n}\n\n/**\n * Everything needed to render a bucket image without a pop-in: the refcounted\n * object URL, the blurhash placeholder, and a decode gate so the real bitmap\n * is only revealed once it can paint in full. `BucketImage` wraps this into a\n * drop-in component; use the hook for custom markup.\n */\nexport function useBucketImage<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseBucketImageOptions\n): UseBucketImageResult;\nexport function useBucketImage<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseBucketImageOptions\n): UseBucketImageResult;\nexport function useBucketImage<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n pathOrOptions?: Accessor<string | null | undefined> | UseBucketImageOptions,\n maybeOptions?: UseBucketImageOptions\n): UseBucketImageResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n let options: UseBucketImageOptions;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n options = (pathOrOptions as UseBucketImageOptions) ?? {};\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = pathOrOptions as Accessor<string | null | undefined>;\n options = maybeOptions ?? {};\n }\n\n // Registered BEFORE the download so the sidecar read enters the serialized\n // remote queue first: the placeholder should never wait behind the bytes it\n // is standing in for.\n const wantHash = options.blurhash !== false;\n const { hash } = useBlurhash(db, bucketName, () => (wantHash ? path() : null));\n\n const file = useDownloadFile(db, bucketName, path, options);\n\n // Written from the decode continuation (and from the url effect's apply\n // phase), outside any tracking scope.\n const [ready, setReady] = createSignal(false, { ownedWrite: true });\n // A new url (path change, refetch) means a new undecoded bitmap.\n createEffect(\n file.url,\n () => {\n setReady(false);\n },\n { defer: true }\n );\n\n const gate = (img: HTMLImageElement) => {\n const done = () => setReady(true);\n if (typeof img.decode === 'function') {\n img.decode().then(done, done);\n } else if (img.complete) {\n done();\n } else {\n img.onload = done;\n img.onerror = done;\n }\n };\n\n return { ...file, blurhash: hash, ready, gate };\n}\n","import { createEffect, type Element } from 'solid-js';\nimport { decodeBlurhash } from '@spooky-sync/core';\n\nexport interface BlurhashProps {\n /** The blurhash string. Nullish paints nothing (transparent canvas). */\n hash: string | null | undefined;\n /** Decode resolution. 32x32 is plenty: blurhash carries at most 9x9 DCT\n * components, the canvas is meant to be CSS-scaled to fill. */\n width?: number;\n height?: number;\n /** Contrast punch, see the blurhash reference decoder. Defaults to 1. */\n punch?: number;\n class?: string;\n style?: string;\n}\n\n/**\n * A blurhash painted onto a canvas, once per hash change. Size the canvas via\n * `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode\n * resolution stays tiny regardless of the displayed size.\n */\nexport function Blurhash(props: BlurhashProps): Element {\n if (typeof document === 'undefined') return null;\n const canvas = document.createElement('canvas');\n\n createEffect(\n () => props.class ?? '',\n (className) => {\n canvas.className = className;\n }\n );\n createEffect(\n () => props.style ?? '',\n (style) => {\n canvas.style.cssText = style;\n }\n );\n\n createEffect(\n () => ({\n width: props.width ?? 32,\n height: props.height ?? 32,\n hash: props.hash,\n punch: props.punch ?? 1,\n }),\n ({ width, height, hash, punch }) => {\n canvas.width = width;\n canvas.height = height;\n if (!hash) return;\n try {\n const pixels = decodeBlurhash(hash, width, height, punch);\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n const imageData = ctx.createImageData(width, height);\n imageData.data.set(pixels);\n ctx.putImageData(imageData, 0, 0);\n } catch {\n // An invalid hash paints nothing; the layer below stays visible.\n }\n }\n );\n\n return canvas as unknown as Element;\n}\n","import { createSignal, createEffect, onSettled, children, type Element } from 'solid-js';\nimport type { BucketNames, SchemaStructure } from '@spooky-sync/query-builder';\nimport { useBucketImage, type UseBucketImageOptions } from './use-bucket-image';\nimport { Blurhash } from './Blurhash';\n\nexport interface BucketImageProps {\n /** Bucket name from the schema. */\n bucket: string;\n /** Path within the bucket. Nullish renders only the fallback layers. */\n path: string | null | undefined;\n alt?: string;\n /** Classes for the container element (sizing/positioning). */\n class?: string;\n /** Classes for the inner `<img>` (the layout styles are inline). */\n imgClass?: string;\n /** `object-fit` for the image. Defaults to `cover`. */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n /**\n * Bottom placeholder layer (your own plate/skeleton), shown until the image\n * settles. The blurhash layer paints on top of it once the sidecar resolves.\n */\n fallback?: Element;\n /** Crossfade duration in ms. Defaults to 300. */\n transition?: number;\n /** Crossfade easing. Defaults to an ease-out-expo curve. */\n easing?: string;\n /** Resolve the blurhash sidecar. Defaults to true. */\n blurhash?: boolean;\n /** Download tuning, forwarded to the underlying `useDownloadFile`. */\n options?: UseBucketImageOptions;\n}\n\nconst LAYER_STYLE = 'position:absolute;inset:0;width:100%;height:100%;';\n\n/**\n * A bucket image that never pops in: it layers (bottom to top) your `fallback`\n * plate, the automatically stored blurhash, and the real image, which stays\n * transparent until the bitmap is DECODED and then crossfades over the\n * placeholders. Placeholder layers unmount once the fade settles. Respects\n * prefers-reduced-motion (instant swap). The container is made\n * `position: relative` unless your `class` positions it already.\n *\n * ```tsx\n * <BucketImage bucket=\"covers\" path={row.cover_key} class=\"absolute inset-0\"\n * fallback={<MyPlate />} alt=\"\" />\n * ```\n */\nexport function BucketImage(props: BucketImageProps): Element {\n if (typeof document === 'undefined') return null;\n\n const image = useBucketImage(\n props.bucket as BucketNames<SchemaStructure>,\n () => props.path,\n { ...props.options, blurhash: props.blurhash !== false }\n );\n\n const reducedMotion =\n typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const root = document.createElement('div');\n createEffect(\n () => props.class ?? '',\n (className) => {\n root.className = className;\n }\n );\n // Layers are absolutely positioned; give them an anchor without stomping on\n // a caller class that already positions the container (inline would win).\n onSettled(() => {\n if (getComputedStyle(root).position === 'static') root.style.position = 'relative';\n });\n\n const placeholder = document.createElement('div');\n placeholder.style.cssText = LAYER_STYLE;\n // Solid hands JSX props over as lazy thunks; `children` resolves them (and\n // nested arrays/functions) to real nodes and keeps them alive reactively.\n const fallbackHolder = document.createElement('div');\n fallbackHolder.style.cssText = LAYER_STYLE;\n placeholder.append(fallbackHolder);\n const resolvedFallback = children(() => props.fallback);\n createEffect(\n () => resolvedFallback.toArray().filter((node) => node instanceof Node) as Node[],\n (nodes) => {\n fallbackHolder.replaceChildren(...nodes);\n }\n );\n const hashCanvas = Blurhash({\n get hash() {\n return image.blurhash();\n },\n style: LAYER_STYLE,\n });\n if (hashCanvas instanceof Node) placeholder.append(hashCanvas);\n\n const img = document.createElement('img');\n img.decoding = 'async';\n img.style.cssText = `${LAYER_STYLE}opacity:0;`;\n createEffect(\n () => props.imgClass ?? '',\n (className) => {\n img.className = className;\n }\n );\n createEffect(\n () => props.fit ?? 'cover',\n (fit) => {\n img.style.objectFit = fit;\n }\n );\n createEffect(\n () => props.alt ?? '',\n (alt) => {\n img.alt = alt;\n }\n );\n createEffect(\n () =>\n reducedMotion\n ? 'none'\n : `opacity ${props.transition ?? 300}ms ${props.easing ?? 'cubic-bezier(0.16, 1, 0.3, 1)'}`,\n (transition) => {\n img.style.transition = transition;\n }\n );\n createEffect(\n () => image.url(),\n (url) => {\n if (!url) {\n img.removeAttribute('src');\n return;\n }\n img.src = url;\n image.gate(img);\n }\n );\n createEffect(\n () => image.ready(),\n (ready) => {\n img.style.opacity = ready ? '1' : '0';\n }\n );\n\n // Placeholders leave the DOM once the fade is over (a shelf of covers should\n // not composite three layers each forever) and come back when the path\n // changes mid-life (`ready` re-arms via the hook). `settled` is written from\n // the apply phase and a timer, outside any tracking scope.\n const [settled, setSettled] = createSignal(false, { ownedWrite: true });\n createEffect(\n () => ({ ready: image.ready(), transition: props.transition ?? 300 }),\n ({ ready, transition }) => {\n if (!ready) {\n setSettled(false);\n return;\n }\n const wait = (reducedMotion ? 0 : transition) + 120;\n const timer = setTimeout(() => setSettled(true), wait);\n return () => clearTimeout(timer);\n }\n );\n createEffect(\n () => settled(),\n (isSettled) => {\n if (isSettled) placeholder.remove();\n else if (!placeholder.isConnected) root.insertBefore(placeholder, img);\n }\n );\n\n root.append(placeholder, img);\n return root as unknown as Element;\n}\n","import type { Element } from 'solid-js';\nimport {\n createSignal,\n onSettled,\n onCleanup,\n createComponent,\n createMemo,\n merge,\n} from 'solid-js';\nimport type { SchemaStructure } from '@spooky-sync/query-builder';\nimport type { SyncedDbConfig } from '../types';\nimport { SyncedDb } from '../index';\nimport { Sp00kyContext } from './context';\n\nexport interface Sp00kyProviderProps<S extends SchemaStructure> {\n config: SyncedDbConfig<S>;\n fallback?: Element;\n onError?: (error: Error) => void;\n onReady?: (db: SyncedDb<S>) => void;\n /**\n * Prewarm data into the local cache before revealing the UI. Runs after\n * `init()`; the `fallback` stays visible until it resolves. Use awaitable\n * `db.preload(...)` calls here to gate first-load on essential data (e.g.\n * config). On warm loads preload returns instantly, so there's no perceptible\n * gate after the first run. Best-effort: a rejection is caught and the UI is\n * revealed anyway.\n */\n preload?: (db: SyncedDb<S>) => Promise<void>;\n children: Element;\n}\n\nexport function Sp00kyProvider<S extends SchemaStructure>(\n props: Sp00kyProviderProps<S>\n): Element {\n const merged = merge({ fallback: undefined as Element | undefined }, props);\n\n // Written from the async init continuation — outside any tracking scope.\n const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined, { ownedWrite: true });\n\n // Init is async, so a dispose can land mid-init. Only that narrow race is\n // handled here: an instance whose init finished AFTER the provider was\n // already gone is closed, because nothing will ever reference it.\n //\n // A live, mounted client is deliberately NOT closed on cleanup. Doing that\n // nulls `SyncedDb.sp00ky`, so every later `create`/`update`/`delete` throws\n // \"SyncedDb not initialized\" while reads keep rendering from state that is\n // already subscribed — i.e. mutations die silently and the app looks fine. In\n // a host app the provider wraps the whole tree and only unmounts with the\n // page, where the browser reclaims the worker anyway, so the leak this was\n // meant to fix is worth far less than that risk.\n let disposed = false;\n\n onCleanup(() => {\n disposed = true;\n });\n\n // `onSettled` replaces Solid 1's `onMount`.\n onSettled(() => {\n void (async () => {\n try {\n const instance = new SyncedDb<S>(merged.config);\n await instance.init();\n if (disposed) {\n await instance.close();\n return;\n }\n // Gate first-load UI on prewarmed data. Best-effort: never let a\n // preload failure keep the app stuck on the fallback.\n if (merged.preload) {\n try {\n await merged.preload(instance);\n } catch (e) {\n // oxlint-disable-next-line no-console\n console.error('Sp00kyProvider: preload failed; revealing UI anyway', e);\n }\n }\n setDb(() => instance);\n merged.onReady?.(instance);\n } catch (e) {\n const error = e instanceof Error ? e : new Error(String(e));\n if (merged.onError) {\n merged.onError(error);\n } else {\n // oxlint-disable-next-line no-console\n console.error('Sp00kyProvider: Failed to initialize database', error);\n }\n }\n })();\n });\n\n const content = createMemo(() => {\n const instance = db();\n if (!instance) return merged.fallback;\n // Solid 2: the context object IS the provider component.\n return createComponent(Sp00kyContext, {\n value: instance,\n get children() {\n return merged.children;\n },\n });\n });\n\n return content as unknown as Element;\n}\n","import { createSignal, type Accessor } from 'solid-js';\n\nexport interface Submission<Args extends unknown[], R> {\n /** Run the wrapped async fn. Concurrent submits share the pending flag. */\n submit: (...args: Args) => Promise<R | undefined>;\n /** True while at least one submit is in flight. */\n pending: Accessor<boolean>;\n /** Error from the most recent settled submit, cleared on the next submit. */\n error: Accessor<Error | undefined>;\n /** Result of the most recent successful submit. */\n result: Accessor<R | undefined>;\n clearError: () => void;\n}\n\n/**\n * Thin submission-state wrapper for mutations — button spinner/disable state\n * around `db.create/update/delete/run` calls.\n *\n * Deliberately NOT built on Solid 2's `action()`/`createOptimisticStore`: the\n * spooky engine is already optimistic local-first (writes commit to the local\n * DB and re-render through live queries before sync; `run()` is an outbox\n * CREATE), so a transaction/revert layer on top buys nothing and `action()`'s\n * await-vs-yield transaction escape is a real footgun. Errors here mean the\n * LOCAL commit failed — sync/push failures surface through `useSyncStatus`\n * and `usePendingMutations` instead.\n */\nexport function createSubmission<Args extends unknown[], R>(\n fn: (...args: Args) => Promise<R>\n): Submission<Args, R> {\n // Written from promise continuations — outside any tracking scope.\n const [inFlight, setInFlight] = createSignal(0, { ownedWrite: true });\n const [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true });\n const [result, setResult] = createSignal<R | undefined>(undefined, { ownedWrite: true });\n\n const submit = async (...args: Args): Promise<R | undefined> => {\n setError(undefined);\n setInFlight((n) => n + 1);\n try {\n const r = await fn(...args);\n setResult(() => r);\n return r;\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return undefined;\n } finally {\n setInFlight((n) => n - 1);\n }\n };\n\n return {\n submit,\n pending: () => inFlight() > 0,\n error,\n result,\n clearError: () => setError(undefined),\n };\n}\n","import type { SyncedDbConfig } from './types';\nimport {\n Sp00kyClient,\n type Sp00kyQueryResultPromise,\n type AuthService,\n type BucketHandle,\n type UpdateOptions,\n type RunOptions,\n type SyncHealth,\n type StorageHealth,\n type PreloadOptions,\n type PreloadRefresh,\n} from '@spooky-sync/core';\n\nimport type {\n GetTable,\n QueryBuilder,\n SchemaStructure,\n TableModel,\n TableNames,\n QueryResult,\n RelatedFieldsMap,\n RelationshipFieldsFromSchema,\n GetRelationship,\n RelatedFieldMapEntry,\n FinalQuery,\n InnerQuery,\n BackendNames,\n BackendRoutes,\n RoutePayload,\n BucketNames,\n BucketDefinitionSchema,\n QueryModifier,\n QueryModifierBuilder,\n QueryInfo,\n RelationshipsMetadata,\n RelationshipDefinition,\n InferRelatedModelFromMetadata,\n GetCardinality,\n} from '@spooky-sync/query-builder';\n\nimport { RecordId, Uuid, type Surreal } from 'surrealdb';\nimport { snapshot } from 'solid-js';\nexport { RecordId, Uuid };\nexport type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';\nexport { createQuery, useQuery, type CreateQueryResult, type QueryOptions } from './lib/create-query';\nexport { createPreload } from './lib/create-preload';\nexport type { PreloadOptions, PreloadRefresh } from '@spooky-sync/core';\nexport { useSyncStatus, type UseSyncStatus } from './lib/use-sync-status';\nexport type {\n SyncHealth,\n SyncHealthStatus,\n SyncHealthConfig,\n ConnectionState,\n ReconnectConfig,\n} from '@spooky-sync/core';\nexport { useStorageStatus, type UseStorageStatus } from './lib/use-storage-status';\nexport type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';\nexport { useCrdtField } from './lib/use-crdt-field';\nexport { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';\nexport {\n useAppRelease,\n type UseAppRelease,\n type UseAppReleaseOptions,\n} from './lib/use-app-release';\nexport { useFileUpload, type FileUploadResult } from './lib/use-file-upload';\nexport {\n useDownloadFile,\n type UseDownloadFileOptions,\n type UseDownloadFileResult,\n} from './lib/use-download-file';\nexport { useBlurhash, type UseBlurhashResult } from './lib/use-blurhash';\nexport {\n useBucketImage,\n type UseBucketImageOptions,\n type UseBucketImageResult,\n} from './lib/use-bucket-image';\nexport { Blurhash, type BlurhashProps } from './lib/Blurhash';\nexport { BucketImage, type BucketImageProps } from './lib/BucketImage';\nexport { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';\nexport { useDb, usePendingMutations } from './lib/context';\nexport { createSubmission, type Submission } from './lib/create-submission';\nexport { conflate } from './lib/conflate';\nexport { fromSubscription } from './lib/from-subscription';\n\n// Re-export query builder types for convenience\nexport type {\n QueryModifier,\n QueryModifierBuilder,\n QueryInfo,\n RelationshipsMetadata,\n RelationshipDefinition,\n InferRelatedModelFromMetadata,\n GetCardinality,\n GetTable,\n TableModel,\n TableNames,\n QueryResult,\n};\n\nexport type RelationshipField<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n Field extends RelationshipFieldsFromSchema<Schema, TableName>,\n> = GetRelationship<Schema, TableName, Field>;\n\nexport type RelatedFieldsTableScoped<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelationshipFieldsFromSchema<Schema, TableName> =\n RelationshipFieldsFromSchema<Schema, TableName>,\n> = {\n [K in RelatedFields]: {\n to: RelationshipField<Schema, TableName, K>['to'];\n relatedFields: RelatedFieldsMap;\n cardinality: RelationshipField<Schema, TableName, K>['cardinality'];\n };\n};\n\nexport type InferModel<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelatedFieldsTableScoped<Schema, TableName>,\n> = QueryResult<Schema, TableName, RelatedFields, true>;\n\nexport type WithRelated<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: Omit<RelatedFieldMapEntry, 'relatedFields'> & {\n relatedFields: RelatedFields;\n };\n};\n\nexport type WithRelatedMany<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: {\n to: Field;\n relatedFields: RelatedFields;\n cardinality: 'many';\n };\n};\n\n/**\n * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration.\n * Delegates all logic to the underlying sp00ky-ts instance.\n *\n * NOTE: keep in sync with packages/client-solid/src/index.ts (SyncedDb).\n * Copied rather than shared so this package's dependency graph never pulls\n * in solid-js 1.x; fold the two together once client-solid moves to Solid 2.\n * Solid-2-only differences: `delete` accepts 'bound RecordId', and write\n * payloads go through `snapshot()` (see `unproxy` below).\n */\n\n/**\n * Solid 2 stores wrap every object read out of them (query rows, their nested\n * arrays, `createStore` docs) in a Proxy. Those proxies cannot cross\n * `postMessage` (the sqlite and shared-tabs workers): structuredClone throws\n * DataCloneError. `snapshot()` returns the underlying plain value for store\n * proxies and passes anything else through untouched.\n */\nfunction unproxy<T>(value: T): T {\n if (value === null || typeof value !== 'object') return value;\n return snapshot(value as object) as T;\n}\nexport class SyncedDb<S extends SchemaStructure> {\n private config: SyncedDbConfig<S>;\n private sp00ky: Sp00kyClient<S> | null = null;\n private _initialized = false;\n\n constructor(config: SyncedDbConfig<S>) {\n this.config = config;\n }\n\n public getSp00ky(): Sp00kyClient<S> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky;\n }\n\n /**\n * Initialize the sp00ky-ts instance\n */\n async init(): Promise<void> {\n if (this._initialized) return;\n this.sp00ky = new Sp00kyClient<S>(this.config);\n await this.sp00ky.init();\n this._initialized = true;\n }\n\n /**\n * Tear down the client: leaves the tabs broker, closes the local store and\n * remote socket, and frees the wasm circuit. Without this a remounted provider\n * (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay\n * resident because V8 cannot see how much wasm memory a dropped wrapper holds.\n */\n async close(): Promise<void> {\n const instance = this.sp00ky;\n this.sp00ky = null;\n this._initialized = false;\n if (instance) await instance.close();\n }\n\n /**\n * Create a new record in the database\n */\n async create(id: string, payload: Record<string, unknown>): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.create(id, unproxy(payload) as Record<string, unknown>);\n }\n\n /**\n * Update an existing record in the database\n */\n async update<TName extends TableNames<S>>(\n tableName: TName,\n recordId: string,\n payload: Partial<TableModel<GetTable<S, TName>>>,\n options?: UpdateOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.update(\n tableName as string,\n recordId,\n unproxy(payload) as Record<string, unknown>,\n options\n );\n }\n\n /**\n * Delete an existing record in the database\n */\n async delete<TName extends TableNames<S>>(\n tableName: TName,\n selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n // Accept a `\"table:id\"` string OR a RecordId — live-query rows carry their\n // `id` as a RecordId, so callers can pass `db.delete('game', row.id)`\n // directly. Build the canonical string from the raw id part (not\n // `RecordId.toString()`, which escapes special chars) so it round-trips\n // through the engine's `parseRecordIdString`. InnerQuery selectors are not\n // supported yet. A RecordId read out of a Solid 2 query row is a store\n // proxy that hides its prototype (no instanceof, no constructor.name, on\n // rc.1), so unwrap it first; cross-package RecordId instances then match\n // by constructor name ('bound RecordId' covers rc.0 proxies).\n const raw = unproxy(selector);\n const ctorName = (raw as any)?.constructor?.name;\n const isRecordId =\n raw instanceof RecordId || ctorName === 'RecordId' || ctorName === 'bound RecordId';\n let id: string;\n if (typeof raw === 'string') {\n id = raw;\n } else if (isRecordId) {\n id = `${tableName as string}:${(raw as RecordId).id}`;\n } else {\n throw new Error('Only string ID or RecordId selectors are supported currently with core');\n }\n await this.sp00ky.delete(tableName as string, id);\n }\n\n /**\n * Preload/prewarm a built query into the local cache without registering a\n * live view. Fetches once and stores the rows (+ embedded related children)\n * locally so a later `createQuery` for the same data paints instantly. Best-effort.\n */\n public async preload(\n finalQuery: FinalQuery<S, any, any, any, any, Sp00kyQueryResultPromise>,\n options?: PreloadOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.preload(finalQuery, options);\n }\n\n /**\n * Query data from the database\n */\n public query<TName extends TableNames<S>>(\n table: TName\n ): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.query(table, {});\n }\n\n /**\n * Run a backend operation\n */\n public async run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(\n backend: B,\n path: R,\n payload: RoutePayload<S, B, R>,\n options?: RunOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.run(backend, path, unproxy(payload), options);\n }\n\n /**\n * Sign out, clear session and local storage\n */\n public async signOut(): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.auth.signOut();\n }\n\n /**\n * Execute a function with direct access to the remote database connection\n */\n public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return await this.sp00ky.useRemote(fn);\n }\n /**\n * Access the remote database service directly\n */\n get remote(): Sp00kyClient<S>['remoteClient'] {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.remoteClient;\n }\n\n /**\n * Access the local database service directly\n */\n get local(): Sp00kyClient<S>['localClient'] {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.localClient;\n }\n\n /**\n * Access the auth service\n */\n get auth(): AuthService<S> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.auth;\n }\n\n get pendingMutationCount(): number {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.pendingMutationCount;\n }\n\n /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */\n get liveRetryCount(): number {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.liveRetryCount;\n }\n\n subscribeToPendingMutations(cb: (count: number) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToPendingMutations(cb);\n }\n\n /** Current sync-health snapshot. See {@link useSyncStatus}. */\n get syncHealth(): SyncHealth {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.syncHealth;\n }\n\n /**\n * Observe sync health. Fires immediately with the current status and again\n * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in\n * components; this is the imperative escape hatch.\n */\n subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToSyncHealth(cb);\n }\n\n /** Current local-store durability snapshot. See {@link useStorageStatus}. */\n get storageHealth(): StorageHealth {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.storageHealth;\n }\n\n /**\n * Observe local-store durability. Fires immediately with the current snapshot\n * and again on change. Prefer the `useStorageStatus` hook in components; this\n * is the imperative escape hatch.\n */\n subscribeToStorageHealth(cb: (health: StorageHealth) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToStorageHealth(cb);\n }\n\n bucket<B extends BucketNames<S>>(name: B): BucketHandle {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.bucket(name);\n }\n\n getBucketConfig(name: string): BucketDefinitionSchema | undefined {\n return this.config.schema.buckets?.find((b) => b.name === name);\n }\n}\n\nexport * from './types';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["Sp00kyClient","RecordId"],"sources":["../src/lib/conflate.ts","../src/lib/from-subscription.ts","../src/lib/context.ts","../src/lib/merge-rows.ts","../src/lib/create-query.ts","../src/lib/create-preload.ts","../src/lib/use-sync-status.ts","../src/lib/use-storage-status.ts","../src/lib/use-crdt-field.ts","../src/lib/use-feature-flag.ts","../src/lib/use-app-release.ts","../src/lib/use-file-upload.ts","../src/lib/use-download-file.ts","../src/lib/use-blurhash.ts","../src/lib/use-bucket-image.ts","../src/lib/Blurhash.ts","../src/lib/BucketImage.ts","../src/lib/Sp00kyProvider.ts","../src/lib/create-submission.ts","../src/index.ts"],"sourcesContent":["/**\n * Latest-wins async iterable over a subscribe-callback source.\n *\n * Bridges spooky's push-callback subscriptions into the AsyncIterable shape\n * Solid 2 computations consume natively. Each spooky emission is a full result\n * set, so intermediate values are droppable: only the newest unconsumed value\n * is buffered, and a pending pull resolves with it immediately.\n *\n * Teardown contract (probed in rc-semantics.test.ts): Solid 2 does NOT\n * terminate a superseded/disposed computation's async generator — no\n * `return()`, no `finally`. Consumers MUST call `it.return()` themselves from\n * an `onCleanup` registered synchronously in the compute scope. `return()`\n * unsubscribes (awaiting the unsubscribe if the subscribe returned a promise,\n * as `sp00ky.subscribe` does) and resolves any parked pull as done.\n */\nexport function conflate<T>(\n subscribe: (cb: (v: T) => void) => (() => void) | Promise<() => void>\n): AsyncIterable<T> {\n return {\n [Symbol.asyncIterator](): AsyncIterator<T> {\n let buffered: { v: T } | undefined;\n let resolveNext: ((r: IteratorResult<T>) => void) | undefined;\n let done = false;\n\n const unsubMaybe = subscribe((v) => {\n if (done) return;\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = undefined;\n r({ value: v, done: false });\n } else {\n buffered = { v };\n }\n });\n\n const finish = () => {\n if (done) return;\n done = true;\n buffered = undefined;\n // Unsubscribe may still be in flight (async registration); chain it.\n Promise.resolve(unsubMaybe)\n .then((unsub) => unsub())\n .catch(() => {\n // Registration failed — there is nothing to unsubscribe.\n });\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = undefined;\n r({ value: undefined as never, done: true });\n }\n };\n\n return {\n next(): Promise<IteratorResult<T>> {\n if (done) return Promise.resolve({ value: undefined as never, done: true });\n if (buffered) {\n const v = buffered.v;\n buffered = undefined;\n return Promise.resolve({ value: v, done: false });\n }\n return new Promise<IteratorResult<T>>((r) => (resolveNext = r));\n },\n return(): Promise<IteratorResult<T>> {\n finish();\n return Promise.resolve({ value: undefined as never, done: true });\n },\n throw(e: unknown): Promise<IteratorResult<T>> {\n finish();\n return Promise.reject(e);\n },\n };\n },\n };\n}\n","import { createMemo, onCleanup, type Accessor } from 'solid-js';\nimport { conflate } from './conflate';\n\n/**\n * Reactive view over a spooky subscribe-callback API.\n *\n * The memo's async generator pulls from a conflated (latest-wins) iterator;\n * `initial` is committed as the memo's `loadingValue`, so the accessor is\n * readable synchronously from birth and never suspends. Spooky's subscribe\n * APIs fire immediately with the current value, so the real value lands within\n * a tick of the first read.\n *\n * Teardown is manual by contract (see conflate.ts): onCleanup terminates the\n * iterator, which unsubscribes.\n */\nexport function fromSubscription<T>(\n subscribe: (cb: (v: T) => void) => (() => void) | Promise<() => void>,\n initial: T\n): Accessor<T> {\n return createMemo(\n async function* (): AsyncGenerator<T> {\n const it = conflate(subscribe)[Symbol.asyncIterator]();\n onCleanup(() => void it.return?.());\n while (true) {\n const r = await it.next();\n if (r.done) break;\n yield r.value;\n }\n },\n { loadingValue: initial }\n );\n}\n","import { createContext, useContext, type Accessor } from 'solid-js';\nimport type { SchemaStructure } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { fromSubscription } from './from-subscription';\n\n// Solid 2: the context object doubles as its provider component —\n// <Sp00kyContext value={db}>{children}</Sp00kyContext>.\nexport const Sp00kyContext = createContext<SyncedDb<any>>();\n\nexport function useDb<S extends SchemaStructure>(): SyncedDb<S> {\n try {\n return useContext(Sp00kyContext) as SyncedDb<S>;\n } catch {\n // Solid 2 throws ContextNotFoundError; rethrow with actionable guidance.\n throw new Error(\n 'useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.'\n );\n }\n}\n\n/**\n * Count of locally-committed mutations not yet acknowledged by the server.\n * Drive an \"unsaved changes\" indicator off this.\n */\nexport function usePendingMutations(): Accessor<number> {\n const db = useDb();\n return fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);\n}\n","import { snapshot } from 'solid-js';\n\n/**\n * Keyed in-place merge of a fresh row list into a store draft array.\n *\n * Why not `reconcile(rows, 'id')`: in Solid 2 rc.1 a reconcile REPLACES the row\n * objects, so a component that captured `data()[0]` sees a different object\n * after the next emission, and anything keyed on row identity (`<For>` without\n * an explicit key, a memo comparing rows) re-creates its subtree on every live\n * update. Mutating the draft in place keeps the identity AND keeps updates\n * fine-grained: only the fields that actually changed are written, so a row\n * whose data is unchanged notifies nobody.\n *\n * Rows are matched by `key` (default `id`); unmatched incoming rows are\n * inserted as-is and trailing rows are dropped, so add / remove / reorder all\n * reach coarse readers.\n */\n/**\n * Field-level equality for the merge. `===` is not enough: the decoder hands\n * out a fresh RecordId / Date instance for every record-link and datetime\n * column on every emission, so by identity every one of those fields \"changed\"\n * on every update - and a store write on an unchanged `id` re-ran everything a\n * page keyed on that row (a thread's anchor, its composer, its whole subtree).\n * Compare those wrappers by value; everything else stays identity (nested\n * objects and arrays are reconciled by their own writes).\n */\nexport function sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return false;\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n // RecordId (and the SDK's other value wrappers: Duration, Decimal, Uuid…)\n // all carry a stable `toString`; two of the same class that print alike ARE\n // the same value. Plain objects are excluded: their `toString` is\n // \"[object Object]\" and would make every object equal to every other.\n if (typeof a === 'object' && typeof b === 'object') {\n const ctor = (a as any).constructor;\n if (ctor && ctor === (b as any).constructor && ctor !== Object && ctor !== Array) {\n return String(a) === String(b);\n }\n }\n return false;\n}\n\nexport function mergeRows(draft: any[], next: any[], key = 'id'): void {\n const byKey = new Map<any, any>();\n for (const row of draft) {\n const k = row?.[key];\n if (k !== undefined) byKey.set(String(k), row);\n }\n\n for (let i = 0; i < next.length; i++) {\n const incoming = next[i];\n const k = incoming?.[key];\n const reuse = k !== undefined ? byKey.get(String(k)) : undefined;\n\n if (reuse) {\n for (const field of Object.keys(incoming)) {\n if (!sameValue(reuse[field], incoming[field])) reuse[field] = incoming[field];\n }\n // `snapshot` first: iterating the proxy's own keys while deleting from it\n // is not safe, and the raw object is what carries the stale fields.\n for (const field of Object.keys(snapshot(reuse))) {\n if (!(field in incoming)) delete reuse[field];\n }\n if (draft[i] !== reuse) draft[i] = reuse;\n } else if (draft[i] !== incoming) {\n draft[i] = incoming;\n }\n }\n\n if (draft.length > next.length) draft.splice(next.length);\n}\n","import type {\n ColumnSchema,\n FinalQuery,\n SchemaStructure,\n TableNames,\n QueryResult,\n} from '@spooky-sync/query-builder';\nimport {\n createEffect,\n createMemo,\n createSignal,\n createStore,\n onCleanup,\n type Accessor,\n} from 'solid-js';\nimport { SyncedDb } from '..';\nimport type { Sp00kyQueryResultPromise } from '@spooky-sync/core';\nimport { useDb } from './context';\nimport { mergeRows } from './merge-rows';\n\ntype QueryArg<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n> =\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | (() =>\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | null\n | undefined);\n\nexport type QueryOptions = {\n enabled?: () => boolean;\n /**\n * Tear down the query (remote `_00_query` view + local WASM view) when this\n * hook is disposed and no other subscriber remains, instead of keeping it\n * resident for cheap re-subscription. Use for viewport-windowed lists that\n * mount/unmount a query per scroll window and want off-screen windows\n * cancelled. Trade-off: scrolling back to a torn-down window re-registers it.\n */\n deregisterOnCleanup?: boolean;\n};\n\nexport type CreateQueryResult<TData> = {\n /**\n * Reactive result. Never suspends and never throws: born as an empty\n * committed value (`[]` / `null`) and reconciled in place (keyed by `id`) on\n * every live emission — unchanged rows keep identity, and coarse readers\n * (`<For>`) are notified on add/remove/reorder.\n */\n data: Accessor<TData>;\n /**\n * Suspending read of the same result for `<Loading>` users: throws Solid's\n * not-ready protocol until the query has delivered its first real result\n * (or errored, in which case it returns the empty value and `error()` is\n * set). Read this inside a `<Loading>` boundary.\n */\n ready: Accessor<TData>;\n error: Accessor<Error | undefined>;\n isLoading: Accessor<boolean>;\n isFetching: Accessor<boolean>;\n /**\n * True once the query has delivered a result AND no fetch cycle is in\n * flight (registration + initial sync included). While settled, results are\n * authoritative: a windowed query returning fewer rows than its LIMIT\n * really is the end of the list. Resets when the query identity changes.\n */\n isSettled: Accessor<boolean>;\n};\n\n// Overload: context-based (no explicit db)\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions\n): CreateQueryResult<TData>;\n\n// Overload: explicit db\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n db: SyncedDb<S>,\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions\n): CreateQueryResult<TData>;\n\n// Implementation\nexport function createQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends {\n columns: Record<string, ColumnSchema>;\n },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n dbOrQuery: SyncedDb<S> | QueryArg<S, TableName, T, RelatedFields, IsOne>,\n queryOrOptions?: QueryArg<S, TableName, T, RelatedFields, IsOne> | QueryOptions,\n maybeOptions?: QueryOptions\n): CreateQueryResult<TData> {\n let db: SyncedDb<S>;\n let finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>;\n let options: QueryOptions | undefined;\n\n if (dbOrQuery instanceof SyncedDb) {\n db = dbOrQuery;\n finalQuery = queryOrOptions as QueryArg<S, TableName, T, RelatedFields, IsOne>;\n options = maybeOptions;\n } else {\n db = useDb<S>();\n finalQuery = dbOrQuery;\n options = queryOrOptions as QueryOptions | undefined;\n }\n\n const sp00ky = db.getSp00ky();\n\n // Status channel. Written from subscription callbacks and generator\n // continuations, which run outside any tracking scope — `ownedWrite` opts\n // these signals out of Solid 2's owned-scope write guard.\n const [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true });\n const [isFetched, setIsFetched] = createSignal(false, { ownedWrite: true });\n const [isFetching, setIsFetching] = createSignal(false, { ownedWrite: true });\n\n // The hash of the currently-installed subscription, for opt-in deregister on\n // dispose (see `deregisterOnCleanup`).\n let activeHash: string | undefined;\n\n // Results live in a plain store, written from the engine's subscription\n // callback and reconciled in place keyed by `id`, so unchanged rows keep\n // identity and coarse readers (`<For>`) are notified on add/remove/reorder.\n //\n // NOT an async-generator projection. A live query's generator never returns:\n // it awaits the next emission forever, which leaves its node permanently\n // PENDING. Solid 2 holds a navigation transition until every pending node in\n // the new tree settles, so with a generator here the first client-side\n // navigation into a screen that opens a query never commits: the URL and\n // effects update while the old DOM stays on screen, with no error and no\n // <Loading> fallback that can rescue it. Subscription writes settle\n // immediately, which is what the Solid 1 binding did too.\n //\n // Wrapped in an object so `one()` queries (row object or null) and list\n // queries share one store shape; `mergeRows` keys `value`'s contents by id.\n const [store, setStore] = createStore<{ value: TData }>({ value: null as TData });\n\n // Identity of the installed subscription, so a superseded run cannot write\n // results for a query the caller has already moved off.\n let runId = 0;\n\n // Woken by the first result or error, for the suspending `ready()` read.\n let readyWaiters: (() => void)[] = [];\n const wakeReady = () => {\n const waiters = readyWaiters;\n readyWaiters = [];\n for (const w of waiters) w();\n };\n\n // Tracked: the query identity and the `enabled` gate. Untracked apply: the\n // registration + subscription, whose teardown is the returned cleanup.\n createEffect(\n () => {\n const enabled = options?.enabled?.() ?? true;\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n return { enabled, query };\n },\n ({ enabled, query }) => {\n const myRun = ++runId;\n // A new identity starts clean: a previous identity's failure must not\n // keep this one out of its loading state.\n setIsFetched(false);\n setError(undefined);\n if (!enabled || !query) return;\n\n const cleanups: (() => void)[] = [];\n let disposed = false;\n // `sp00ky.subscribe` resolves its unsubscribe asynchronously; the status\n // subscription returns one directly. Accept both, and if teardown already\n // happened while the promise was in flight, unsubscribe immediately.\n const addCleanup = (c: (() => void) | Promise<(() => void) | undefined> | undefined) => {\n if (!c) return;\n if (typeof c === 'function') {\n if (disposed) c();\n else cleanups.push(c);\n return;\n }\n void Promise.resolve(c).then((fn) => {\n if (typeof fn !== 'function') return;\n if (disposed) fn();\n else cleanups.push(fn);\n });\n };\n\n /**\n * Registration can fail — the canonical case is the SSP answering 503\n * NOT_READY while it bootstraps. Surface it as `error()` instead of\n * throwing into the graph: the sync scheduler retries the registration\n * underneath, so a transient failure still recovers, and a spinner\n * driven by `isLoading()` resolves via `error()`.\n */\n query\n .run()\n .then(({ hash }: { hash: string }) => {\n if (disposed || myRun !== runId) return;\n activeHash = hash;\n\n // Mirror the query's fetch status so the UI can show a \"loading\n // more\" state while the sync engine pulls records in the background.\n addCleanup(\n sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === 'fetching'), {\n immediate: true,\n })\n );\n\n let isFirstCall = true;\n addCleanup(\n sp00ky.subscribe(\n hash,\n (rows: Record<string, any>[]) => {\n if (disposed || myRun !== runId) return;\n const queryData = (query.isOne ? (rows[0] ?? null) : rows) as TData;\n // The first (immediate) callback with no data likely means the\n // local DB has not synced yet — don't mark as fetched, so the\n // UI keeps showing its loading state.\n const hasData = query.isOne\n ? queryData !== null && queryData !== undefined\n : rows.length > 0;\n if (!isFirstCall || hasData) {\n setIsFetched(true);\n wakeReady();\n }\n isFirstCall = false;\n\n const t0 = performance.now();\n setStore((s) => {\n if (query.isOne || queryData === null || !Array.isArray(s.value)) {\n s.value = queryData;\n } else {\n mergeRows(s.value as any[], queryData as any[]);\n }\n });\n sp00ky.reportFrontendTiming(hash, performance.now() - t0);\n },\n { immediate: true }\n )\n );\n })\n .catch((err: unknown) => {\n if (disposed || myRun !== runId) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n wakeReady();\n });\n\n return () => {\n disposed = true;\n for (const c of cleanups) c();\n };\n }\n );\n\n // Fallback empty value served before the first emission of a list query.\n const emptyList = [] as unknown as TData;\n\n const data: Accessor<TData> = () => {\n const v = store.value;\n if (v === null || v === undefined) {\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n if (query && !query.isOne) return emptyList;\n }\n return v as TData;\n };\n\n // Suspending read: pends until the first real result (or error). Reading it\n // inside <Loading> integrates with Solid 2's boundary protocol; `data` stays\n // non-throwing.\n //\n // `lazy` matters, and so does the promise actually resolving: an unresolved\n // async node stays PENDING, every queue flush carries pending nodes forward,\n // and a navigation transition waits on them. Lazy means only a query whose\n // `ready()` is read creates the node at all, and the deferred below is\n // settled by the same emission that flips `isFetched`.\n const readyGate = createMemo(\n async (): Promise<true> => {\n if (isFetched() || error()) return true;\n await new Promise<void>((resolve) => readyWaiters.push(resolve));\n return true;\n },\n { lazy: true }\n );\n const ready: Accessor<TData> = () => {\n readyGate();\n return data();\n };\n\n // Tear down the live subscription when the hook's owner is disposed. The\n // projection's own onCleanup (inside the compute) already unsubscribes; this\n // hook-scope cleanup only handles the opt-in query deregistration.\n onCleanup(() => {\n // Opt-in: cancel the query once this hook (its last subscriber) is gone.\n // The compute's cleanup removed this hook's callback, so deregisterQuery's\n // refcount guard sees the true remaining-subscriber count.\n if (options?.deregisterOnCleanup && activeHash) {\n sp00ky.deregisterQuery(activeHash);\n }\n });\n\n const isLoading = () => !isFetched() && error() === undefined;\n const isSettled = () => isFetched() && !isFetching();\n\n return {\n data,\n ready,\n error,\n isLoading,\n isFetching,\n isSettled,\n };\n}\n\n/** @deprecated Renamed `createQuery` in the Solid 2 binding. */\nexport const useQuery = createQuery;\n","import type {\n ColumnSchema,\n FinalQuery,\n SchemaStructure,\n TableNames,\n} from '@spooky-sync/query-builder';\nimport { createEffect } from 'solid-js';\nimport { SyncedDb } from '..';\nimport type {\n Sp00kyQueryResultPromise,\n PreloadOptions as CorePreloadOptions,\n} from '@spooky-sync/core';\nimport { useDb } from './context';\n\ntype PreloadArg<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n> =\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | (() =>\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, Sp00kyQueryResultPromise>\n | null\n | undefined);\n\ntype PreloadOptions = CorePreloadOptions & {\n /** Only preload while this returns true (defaults to always). */\n enabled?: () => boolean;\n};\n\n// Overload: context-based (no explicit db)\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n options?: PreloadOptions\n): void;\n\n// Overload: explicit db\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n db: SyncedDb<S>,\n finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n options?: PreloadOptions\n): void;\n\n/**\n * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a\n * function so it tracks reactive deps), dedupes on the query's stable identity\n * hash, and warms it into the local cache via `db.preload`. No subscription and\n * no cleanup: preload registers nothing that needs tearing down.\n *\n * Typical use: inside a list row, preload the detail query the user is likely\n * to open next, so navigation paints from cache instead of the network.\n */\nexport function createPreload<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n>(\n dbOrQuery: SyncedDb<S> | PreloadArg<S, TableName, T, RelatedFields, IsOne>,\n queryOrOptions?: PreloadArg<S, TableName, T, RelatedFields, IsOne> | PreloadOptions,\n maybeOptions?: PreloadOptions\n): void {\n let db: SyncedDb<S>;\n let finalQuery: PreloadArg<S, TableName, T, RelatedFields, IsOne>;\n let options: PreloadOptions | undefined;\n\n if (dbOrQuery instanceof SyncedDb) {\n db = dbOrQuery;\n finalQuery = queryOrOptions as PreloadArg<S, TableName, T, RelatedFields, IsOne>;\n options = maybeOptions;\n } else {\n db = useDb<S>();\n finalQuery = dbOrQuery;\n options = queryOrOptions as PreloadOptions | undefined;\n }\n\n let prevHash: number | undefined;\n\n // Two-arg Solid 2 effect: compute resolves the query (tracking its reactive\n // deps) and dedupes on the identity hash; the untracked apply fires the\n // preload. Returning `undefined` from compute skips nothing — apply guards.\n createEffect(\n () => {\n if (!(options?.enabled?.() ?? true)) return undefined;\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n if (!query) return undefined;\n // Dedupe on the query's stable identity hash so a reactive re-run with\n // an unchanged query doesn't refetch (the core also dedupes per session).\n if (query.hash === prevHash) return undefined;\n prevHash = query.hash;\n return query;\n },\n (query) => {\n if (!query) return;\n void db\n .getSp00ky()\n .preload(query, { refresh: options?.refresh, staleTime: options?.staleTime });\n }\n );\n}\n","import type { Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { ConnectionState, SyncHealth, SyncHealthStatus } from '@spooky-sync/core';\n\nexport interface UseSyncStatus {\n /** Full health snapshot; updates reactively on every transition. */\n health: Accessor<SyncHealth>;\n /** `'healthy'` | `'degraded'`. */\n status: Accessor<SyncHealthStatus>;\n isHealthy: Accessor<boolean>;\n /** `true` once sync has failed for a sustained run — drive a banner off this. */\n isDegraded: Accessor<boolean>;\n /** `true` once at least one sync round has succeeded this session. */\n everConnected: Accessor<boolean>;\n /**\n * `true` only for a real lost connection: degraded AFTER a first successful\n * sync. Stays `false` during the initial \"connecting\" phase (degraded but\n * never reached the server yet), so an indicator can show nothing until the\n * app has actually connected once.\n */\n isOffline: Accessor<boolean>;\n /**\n * Transport state of the remote WebSocket. Flips the instant the socket\n * drops, unlike `status`, which only degrades after a sustained run of failed\n * sync rounds — so this is what to drive a \"reconnecting…\" affordance off.\n */\n connection: Accessor<ConnectionState>;\n /**\n * `true` while the connection is being re-established. Usually still\n * `isHealthy()`: a short reconnect is invisible to sync, and writes made\n * during it are queued locally and pushed once the socket is back.\n */\n isReconnecting: Accessor<boolean>;\n}\n\n/**\n * Observe sync health for a \"can't reach the server\" banner / indicator.\n *\n * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient\n * remote 500 on query registration, a dropped socket) are absorbed by the\n * retry and never flip this; `isDegraded()` only goes true once failures\n * persist for the configured number of consecutive rounds (sp00ky core config\n * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on\n * the next successful round. Must be used within a `<Sp00kyProvider>`.\n */\nexport function useSyncStatus(): UseSyncStatus {\n const db = useDb();\n // The subscription fires synchronously with the current status; the initial\n // value (loadingValue) just avoids a flash before it lands.\n const health = fromSubscription<SyncHealth>((cb) => db.subscribeToSyncHealth(cb), db.syncHealth);\n\n return {\n health,\n status: () => health().status,\n isHealthy: () => health().status === 'healthy',\n isDegraded: () => health().status === 'degraded',\n everConnected: () => health().everConnected,\n isOffline: () => health().status === 'degraded' && health().everConnected,\n connection: () => health().connection,\n isReconnecting: () => health().connection === 'reconnecting',\n };\n}\n","import type { Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';\n\nexport interface UseStorageStatus {\n /** Full durability snapshot; updates reactively. */\n health: Accessor<StorageHealth>;\n /** `'unknown'` | `'persistent'` | `'memory'`. */\n status: Accessor<StorageHealthStatus>;\n /** `true` when the local store survives a reload. */\n isPersistent: Accessor<boolean>;\n /**\n * `true` only when durable storage was requested and could NOT be opened, so\n * the dataset is sitting in RAM and local writes die on reload. Drive a\n * warning off this, not off `status`: a store configured as in-memory reports\n * `'memory'` too, and that is a choice rather than a problem.\n */\n isMemoryFallback: Accessor<boolean>;\n}\n\n/**\n * Observe how durable the LOCAL cache is, for a \"no local storage\" warning.\n *\n * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is\n * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a\n * second tab of the same app cannot get it and runs in memory instead (the\n * engine retries first, so a closing tab's lock is usually waited out). Must be\n * used within a `<Sp00kyProvider>`.\n */\nexport function useStorageStatus(): UseStorageStatus {\n const db = useDb();\n const health = fromSubscription<StorageHealth>(\n (cb) => db.subscribeToStorageHealth(cb),\n db.storageHealth\n );\n\n return {\n health,\n status: () => health().status,\n isPersistent: () => health().status === 'persistent',\n isMemoryFallback: () => health().fallback,\n };\n}\n","import { createEffect, createSignal, type Accessor } from 'solid-js';\nimport { useDb } from './context';\nimport type { CrdtField } from '@spooky-sync/core';\n\nexport function useCrdtField(\n table: string,\n recordId: () => string | undefined,\n field: string,\n fallbackText?: () => string | undefined\n): Accessor<CrdtField | null> {\n const db = useDb();\n const [crdtField, setCrdtField] = createSignal<CrdtField | null>(null, { ownedWrite: true });\n\n // Two-arg Solid 2 effect: the compute tracks `recordId`, the apply owns the\n // open/close lifecycle and returns the cleanup — which runs both when the id\n // changes (before the next apply) and on unmount. That replaces the Solid 1\n // version's manual currentId/initialized bookkeeping.\n createEffect(\n () => recordId(),\n (id) => {\n if (!id) {\n setCrdtField(null);\n return;\n }\n const sp00ky = db.getSp00ky();\n let superseded = false;\n const text = fallbackText?.();\n sp00ky\n .openCrdtField(table, id, field, text)\n .then((cf) => {\n if (!superseded) {\n setCrdtField(cf);\n } else {\n sp00ky.closeCrdtField(table, id, field);\n }\n })\n .catch((err) => {\n // Silent rejections here leave the consumer's `Show when={field()}`\n // permanently stuck on its fallback (typically a static `<p>` with\n // no editing UI), with no error trail. Surface the failure so the\n // root cause (missing `@crdt` annotation, schema codegen drift,\n // local DB query failure, etc.) is visible in the console instead\n // of silently breaking collaborative fields.\n console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);\n });\n return () => {\n superseded = true;\n if (crdtField()) {\n sp00ky.closeCrdtField(table, id, field);\n setCrdtField(null);\n }\n };\n }\n );\n\n return crdtField;\n}\n","import type { Accessor } from 'solid-js';\nimport { onCleanup } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport type { FeatureFlagOptions } from '@spooky-sync/core';\n\nexport interface UseFeatureFlag {\n variant: Accessor<string | undefined>;\n payload: Accessor<unknown | undefined>;\n enabled: Accessor<boolean>;\n}\n\n/**\n * Subscribe to a feature flag for the currently authenticated user.\n *\n * Returns three Solid accessors that update reactively whenever the\n * server-materialized assignment in `_00_user_feature` changes. Backed by\n * the same SSP + sync pipeline that powers `createQuery`, so toggling a flag\n * via `spky flag enable <key>` propagates to the UI without a refresh.\n *\n * `enabled()` is `true` when the resolved variant exists and is not 'off'.\n * For multi-variant flags, prefer `variant()` directly.\n */\nexport function useFeatureFlag(key: string, options?: FeatureFlagOptions): UseFeatureFlag {\n const db = useDb();\n const handle = db.getSp00ky().feature(key, options);\n onCleanup(() => handle.close());\n\n const state = fromSubscription<{ variant: string | undefined; payload: unknown }>(\n (cb) =>\n handle.subscribe((s) => cb({ variant: s.variant ?? options?.fallback, payload: s.payload })),\n { variant: handle.variant(), payload: handle.payload() }\n );\n\n return {\n variant: () => state().variant,\n payload: () => state().payload,\n enabled: () => {\n const v = state().variant;\n return v !== undefined && v !== 'off';\n },\n };\n}\n","import type { Accessor } from 'solid-js';\nimport { onCleanup } from 'solid-js';\nimport { useDb } from './context';\nimport { fromSubscription } from './from-subscription';\nimport { semverGt, type AppReleaseOptions, type AppReleaseSnapshot } from '@spooky-sync/core';\n\nexport interface UseAppReleaseOptions extends AppReleaseOptions {\n /** App name from sp00ky.yml, e.g. `web`. */\n app: string;\n /**\n * The running build's version (X.Y.Z), typically baked in at build time\n * (e.g. a vite `define` from package.json). `updateAvailable()` is true when\n * the announced release is semver-newer than this.\n */\n currentVersion: string;\n}\n\nexport interface UseAppRelease {\n /** Latest announced version for the app, or undefined when no row exists. */\n latestVersion: Accessor<string | undefined>;\n /** Announced version is semver-newer than the running build. */\n updateAvailable: Accessor<boolean>;\n /** The newer release asks clients to update/reload without prompting. */\n mandatory: Accessor<boolean>;\n /** The newer release asks reloads to clear service-worker caches first. */\n cacheBust: Accessor<boolean>;\n /**\n * Reload onto the announced release. Plain `location.reload()` normally;\n * when the release is flagged cache-bust, CacheStorage is cleared, the\n * service-worker registration is nudged to update, and navigation carries a\n * `?cb=` token to punch through intermediary caches. The service worker is\n * deliberately NOT unregistered: navigating while still controlled by a\n * just-unregistered worker strands subresource fetches on the dead worker\n * and the page hangs until a manual reload.\n */\n reload: () => Promise<void>;\n}\n\nasync function reloadForSnapshot(snapshot: AppReleaseSnapshot): Promise<void> {\n if (typeof window === 'undefined') return;\n if (snapshot.cacheBust) {\n try {\n if (window.caches) {\n const keys = await window.caches.keys();\n await Promise.all(keys.map((k) => window.caches.delete(k)));\n }\n if (navigator.serviceWorker) {\n const regs = await navigator.serviceWorker.getRegistrations();\n for (const r of regs) r.update().catch(() => {});\n }\n window.location.href = window.location.pathname + '?cb=' + Date.now();\n return;\n } catch {\n /* fall through to a plain reload */\n }\n }\n window.location.reload();\n}\n\n/**\n * Observe the app's announced release (`_00_app_release:<app>`, written by\n * `spky deploy` / `spky release`) and compare it against the running build.\n *\n * Typical use: mount a small \"new version available — Reload\" notification\n * gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`\n * (guard the auto path against reload loops with a per-version marker, since\n * a client can reload while the deploy is still rolling out and land on the\n * old bundle again).\n */\nexport function useAppRelease(options: UseAppReleaseOptions): UseAppRelease {\n const db = useDb();\n const handle = db.getSp00ky().appRelease(options.app, { ttl: options.ttl });\n onCleanup(() => handle.close());\n\n const snapshot = fromSubscription<AppReleaseSnapshot>(\n (cb) => handle.subscribe(cb),\n handle.snapshot()\n );\n\n const updateAvailable = () => semverGt(snapshot().version, options.currentVersion);\n\n return {\n latestVersion: () => snapshot().version,\n updateAvailable,\n mandatory: () => updateAvailable() && snapshot().mandatory,\n cacheBust: () => snapshot().cacheBust,\n reload: () => reloadForSnapshot(snapshot()),\n };\n}\n","import { createSignal, onCleanup } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport { fileToUint8Array } from '@spooky-sync/core';\nimport type { BucketPutOptions, BucketPutResult } from '@spooky-sync/core';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface FileUploadResult {\n isUploading: () => boolean;\n error: () => Error | null;\n clearError: () => void;\n upload: (\n path: string,\n file: File | Blob,\n options?: BucketPutOptions\n ) => Promise<BucketPutResult | void>;\n download: (path: string) => Promise<string | null>;\n remove: (path: string) => Promise<void>;\n exists: (path: string) => Promise<boolean>;\n}\n\nexport function useFileUpload<S extends SchemaStructure>(\n bucketName: BucketNames<S>\n): FileUploadResult;\nexport function useFileUpload<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>\n): FileUploadResult;\nexport function useFileUpload<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n maybeBucketName?: BucketNames<S>\n): FileUploadResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n // oxlint-disable-next-line no-non-null-assertion\n bucketName = maybeBucketName!;\n }\n\n // Written from async continuations — outside any tracking scope.\n const [isUploading, setIsUploading] = createSignal(false, { ownedWrite: true });\n const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });\n\n const objectUrls: string[] = [];\n onCleanup(() => {\n for (const url of objectUrls) {\n URL.revokeObjectURL(url);\n }\n });\n\n const clearError = () => setError(null);\n\n const validate = (file: File | Blob): void => {\n const config = db.getBucketConfig(bucketName as string);\n if (!config) return;\n\n if (config.maxSize !== null && config.maxSize !== undefined && file.size > config.maxSize) {\n const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);\n throw new Error(`File exceeds maximum size of ${maxMB} MB.`);\n }\n\n if (config.allowedExtensions && config.allowedExtensions.length > 0) {\n const fileName = (file as File).name;\n if (fileName) {\n const ext = fileName.split('.').pop()?.toLowerCase();\n if (!ext || !config.allowedExtensions.includes(ext)) {\n throw new Error(\n `File type not allowed. Accepted: ${config.allowedExtensions.join(', ')}.`\n );\n }\n }\n }\n };\n\n const upload = async (\n path: string,\n file: File | Blob,\n options?: BucketPutOptions\n ): Promise<BucketPutResult | void> => {\n setError(null);\n try {\n validate(file);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n\n setIsUploading(true);\n try {\n const bytes = await fileToUint8Array(file);\n return await db.bucket(bucketName).put(path, bytes, options);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n setIsUploading(false);\n }\n };\n\n const download = async (path: string): Promise<string | null> => {\n setError(null);\n try {\n const content = await db.bucket(bucketName).get(path);\n if (!content) return null;\n const objectUrl = URL.createObjectURL(new Blob([content as BlobPart]));\n objectUrls.push(objectUrl);\n return objectUrl;\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return null;\n }\n };\n\n const remove = async (path: string): Promise<void> => {\n setError(null);\n try {\n await db.bucket(bucketName).delete(path);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n }\n };\n\n const exists = async (path: string): Promise<boolean> => {\n setError(null);\n try {\n return await db.bucket(bucketName).exists(path);\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return false;\n }\n };\n\n return {\n isUploading,\n error,\n clearError,\n upload,\n download,\n remove,\n exists,\n };\n}\n","import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { BlobUrlLease } from '@spooky-sync/core';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface UseDownloadFileOptions {\n /**\n * Master switch, default `true`. `false` gives every hook instance its own\n * private object URL fetched fresh from the bucket and revoked on unmount —\n * no sharing, no persistence, no reuse.\n */\n cache?: boolean;\n /**\n * Keep the bytes in OPFS so they survive a reload and are available offline.\n * Default `true`. Turn off for one-shot or sensitive files; the in-tab object\n * URL is still shared between components rendering the same path.\n */\n persist?: boolean;\n /** Exempt this file from pressure eviction. Pinned bytes never expire. */\n pin?: boolean;\n /**\n * `'never'` (default) treats a bucket path as immutable, which is how paths\n * are written (`crypto.randomUUID() + ext`). `'head'` spends a remote `head()`\n * to compare sizes before trusting the cached copy — for paths the app\n * overwrites in place.\n */\n revalidate?: 'never' | 'head';\n}\n\nexport interface UseDownloadFileResult {\n url: Accessor<string | null>;\n isLoading: Accessor<boolean>;\n error: Accessor<Error | null>;\n refetch: () => void;\n}\n\nexport function useDownloadFile<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseDownloadFileOptions\n): UseDownloadFileResult;\nexport function useDownloadFile<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseDownloadFileOptions\n): UseDownloadFileResult;\nexport function useDownloadFile<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n pathOrOptions?: Accessor<string | null | undefined> | UseDownloadFileOptions,\n maybeOptions?: UseDownloadFileOptions\n): UseDownloadFileResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n let options: UseDownloadFileOptions;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n options = (pathOrOptions as UseDownloadFileOptions) ?? {};\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = pathOrOptions as Accessor<string | null | undefined>;\n options = maybeOptions ?? {};\n }\n\n const useCache = options.cache !== false;\n\n // Written from fetch continuations — outside any tracking scope.\n const [url, setUrl] = createSignal<string | null>(null, { ownedWrite: true });\n const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });\n const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });\n\n // Exactly one of these is held at a time: a refcounted lease on the shared\n // cache entry, or a private URL this instance minted and must revoke itself.\n let lease: BlobUrlLease | null = null;\n let privateUrl: string | null = null;\n\n const [refetchSignal, setRefetchSignal] = createSignal(0);\n /** Consumed by the next effect run, so `refetch()` bypasses every layer once. */\n let reloadOnce = false;\n\n function releaseCurrent() {\n lease?.release();\n lease = null;\n if (privateUrl) {\n URL.revokeObjectURL(privateUrl);\n privateUrl = null;\n }\n }\n\n // Two-arg Solid 2 effect: compute tracks path + refetch tick; apply runs the\n // fetch and returns the cancel/release cleanup, which runs before the next\n // apply and on unmount.\n createEffect(\n () => {\n refetchSignal();\n return path();\n },\n (filePath) => {\n releaseCurrent();\n\n if (!filePath) {\n setUrl(null);\n setIsLoading(false);\n setError(null);\n return;\n }\n\n const reload = reloadOnce;\n reloadOnce = false;\n\n let cancelled = false;\n setIsLoading(true);\n setError(null);\n\n const bucket = db.bucket(bucketName);\n const resolve = useCache\n ? bucket\n .url(filePath, {\n persist: options.persist !== false,\n pin: options.pin,\n revalidate: options.revalidate,\n reload,\n })\n .then((acquired) => {\n if (!acquired) return null;\n if (cancelled) {\n // Unmounted or the path changed mid-flight — hand the reference\n // straight back, or the entry never drops to zero and its object\n // URL leaks for the life of the tab.\n acquired.release();\n return null;\n }\n lease = acquired;\n return acquired.url;\n })\n : bucket.read(filePath, { persist: false, reload: true }).then((blob) => {\n if (!blob || cancelled) return null;\n privateUrl = URL.createObjectURL(blob);\n return privateUrl;\n });\n\n resolve.then(\n (result) => {\n if (!cancelled) {\n setUrl(result);\n setIsLoading(false);\n }\n return undefined;\n },\n (err) => {\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsLoading(false);\n }\n }\n );\n\n return () => {\n cancelled = true;\n };\n }\n );\n\n onCleanup(() => {\n releaseCurrent();\n });\n\n const refetch = () => {\n reloadOnce = true;\n setRefetchSignal((n) => n + 1);\n };\n\n return { url, isLoading, error, refetch };\n}\n","import { createSignal, createEffect, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\n\nexport interface UseBlurhashResult {\n /** The stored blurhash for the path, or null while loading / when none exists. */\n hash: Accessor<string | null>;\n isLoading: Accessor<boolean>;\n}\n\n/**\n * The blurhash sidecar for a bucket image (written automatically by\n * `bucket.put`, see `Sp00kyConfig.blurhash`). Resolves from OPFS instantly on\n * warm clients; a miss is remembered per tab. Use this directly when the hash\n * belongs to a different rendition than the displayed image; otherwise\n * `useBucketImage` / `BucketImage` bundle it with the download.\n */\nexport function useBlurhash<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>\n): UseBlurhashResult;\nexport function useBlurhash<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>\n): UseBlurhashResult;\nexport function useBlurhash<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n maybePath?: Accessor<string | null | undefined>\n): UseBlurhashResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = maybePath as Accessor<string | null | undefined>;\n }\n\n // Written from the lookup continuation, outside any tracking scope.\n const [hash, setHash] = createSignal<string | null>(null, { ownedWrite: true });\n const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });\n\n // Compute tracks the path; apply runs the lookup and returns the cancel\n // cleanup, which runs before the next apply and on unmount.\n createEffect(\n () => path(),\n (filePath) => {\n if (!filePath) {\n setHash(null);\n setIsLoading(false);\n return;\n }\n let cancelled = false;\n setIsLoading(true);\n db.bucket(bucketName)\n .blurhash(filePath)\n .then((result) => {\n if (cancelled) return;\n setHash(result);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setHash(null);\n setIsLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }\n );\n\n return { hash, isLoading };\n}\n","import { createSignal, createEffect, type Accessor } from 'solid-js';\nimport type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';\nimport type { SyncedDb } from '../index';\nimport { useDb } from './context';\nimport {\n useDownloadFile,\n type UseDownloadFileOptions,\n type UseDownloadFileResult,\n} from './use-download-file';\nimport { useBlurhash } from './use-blurhash';\n\nexport interface UseBucketImageOptions extends UseDownloadFileOptions {\n /**\n * Also resolve the image's blurhash sidecar (see `Sp00kyConfig.blurhash`).\n * Default `true`; the read is registered before the image bytes so the tiny\n * sidecar tends to land first on the serialized remote chain.\n */\n blurhash?: boolean;\n}\n\nexport interface UseBucketImageResult extends UseDownloadFileResult {\n /** Blurhash for the same path, or null (off, missing, still loading). */\n blurhash: Accessor<string | null>;\n /** True once the current `url()` has been decoded and is safe to paint. */\n ready: Accessor<boolean>;\n /**\n * Ref callback for the `<img>` rendering `url()`: flips `ready` when the\n * bitmap is decoded (resolves on failure too, so a broken blob degrades to\n * paint-on-load instead of hiding the image forever). Re-arms itself when\n * the url changes.\n */\n gate: (img: HTMLImageElement) => void;\n}\n\n/**\n * Everything needed to render a bucket image without a pop-in: the refcounted\n * object URL, the blurhash placeholder, and a decode gate so the real bitmap\n * is only revealed once it can paint in full. `BucketImage` wraps this into a\n * drop-in component; use the hook for custom markup.\n */\nexport function useBucketImage<S extends SchemaStructure>(\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseBucketImageOptions\n): UseBucketImageResult;\nexport function useBucketImage<S extends SchemaStructure>(\n db: SyncedDb<S>,\n bucketName: BucketNames<S>,\n path: Accessor<string | null | undefined>,\n options?: UseBucketImageOptions\n): UseBucketImageResult;\nexport function useBucketImage<S extends SchemaStructure>(\n dbOrBucketName: SyncedDb<S> | BucketNames<S>,\n bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,\n pathOrOptions?: Accessor<string | null | undefined> | UseBucketImageOptions,\n maybeOptions?: UseBucketImageOptions\n): UseBucketImageResult {\n let db: SyncedDb<S>;\n let bucketName: BucketNames<S>;\n let path: Accessor<string | null | undefined>;\n let options: UseBucketImageOptions;\n\n if (typeof dbOrBucketName === 'string') {\n db = useDb<S>();\n bucketName = dbOrBucketName as BucketNames<S>;\n path = bucketNameOrPath as Accessor<string | null | undefined>;\n options = (pathOrOptions as UseBucketImageOptions) ?? {};\n } else {\n db = dbOrBucketName as SyncedDb<S>;\n bucketName = bucketNameOrPath as BucketNames<S>;\n path = pathOrOptions as Accessor<string | null | undefined>;\n options = maybeOptions ?? {};\n }\n\n // Registered BEFORE the download so the sidecar read enters the serialized\n // remote queue first: the placeholder should never wait behind the bytes it\n // is standing in for.\n const wantHash = options.blurhash !== false;\n const { hash } = useBlurhash(db, bucketName, () => (wantHash ? path() : null));\n\n const file = useDownloadFile(db, bucketName, path, options);\n\n // Written from the decode continuation (and from the url effect's apply\n // phase), outside any tracking scope.\n const [ready, setReady] = createSignal(false, { ownedWrite: true });\n // A new url (path change, refetch) means a new undecoded bitmap.\n createEffect(\n file.url,\n () => {\n setReady(false);\n },\n { defer: true }\n );\n\n const gate = (img: HTMLImageElement) => {\n const done = () => setReady(true);\n if (typeof img.decode === 'function') {\n img.decode().then(done, done);\n } else if (img.complete) {\n done();\n } else {\n img.onload = done;\n img.onerror = done;\n }\n };\n\n return { ...file, blurhash: hash, ready, gate };\n}\n","import { createEffect, type Element } from 'solid-js';\nimport { decodeBlurhash } from '@spooky-sync/core';\n\nexport interface BlurhashProps {\n /** The blurhash string. Nullish paints nothing (transparent canvas). */\n hash: string | null | undefined;\n /** Decode resolution. 32x32 is plenty: blurhash carries at most 9x9 DCT\n * components, the canvas is meant to be CSS-scaled to fill. */\n width?: number;\n height?: number;\n /** Contrast punch, see the blurhash reference decoder. Defaults to 1. */\n punch?: number;\n class?: string;\n style?: string;\n}\n\n/**\n * A blurhash painted onto a canvas, once per hash change. Size the canvas via\n * `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode\n * resolution stays tiny regardless of the displayed size.\n */\nexport function Blurhash(props: BlurhashProps): Element {\n if (typeof document === 'undefined') return null;\n const canvas = document.createElement('canvas');\n\n createEffect(\n () => props.class ?? '',\n (className) => {\n canvas.className = className;\n }\n );\n createEffect(\n () => props.style ?? '',\n (style) => {\n canvas.style.cssText = style;\n }\n );\n\n createEffect(\n () => ({\n width: props.width ?? 32,\n height: props.height ?? 32,\n hash: props.hash,\n punch: props.punch ?? 1,\n }),\n ({ width, height, hash, punch }) => {\n canvas.width = width;\n canvas.height = height;\n if (!hash) return;\n try {\n const pixels = decodeBlurhash(hash, width, height, punch);\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n const imageData = ctx.createImageData(width, height);\n imageData.data.set(pixels);\n ctx.putImageData(imageData, 0, 0);\n } catch {\n // An invalid hash paints nothing; the layer below stays visible.\n }\n }\n );\n\n return canvas as unknown as Element;\n}\n","import { createSignal, createEffect, onSettled, children, type Element } from 'solid-js';\nimport type { BucketNames, SchemaStructure } from '@spooky-sync/query-builder';\nimport { useBucketImage, type UseBucketImageOptions } from './use-bucket-image';\nimport { Blurhash } from './Blurhash';\n\nexport interface BucketImageProps {\n /** Bucket name from the schema. */\n bucket: string;\n /** Path within the bucket. Nullish renders only the fallback layers. */\n path: string | null | undefined;\n alt?: string;\n /** Classes for the container element (sizing/positioning). */\n class?: string;\n /** Classes for the inner `<img>` (the layout styles are inline). */\n imgClass?: string;\n /** `object-fit` for the image. Defaults to `cover`. */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n /**\n * Bottom placeholder layer (your own plate/skeleton), shown until the image\n * settles. The blurhash layer paints on top of it once the sidecar resolves.\n */\n fallback?: Element;\n /** Crossfade duration in ms. Defaults to 300. */\n transition?: number;\n /** Crossfade easing. Defaults to an ease-out-expo curve. */\n easing?: string;\n /** Resolve the blurhash sidecar. Defaults to true. */\n blurhash?: boolean;\n /** Download tuning, forwarded to the underlying `useDownloadFile`. */\n options?: UseBucketImageOptions;\n}\n\nconst LAYER_STYLE = 'position:absolute;inset:0;width:100%;height:100%;';\n\n/**\n * A bucket image that never pops in: it layers (bottom to top) your `fallback`\n * plate, the automatically stored blurhash, and the real image, which stays\n * transparent until the bitmap is DECODED and then crossfades over the\n * placeholders. Placeholder layers unmount once the fade settles. Respects\n * prefers-reduced-motion (instant swap). The container is made\n * `position: relative` unless your `class` positions it already.\n *\n * ```tsx\n * <BucketImage bucket=\"covers\" path={row.cover_key} class=\"absolute inset-0\"\n * fallback={<MyPlate />} alt=\"\" />\n * ```\n */\nexport function BucketImage(props: BucketImageProps): Element {\n if (typeof document === 'undefined') return null;\n\n const image = useBucketImage(\n props.bucket as BucketNames<SchemaStructure>,\n () => props.path,\n { ...props.options, blurhash: props.blurhash !== false }\n );\n\n const reducedMotion =\n typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const root = document.createElement('div');\n createEffect(\n () => props.class ?? '',\n (className) => {\n root.className = className;\n }\n );\n // Layers are absolutely positioned; give them an anchor without stomping on\n // a caller class that already positions the container (inline would win).\n onSettled(() => {\n if (getComputedStyle(root).position === 'static') root.style.position = 'relative';\n });\n\n const placeholder = document.createElement('div');\n placeholder.style.cssText = LAYER_STYLE;\n // Solid hands JSX props over as lazy thunks; `children` resolves them (and\n // nested arrays/functions) to real nodes and keeps them alive reactively.\n const fallbackHolder = document.createElement('div');\n fallbackHolder.style.cssText = LAYER_STYLE;\n placeholder.append(fallbackHolder);\n const resolvedFallback = children(() => props.fallback);\n createEffect(\n () => resolvedFallback.toArray().filter((node) => node instanceof Node) as Node[],\n (nodes) => {\n fallbackHolder.replaceChildren(...nodes);\n }\n );\n const hashCanvas = Blurhash({\n get hash() {\n return image.blurhash();\n },\n style: LAYER_STYLE,\n });\n if (hashCanvas instanceof Node) placeholder.append(hashCanvas);\n\n const img = document.createElement('img');\n img.decoding = 'async';\n img.style.cssText = `${LAYER_STYLE}opacity:0;`;\n createEffect(\n () => props.imgClass ?? '',\n (className) => {\n img.className = className;\n }\n );\n createEffect(\n () => props.fit ?? 'cover',\n (fit) => {\n img.style.objectFit = fit;\n }\n );\n createEffect(\n () => props.alt ?? '',\n (alt) => {\n img.alt = alt;\n }\n );\n createEffect(\n () =>\n reducedMotion\n ? 'none'\n : `opacity ${props.transition ?? 300}ms ${props.easing ?? 'cubic-bezier(0.16, 1, 0.3, 1)'}`,\n (transition) => {\n img.style.transition = transition;\n }\n );\n createEffect(\n () => image.url(),\n (url) => {\n if (!url) {\n img.removeAttribute('src');\n return;\n }\n img.src = url;\n image.gate(img);\n }\n );\n createEffect(\n () => image.ready(),\n (ready) => {\n img.style.opacity = ready ? '1' : '0';\n }\n );\n\n // Placeholders leave the DOM once the fade is over (a shelf of covers should\n // not composite three layers each forever) and come back when the path\n // changes mid-life (`ready` re-arms via the hook). `settled` is written from\n // the apply phase and a timer, outside any tracking scope.\n const [settled, setSettled] = createSignal(false, { ownedWrite: true });\n createEffect(\n () => ({ ready: image.ready(), transition: props.transition ?? 300 }),\n ({ ready, transition }) => {\n if (!ready) {\n setSettled(false);\n return;\n }\n const wait = (reducedMotion ? 0 : transition) + 120;\n const timer = setTimeout(() => setSettled(true), wait);\n return () => clearTimeout(timer);\n }\n );\n createEffect(\n () => settled(),\n (isSettled) => {\n if (isSettled) placeholder.remove();\n else if (!placeholder.isConnected) root.insertBefore(placeholder, img);\n }\n );\n\n root.append(placeholder, img);\n return root as unknown as Element;\n}\n","import type { Element } from 'solid-js';\nimport {\n createSignal,\n onSettled,\n onCleanup,\n createComponent,\n createMemo,\n merge,\n} from 'solid-js';\nimport type { SchemaStructure } from '@spooky-sync/query-builder';\nimport type { SyncedDbConfig } from '../types';\nimport { SyncedDb } from '../index';\nimport { Sp00kyContext } from './context';\n\nexport interface Sp00kyProviderProps<S extends SchemaStructure> {\n config: SyncedDbConfig<S>;\n fallback?: Element;\n onError?: (error: Error) => void;\n onReady?: (db: SyncedDb<S>) => void;\n /**\n * Prewarm data into the local cache before revealing the UI. Runs after\n * `init()`; the `fallback` stays visible until it resolves. Use awaitable\n * `db.preload(...)` calls here to gate first-load on essential data (e.g.\n * config). On warm loads preload returns instantly, so there's no perceptible\n * gate after the first run. Best-effort: a rejection is caught and the UI is\n * revealed anyway.\n */\n preload?: (db: SyncedDb<S>) => Promise<void>;\n children: Element;\n}\n\nexport function Sp00kyProvider<S extends SchemaStructure>(\n props: Sp00kyProviderProps<S>\n): Element {\n const merged = merge({ fallback: undefined as Element | undefined }, props);\n\n // Written from the async init continuation — outside any tracking scope.\n const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined, { ownedWrite: true });\n\n // Init is async, so a dispose can land mid-init. Only that narrow race is\n // handled here: an instance whose init finished AFTER the provider was\n // already gone is closed, because nothing will ever reference it.\n //\n // A live, mounted client is deliberately NOT closed on cleanup. Doing that\n // nulls `SyncedDb.sp00ky`, so every later `create`/`update`/`delete` throws\n // \"SyncedDb not initialized\" while reads keep rendering from state that is\n // already subscribed — i.e. mutations die silently and the app looks fine. In\n // a host app the provider wraps the whole tree and only unmounts with the\n // page, where the browser reclaims the worker anyway, so the leak this was\n // meant to fix is worth far less than that risk.\n let disposed = false;\n\n onCleanup(() => {\n disposed = true;\n });\n\n // `onSettled` replaces Solid 1's `onMount`.\n onSettled(() => {\n void (async () => {\n try {\n const instance = new SyncedDb<S>(merged.config);\n await instance.init();\n if (disposed) {\n await instance.close();\n return;\n }\n // Gate first-load UI on prewarmed data. Best-effort: never let a\n // preload failure keep the app stuck on the fallback.\n if (merged.preload) {\n try {\n await merged.preload(instance);\n } catch (e) {\n // oxlint-disable-next-line no-console\n console.error('Sp00kyProvider: preload failed; revealing UI anyway', e);\n }\n }\n setDb(() => instance);\n merged.onReady?.(instance);\n } catch (e) {\n const error = e instanceof Error ? e : new Error(String(e));\n if (merged.onError) {\n merged.onError(error);\n } else {\n // oxlint-disable-next-line no-console\n console.error('Sp00kyProvider: Failed to initialize database', error);\n }\n }\n })();\n });\n\n const content = createMemo(() => {\n const instance = db();\n if (!instance) return merged.fallback;\n // Solid 2: the context object IS the provider component.\n return createComponent(Sp00kyContext, {\n value: instance,\n get children() {\n return merged.children;\n },\n });\n });\n\n return content as unknown as Element;\n}\n","import { createSignal, type Accessor } from 'solid-js';\n\nexport interface Submission<Args extends unknown[], R> {\n /** Run the wrapped async fn. Concurrent submits share the pending flag. */\n submit: (...args: Args) => Promise<R | undefined>;\n /** True while at least one submit is in flight. */\n pending: Accessor<boolean>;\n /** Error from the most recent settled submit, cleared on the next submit. */\n error: Accessor<Error | undefined>;\n /** Result of the most recent successful submit. */\n result: Accessor<R | undefined>;\n clearError: () => void;\n}\n\n/**\n * Thin submission-state wrapper for mutations — button spinner/disable state\n * around `db.create/update/delete/run` calls.\n *\n * Deliberately NOT built on Solid 2's `action()`/`createOptimisticStore`: the\n * spooky engine is already optimistic local-first (writes commit to the local\n * DB and re-render through live queries before sync; `run()` is an outbox\n * CREATE), so a transaction/revert layer on top buys nothing and `action()`'s\n * await-vs-yield transaction escape is a real footgun. Errors here mean the\n * LOCAL commit failed — sync/push failures surface through `useSyncStatus`\n * and `usePendingMutations` instead.\n */\nexport function createSubmission<Args extends unknown[], R>(\n fn: (...args: Args) => Promise<R>\n): Submission<Args, R> {\n // Written from promise continuations — outside any tracking scope.\n const [inFlight, setInFlight] = createSignal(0, { ownedWrite: true });\n const [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true });\n const [result, setResult] = createSignal<R | undefined>(undefined, { ownedWrite: true });\n\n const submit = async (...args: Args): Promise<R | undefined> => {\n setError(undefined);\n setInFlight((n) => n + 1);\n try {\n const r = await fn(...args);\n setResult(() => r);\n return r;\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n return undefined;\n } finally {\n setInFlight((n) => n - 1);\n }\n };\n\n return {\n submit,\n pending: () => inFlight() > 0,\n error,\n result,\n clearError: () => setError(undefined),\n };\n}\n","import type { SyncedDbConfig } from './types';\nimport {\n Sp00kyClient,\n type Sp00kyQueryResultPromise,\n type AuthService,\n type BucketHandle,\n type UpdateOptions,\n type RunOptions,\n type SyncHealth,\n type StorageHealth,\n type PreloadOptions,\n type PreloadRefresh,\n} from '@spooky-sync/core';\n\nimport type {\n GetTable,\n QueryBuilder,\n SchemaStructure,\n TableModel,\n TableNames,\n QueryResult,\n RelatedFieldsMap,\n RelationshipFieldsFromSchema,\n GetRelationship,\n RelatedFieldMapEntry,\n FinalQuery,\n InnerQuery,\n BackendNames,\n BackendRoutes,\n RoutePayload,\n BucketNames,\n BucketDefinitionSchema,\n QueryModifier,\n QueryModifierBuilder,\n QueryInfo,\n RelationshipsMetadata,\n RelationshipDefinition,\n InferRelatedModelFromMetadata,\n GetCardinality,\n} from '@spooky-sync/query-builder';\n\nimport { RecordId, Uuid, type Surreal } from 'surrealdb';\nimport { snapshot } from 'solid-js';\nexport { RecordId, Uuid };\nexport type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';\nexport { createQuery, useQuery, type CreateQueryResult, type QueryOptions } from './lib/create-query';\nexport { createPreload } from './lib/create-preload';\nexport type { PreloadOptions, PreloadRefresh } from '@spooky-sync/core';\nexport { useSyncStatus, type UseSyncStatus } from './lib/use-sync-status';\nexport type {\n SyncHealth,\n SyncHealthStatus,\n SyncHealthConfig,\n ConnectionState,\n ReconnectConfig,\n} from '@spooky-sync/core';\nexport { useStorageStatus, type UseStorageStatus } from './lib/use-storage-status';\nexport type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';\nexport { useCrdtField } from './lib/use-crdt-field';\nexport { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';\nexport {\n useAppRelease,\n type UseAppRelease,\n type UseAppReleaseOptions,\n} from './lib/use-app-release';\nexport { useFileUpload, type FileUploadResult } from './lib/use-file-upload';\nexport {\n useDownloadFile,\n type UseDownloadFileOptions,\n type UseDownloadFileResult,\n} from './lib/use-download-file';\nexport { useBlurhash, type UseBlurhashResult } from './lib/use-blurhash';\nexport {\n useBucketImage,\n type UseBucketImageOptions,\n type UseBucketImageResult,\n} from './lib/use-bucket-image';\nexport { Blurhash, type BlurhashProps } from './lib/Blurhash';\nexport { BucketImage, type BucketImageProps } from './lib/BucketImage';\nexport { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';\nexport { useDb, usePendingMutations } from './lib/context';\nexport { createSubmission, type Submission } from './lib/create-submission';\nexport { conflate } from './lib/conflate';\nexport { fromSubscription } from './lib/from-subscription';\n\n// Re-export query builder types for convenience\nexport type {\n QueryModifier,\n QueryModifierBuilder,\n QueryInfo,\n RelationshipsMetadata,\n RelationshipDefinition,\n InferRelatedModelFromMetadata,\n GetCardinality,\n GetTable,\n TableModel,\n TableNames,\n QueryResult,\n};\n\nexport type RelationshipField<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n Field extends RelationshipFieldsFromSchema<Schema, TableName>,\n> = GetRelationship<Schema, TableName, Field>;\n\nexport type RelatedFieldsTableScoped<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelationshipFieldsFromSchema<Schema, TableName> =\n RelationshipFieldsFromSchema<Schema, TableName>,\n> = {\n [K in RelatedFields]: {\n to: RelationshipField<Schema, TableName, K>['to'];\n relatedFields: RelatedFieldsMap;\n cardinality: RelationshipField<Schema, TableName, K>['cardinality'];\n };\n};\n\nexport type InferModel<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelatedFieldsTableScoped<Schema, TableName>,\n> = QueryResult<Schema, TableName, RelatedFields, true>;\n\nexport type WithRelated<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: Omit<RelatedFieldMapEntry, 'relatedFields'> & {\n relatedFields: RelatedFields;\n };\n};\n\nexport type WithRelatedMany<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: {\n to: Field;\n relatedFields: RelatedFields;\n cardinality: 'many';\n };\n};\n\n/**\n * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration.\n * Delegates all logic to the underlying sp00ky-ts instance.\n *\n * NOTE: keep in sync with packages/client-solid/src/index.ts (SyncedDb).\n * Copied rather than shared so this package's dependency graph never pulls\n * in solid-js 1.x; fold the two together once client-solid moves to Solid 2.\n * Solid-2-only differences: `delete` accepts 'bound RecordId', and write\n * payloads go through `snapshot()` (see `unproxy` below).\n */\n\n/**\n * Solid 2 stores wrap every object read out of them (query rows, their nested\n * arrays, `createStore` docs) in a Proxy. Those proxies cannot cross\n * `postMessage` (the sqlite and shared-tabs workers): structuredClone throws\n * DataCloneError. `snapshot()` returns the underlying plain value for store\n * proxies and passes anything else through untouched.\n */\nfunction unproxy<T>(value: T): T {\n if (value === null || typeof value !== 'object') return value;\n return snapshot(value as object) as T;\n}\nexport class SyncedDb<S extends SchemaStructure> {\n private config: SyncedDbConfig<S>;\n private sp00ky: Sp00kyClient<S> | null = null;\n private _initialized = false;\n\n constructor(config: SyncedDbConfig<S>) {\n this.config = config;\n }\n\n public getSp00ky(): Sp00kyClient<S> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky;\n }\n\n /**\n * Initialize the sp00ky-ts instance\n */\n async init(): Promise<void> {\n if (this._initialized) return;\n this.sp00ky = new Sp00kyClient<S>(this.config);\n await this.sp00ky.init();\n this._initialized = true;\n }\n\n /**\n * Tear down the client: leaves the tabs broker, closes the local store and\n * remote socket, and frees the wasm circuit. Without this a remounted provider\n * (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay\n * resident because V8 cannot see how much wasm memory a dropped wrapper holds.\n */\n async close(): Promise<void> {\n const instance = this.sp00ky;\n this.sp00ky = null;\n this._initialized = false;\n if (instance) await instance.close();\n }\n\n /**\n * Create a new record in the database\n */\n async create(id: string, payload: Record<string, unknown>): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.create(id, unproxy(payload) as Record<string, unknown>);\n }\n\n /**\n * Update an existing record in the database\n */\n async update<TName extends TableNames<S>>(\n tableName: TName,\n recordId: string,\n payload: Partial<TableModel<GetTable<S, TName>>>,\n options?: UpdateOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.update(\n tableName as string,\n recordId,\n unproxy(payload) as Record<string, unknown>,\n options\n );\n }\n\n /**\n * Delete an existing record in the database\n */\n async delete<TName extends TableNames<S>>(\n tableName: TName,\n selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n // Accept a `\"table:id\"` string OR a RecordId — live-query rows carry their\n // `id` as a RecordId, so callers can pass `db.delete('game', row.id)`\n // directly. Build the canonical string from the raw id part (not\n // `RecordId.toString()`, which escapes special chars) so it round-trips\n // through the engine's `parseRecordIdString`. InnerQuery selectors are not\n // supported yet. A RecordId read out of a Solid 2 query row is a store\n // proxy that hides its prototype (no instanceof, no constructor.name, on\n // rc.1), so unwrap it first; cross-package RecordId instances then match\n // by constructor name ('bound RecordId' covers rc.0 proxies).\n const raw = unproxy(selector);\n const ctorName = (raw as any)?.constructor?.name;\n const isRecordId =\n raw instanceof RecordId || ctorName === 'RecordId' || ctorName === 'bound RecordId';\n let id: string;\n if (typeof raw === 'string') {\n id = raw;\n } else if (isRecordId) {\n id = `${tableName as string}:${(raw as RecordId).id}`;\n } else {\n throw new Error('Only string ID or RecordId selectors are supported currently with core');\n }\n await this.sp00ky.delete(tableName as string, id);\n }\n\n /**\n * Preload/prewarm a built query into the local cache without registering a\n * live view. Fetches once and stores the rows (+ embedded related children)\n * locally so a later `createQuery` for the same data paints instantly. Best-effort.\n */\n public async preload(\n finalQuery: FinalQuery<S, any, any, any, any, Sp00kyQueryResultPromise>,\n options?: PreloadOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.preload(finalQuery, options);\n }\n\n /**\n * Query data from the database\n */\n public query<TName extends TableNames<S>>(\n table: TName\n ): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.query(table, {});\n }\n\n /**\n * Run a backend operation\n */\n public async run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(\n backend: B,\n path: R,\n payload: RoutePayload<S, B, R>,\n options?: RunOptions\n ): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.run(backend, path, unproxy(payload), options);\n }\n\n /**\n * Sign out, clear session and local storage\n */\n public async signOut(): Promise<void> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n await this.sp00ky.auth.signOut();\n }\n\n /**\n * Execute a function with direct access to the remote database connection\n */\n public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return await this.sp00ky.useRemote(fn);\n }\n /**\n * Access the remote database service directly\n */\n get remote(): Sp00kyClient<S>['remoteClient'] {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.remoteClient;\n }\n\n /**\n * Access the local database service directly\n */\n get local(): Sp00kyClient<S>['localClient'] {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.localClient;\n }\n\n /**\n * Access the auth service\n */\n get auth(): AuthService<S> {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.auth;\n }\n\n get pendingMutationCount(): number {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.pendingMutationCount;\n }\n\n /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */\n get liveRetryCount(): number {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.liveRetryCount;\n }\n\n subscribeToPendingMutations(cb: (count: number) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToPendingMutations(cb);\n }\n\n /** Current sync-health snapshot. See {@link useSyncStatus}. */\n get syncHealth(): SyncHealth {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.syncHealth;\n }\n\n /**\n * Observe sync health. Fires immediately with the current status and again\n * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in\n * components; this is the imperative escape hatch.\n */\n subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToSyncHealth(cb);\n }\n\n /** Current local-store durability snapshot. See {@link useStorageStatus}. */\n get storageHealth(): StorageHealth {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.storageHealth;\n }\n\n /**\n * Observe local-store durability. Fires immediately with the current snapshot\n * and again on change. Prefer the `useStorageStatus` hook in components; this\n * is the imperative escape hatch.\n */\n subscribeToStorageHealth(cb: (health: StorageHealth) => void): () => void {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.subscribeToStorageHealth(cb);\n }\n\n bucket<B extends BucketNames<S>>(name: B): BucketHandle {\n if (!this.sp00ky) throw new Error('SyncedDb not initialized');\n return this.sp00ky.bucket(name);\n }\n\n getBucketConfig(name: string): BucketDefinitionSchema | undefined {\n return this.config.schema.buckets?.find((b) => b.name === name);\n }\n}\n\nexport * from './types';\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -141,6 +141,25 @@ function usePendingMutations() {
141
141
  * inserted as-is and trailing rows are dropped, so add / remove / reorder all
142
142
  * reach coarse readers.
143
143
  */
144
+ /**
145
+ * Field-level equality for the merge. `===` is not enough: the decoder hands
146
+ * out a fresh RecordId / Date instance for every record-link and datetime
147
+ * column on every emission, so by identity every one of those fields "changed"
148
+ * on every update - and a store write on an unchanged `id` re-ran everything a
149
+ * page keyed on that row (a thread's anchor, its composer, its whole subtree).
150
+ * Compare those wrappers by value; everything else stays identity (nested
151
+ * objects and arrays are reconciled by their own writes).
152
+ */
153
+ function sameValue(a, b) {
154
+ if (a === b) return true;
155
+ if (a == null || b == null) return false;
156
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
157
+ if (typeof a === "object" && typeof b === "object") {
158
+ const ctor = a.constructor;
159
+ if (ctor && ctor === b.constructor && ctor !== Object && ctor !== Array) return String(a) === String(b);
160
+ }
161
+ return false;
162
+ }
144
163
  function mergeRows(draft, next, key = "id") {
145
164
  const byKey = /* @__PURE__ */ new Map();
146
165
  for (const row of draft) {
@@ -152,7 +171,7 @@ function mergeRows(draft, next, key = "id") {
152
171
  const k = incoming?.[key];
153
172
  const reuse = k !== void 0 ? byKey.get(String(k)) : void 0;
154
173
  if (reuse) {
155
- for (const field of Object.keys(incoming)) if (reuse[field] !== incoming[field]) reuse[field] = incoming[field];
174
+ for (const field of Object.keys(incoming)) if (!sameValue(reuse[field], incoming[field])) reuse[field] = incoming[field];
156
175
  for (const field of Object.keys(snapshot(reuse))) if (!(field in incoming)) delete reuse[field];
157
176
  if (draft[i] !== reuse) draft[i] = reuse;
158
177
  } else if (draft[i] !== incoming) draft[i] = incoming;