@helipod/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-DW55SNHW.js +286 -0
- package/dist/chunk-DW55SNHW.js.map +1 -0
- package/dist/chunk-I7ZJFW4E.js +2232 -0
- package/dist/chunk-I7ZJFW4E.js.map +1 -0
- package/dist/client-mJZjEkhK.d.ts +1144 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +642 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox-fs.d.ts +102 -0
- package/dist/outbox-fs.js +361 -0
- package/dist/outbox-fs.js.map +1 -0
- package/dist/outbox-storage-C6VHkXs9.d.ts +191 -0
- package/dist/react.d.ts +114 -0
- package/dist/react.js +139 -0
- package/dist/react.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/outbox-fs.ts"],"sourcesContent":["/**\n * `fsOutbox()` — the filesystem OutboxStorage for Node/Bun/Electron/Tauri hosts (spec:\n * docs/superpowers/specs/2025-11-04-fs-outbox-adapter-design.md). One append-only JSONL journal\n * per queue dir; every seam mutation is one appended line through a serialized write-behind\n * appender (same-microtask ops batch into ONE write+fsync — the fs twin of outbox-idb's\n * pendingOps/queueMicrotask batcher); hydrate replays the journal. A torn TAIL line (the only\n * thing an interrupted append can produce) is physically truncated — that entry was never\n * park-eligible, so dropping it is correct. A corrupt MIDDLE line is quarantined and skipped.\n * Compaction (at open and past 4096 ops) rewrites live state tmp → fsync → rename → dir fsync.\n * `node:*` imports live ONLY in this file — it ships as the `./outbox-fs` subpath export so the\n * browser bundles never see them.\n */\nimport { appendFileSync, unlinkSync } from \"node:fs\";\nimport { mkdir, open, readFile, rename, rm, truncate, unlink, writeFile, type FileHandle } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport {\n memoryOutbox,\n type HydrateResult,\n type OutboxEntry,\n type OutboxEntryError,\n type OutboxEntryStatus,\n type OutboxMeta,\n type OutboxStorage,\n} from \"./outbox-storage\";\nimport { dropStaleVersion } from \"./outbox-idb\";\n\nexport interface FsOutboxOptions {\n /** Queue directory (created recursively). One durable queue per dir. */\n dir: string;\n /** fsync appends before resolving append()'s promise. Default true. */\n fsync?: boolean;\n /** Fired once if the adapter degrades to memoryOutbox() (lock held by another process, or an\n * open/hydrate failure). */\n onFallback?: (reason: unknown) => void;\n}\n\n/** Thrown (rejecting the operation's promise) when a mutating call's flush runs — or would run —\n * after `close()` has completed. Before this existed, a flush that observed `closed` mid-batch\n * simply `return`ed, letting every op in that batch resolve normally despite never being written\n * — a silent, undetectable data-loss window. Now the batch's shared promise rejects instead, so a\n * caller racing `close()` learns its write was dropped rather than believing it durable. */\nexport class OutboxClosedError extends Error {\n readonly code = \"OUTBOX_CLOSED\";\n constructor(\n message = \"fsOutbox is closed; this operation's outcome is UNKNOWN — it may or may not have \" +\n \"been durably persisted (close()'s own final compaction can snapshot state an op mutated \" +\n \"before its flush rejected). Treat like any in-flight-at-shutdown write: verify on next hydrate.\",\n ) {\n super(message);\n this.name = \"OutboxClosedError\";\n }\n}\n\nconst COMPACT_THRESHOLD = 4096;\n\ntype JournalOp =\n | { op: \"append\"; entry: OutboxEntry }\n | { op: \"status\"; clientId: string; seq: number; status: OutboxEntryStatus; error?: OutboxEntryError }\n | { op: \"dequeue\"; clientId: string; seq: number }\n | { op: \"meta\"; clientId: string; meta: OutboxMeta }\n | { op: \"metaDelete\"; clientId: string };\n\nconst key = (clientId: string, seq: number) => `${clientId}\\0${seq}`;\n\ninterface State {\n entries: Map<string, OutboxEntry>;\n meta: Map<string, OutboxMeta>;\n}\n\nfunction applyOp(state: State, op: JournalOp): void {\n switch (op.op) {\n case \"append\":\n state.entries.set(key(op.entry.clientId, op.entry.seq), op.entry);\n break;\n case \"status\": {\n const existing = state.entries.get(key(op.clientId, op.seq));\n if (existing)\n state.entries.set(key(op.clientId, op.seq), {\n ...existing,\n status: op.status,\n ...(op.error !== undefined ? { error: op.error } : {}),\n });\n break;\n }\n case \"dequeue\":\n state.entries.delete(key(op.clientId, op.seq));\n break;\n case \"meta\":\n state.meta.set(op.clientId, op.meta);\n break;\n case \"metaDelete\":\n state.meta.delete(op.clientId);\n break;\n }\n}\n\n/** The direct (lock-free) open — `fsOutbox()` below wraps this with lock + probe-and-fallback. */\nexport class FsOutboxStorage implements OutboxStorage {\n /** Accepts an injectable stats object (the `fsOutbox()` lock wrapper passes its own so the object\n * it returns from `fsOutbox()` stays live even across a probe-and-fallback swap) — defaults to a\n * fresh one for direct construction (tests, direct use of this class). */\n readonly stats: { flushes: number; fsyncs: number };\n\n private state: State = { entries: new Map(), meta: new Map() };\n private opCount = 0;\n private handle: FileHandle | undefined;\n private pending: string[] = [];\n private flushScheduled = false;\n /** Serializes flushes, compactions, and close() — every disk mutation chains on this, INCLUDING\n * close() itself (see close() below), so nothing can run concurrently with or after a completed\n * close(). Fail-stop by construction: once any chained link throws (disk-full, a corrupt-open\n * failure, `OutboxClosedError`), `this.tail` becomes a permanently-rejected promise and every\n * later `.then(...)` chained onto it (the normal way every method here extends the chain) short-\n * circuits straight to that SAME rejection without ever running its own body — callers see the\n * original failure's error, not a fresh one. This is intentional, not a bug to route around: a\n * fsOutbox instance whose disk state may be compromised should not keep pretending to work.\n * Recovery (probe-and-fallback to memoryOutbox, lock steal) is `fsOutbox()`'s seam, not this class's. */\n private tail: Promise<void> = Promise.resolve();\n private opened = false;\n private closed = false;\n\n constructor(\n private readonly dir: string,\n private readonly fsync: boolean,\n stats: { flushes: number; fsyncs: number } = { flushes: 0, fsyncs: 0 },\n ) {\n this.stats = stats;\n }\n\n private get journalPath() {\n return join(this.dir, \"journal.jsonl\");\n }\n\n /** Replay the journal into memory; truncate a torn tail; quarantine corrupt middles; compact. */\n private async openOnce(): Promise<void> {\n if (this.opened) return;\n this.opened = true;\n await mkdir(this.dir, { recursive: true });\n let raw = \"\";\n try {\n raw = await readFile(this.journalPath, \"utf8\");\n } catch {\n /* fresh dir — no journal yet */\n }\n let validBytes = 0;\n // A flush always writes `line + \"\\n\"` as ONE buffer and fsyncs only after that write\n // completes — so a journal missing its trailing newline PROVES the very last line was never\n // acknowledged (append()'s promise for it never resolved), regardless of whether the bytes\n // that did land happen to parse as valid JSON. Accepting a parseable-but-unterminated last\n // line would glue whatever a LATER session appends onto its tail, corrupting an entry that\n // WAS legitimately acknowledged (see the finding this fixes: torn-newline glue).\n const hasTrailingNewline = raw.endsWith(\"\\n\");\n const lines = raw.length === 0 ? [] : raw.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n if (line === \"\") {\n // the split artifact after a final newline, or a blank line mid-file (skip)\n if (i < lines.length - 1) validBytes += 1;\n continue;\n }\n const isLast = i === lines.length - 1;\n if (isLast && !hasTrailingNewline) {\n // Torn tail, unconditionally: never attempt to parse it — its missing newline alone\n // proves it was never durably acknowledged. Truncate back to the last known-good byte.\n await truncate(this.journalPath, validBytes);\n continue;\n }\n try {\n const op = JSON.parse(line) as JournalOp;\n applyOp(this.state, op);\n this.opCount++;\n validBytes += Buffer.byteLength(line, \"utf8\") + 1;\n } catch {\n // A parse-failing line here always HAS its trailing newline (an unterminated final line is\n // intercepted pre-parse above; a file ending \"\\n\" makes split's last element \"\"), so its\n // write completed and the corruption is content-level: quarantine and continue.\n appendFileSync(join(this.dir, \"journal.quarantine\"), line + \"\\n\");\n validBytes += Buffer.byteLength(line, \"utf8\") + 1;\n }\n }\n this.handle = await open(this.journalPath, \"a\");\n // Open-time compaction bounds a journal that grew across prior sessions.\n if (this.opCount > this.state.entries.size + this.state.meta.size) await this.compact();\n }\n\n private ready(): Promise<void> {\n this.tail = this.tail.then(() => this.openOnce());\n return this.tail;\n }\n\n /** Eagerly hydrate the journal. `fsOutbox()`'s probe awaits this BEFORE treating the\n * instance as live, so an open/hydrate failure (corrupt journal, failed truncate, disk error)\n * surfaces to the probe — which can then release the lock and degrade to `memoryOutbox()` — in\n * place of the old lazy-on-first-method-call shape, where the failure only surfaced after the\n * probe had already resolved to this fs store, fail-stopping the instance instead of falling\n * back per the spec's error-handling table. */\n async open(): Promise<void> {\n await this.ready();\n }\n\n private write(op: JournalOp): Promise<void> {\n this.pending.push(JSON.stringify(op) + \"\\n\");\n this.opCount++;\n if (!this.flushScheduled) {\n this.flushScheduled = true;\n this.tail = this.tail.then(async () => {\n await this.openOnce();\n this.flushScheduled = false;\n const batch = this.pending.splice(0);\n if (batch.length === 0) return;\n // close() chains its ENTIRE body (drain -> compact-if-excess -> closed=true ->\n // handle.close) as a single link on `this.tail` too, so `this.closed` can only ever\n // become true strictly BEFORE or AFTER this whole flush runs — never mid-flush. Seeing it\n // true here means this batch lost the race entirely (queued after close() had already\n // completed its own tail link): reject rather than silently `return`ing a \"success\" for\n // ops that were never written — see OutboxClosedError's doc comment.\n if (this.closed) throw new OutboxClosedError();\n await this.handle!.write(batch.join(\"\"));\n this.stats.flushes++;\n if (this.fsync) {\n await this.handle!.sync();\n this.stats.fsyncs++;\n }\n if (this.opCount > COMPACT_THRESHOLD) await this.compact();\n });\n }\n return this.tail;\n }\n\n /** Rewrite live state tmp → fsync → rename → dir fsync. Failure leaves the old journal intact. */\n private async compact(): Promise<void> {\n const tmpPath = join(this.dir, \"journal.tmp\");\n try {\n const lines: string[] = [];\n for (const [clientId, meta] of this.state.meta) lines.push(JSON.stringify({ op: \"meta\", clientId, meta } satisfies JournalOp) + \"\\n\");\n for (const entry of [...this.state.entries.values()].sort((a, b) => a.order - b.order))\n lines.push(JSON.stringify({ op: \"append\", entry } satisfies JournalOp) + \"\\n\");\n const tmp = await open(tmpPath, \"w\");\n await tmp.write(lines.join(\"\"));\n await tmp.sync();\n await tmp.close();\n await this.handle?.close();\n await rename(tmpPath, this.journalPath);\n const dirHandle = await open(this.dir, \"r\").catch(() => undefined);\n if (dirHandle) {\n await dirHandle.sync().catch(() => {});\n await dirHandle.close();\n }\n this.handle = await open(this.journalPath, \"a\");\n this.opCount = this.state.entries.size + this.state.meta.size;\n } catch {\n await rm(tmpPath, { force: true }).catch(() => {});\n // Unconditionally restore a WORKING append handle, regardless of `this.handle`'s current\n // truthiness: a failure partway through the steps above (write/sync/rename to the tmp file\n // failing AFTER `this.handle?.close()` already ran) leaves `this.handle` closed-but-set —\n // the old `if (!this.handle)` guard would then skip reopening entirely, and every later\n // write would throw against a dead handle forever. Close-if-present (best-effort; it may\n // already be closed) then always reopen fresh.\n await this.handle?.close().catch(() => {});\n this.handle = await open(this.journalPath, \"a\");\n }\n }\n\n async append(entry: OutboxEntry): Promise<void> {\n // Hydrate first if this is the very first call on this instance: applying `entry` to\n // `this.state` BEFORE the journal replay would run means the replay (which mutates the SAME\n // `this.state` object) could land AFTER this fresher write, letting a stale on-disk value win\n // over the one just set — a state inversion. `updateStatus`/`dequeue`/`deleteMeta` already\n // guard the same way.\n if (!this.opened) await this.ready();\n applyOp(this.state, { op: \"append\", entry: structuredClone(entry) });\n await this.write({ op: \"append\", entry });\n }\n\n async updateStatus(clientId: string, seq: number, status: OutboxEntryStatus, error?: OutboxEntryError): Promise<void> {\n if (!this.opened) await this.ready();\n if (!this.state.entries.has(key(clientId, seq))) return; // silent no-op, contract behavior\n applyOp(this.state, { op: \"status\", clientId, seq, status, error });\n await this.write({ op: \"status\", clientId, seq, status, ...(error !== undefined ? { error } : {}) });\n }\n\n async dequeue(clientId: string, seq: number): Promise<void> {\n if (!this.opened) await this.ready();\n if (!this.state.entries.has(key(clientId, seq))) return;\n applyOp(this.state, { op: \"dequeue\", clientId, seq });\n await this.write({ op: \"dequeue\", clientId, seq });\n }\n\n async loadAll(): Promise<HydrateResult> {\n await this.ready();\n const all = [...this.state.entries.values()].sort((a, b) => a.order - b.order);\n const { entries, dropped } = dropStaleVersion(all);\n for (const e of dropped) {\n applyOp(this.state, { op: \"dequeue\", clientId: e.clientId, seq: e.seq });\n // Loss-safe, so the rejection is deliberately swallowed rather than surfaced: `e` is\n // ALREADY removed from `this.state` and already returned to the caller inside `dropped` —\n // the caller has already treated it as gone either way. If this particular disk write fails\n // (e.g. transient disk-full), the stale-version row simply survives on disk to be dropped\n // again, idempotently, on the NEXT hydrate. Leaving this unhandled would surface as an\n // `unhandledRejection`, which several Electron/Node hosts treat as fatal — worse than a\n // silently-retried cleanup.\n void this.write({ op: \"dequeue\", clientId: e.clientId, seq: e.seq }).catch(() => {});\n }\n return { entries: entries.map((e) => structuredClone(e)), dropped };\n }\n\n async getMeta(clientId: string): Promise<OutboxMeta | undefined> {\n await this.ready();\n const m = this.state.meta.get(clientId);\n return m ? structuredClone(m) : undefined;\n }\n\n async setMeta(clientId: string, meta: OutboxMeta): Promise<void> {\n // Same first-open-ordering hazard as append() — see its comment.\n if (!this.opened) await this.ready();\n applyOp(this.state, { op: \"meta\", clientId, meta: structuredClone(meta) });\n await this.write({ op: \"meta\", clientId, meta });\n }\n\n async listMetaClientIds(): Promise<string[]> {\n await this.ready();\n return [...this.state.meta.keys()];\n }\n\n async deleteMeta(clientId: string): Promise<void> {\n if (!this.opened) await this.ready();\n applyOp(this.state, { op: \"metaDelete\", clientId });\n await this.write({ op: \"metaDelete\", clientId });\n }\n\n persist(): void {\n // No-op: no eviction advisory exists on a filesystem.\n }\n\n /** NOTE for `finally`-block callers: under the fail-stop tail, `close()` PROPAGATES a prior\n * tail rejection (a racing op's flush error) rather than masking it — `await storage.close?.()`\n * in cleanup paths should tolerate a rejecting close (`.catch` it if the original error is\n * already being handled elsewhere). */\n async close(): Promise<void> {\n // The ENTIRE body runs as ONE link chained onto `this.tail` — not \"await ready(), then do\n // more stuff off-tail\" as before. That old shape let `this.closed = true` and the final\n // `compact()`/`handle.close()` run OUTSIDE the serialization every other disk mutation\n // respects: a flush or compact racing concurrently with close() could interleave file-handle\n // use with it (both touching `journal.tmp`/the append handle), and `closed` could flip true\n // mid-flush, letting that flush's `return` resolve its callers' promises without ever writing\n // their batch (see OutboxClosedError's doc comment for the fix on that half). Chaining the\n // whole thing as one tail link makes \"final drain, then compact-if-excess, then mark closed,\n // then close the handle\" atomic relative to every other operation in this class: anything\n // already chained onto `this.tail` before this call runs first (the drain); anything chained\n // after (including a `write()` racing close()) runs only once this ENTIRE body — including\n // `this.closed = true` — has completed, and correctly observes `closed`.\n this.tail = this.tail.then(async () => {\n await this.openOnce(); // a close() before any op ever ran still needs a real handle to close\n // Final compaction: a threshold-triggered mid-stream compact() resets opCount to the live\n // state size, so a trailing run of ops shorter than COMPACT_THRESHOLD (e.g. ending the\n // session right after crossing the threshold once) never crosses it again and would\n // otherwise sit uncompacted on disk. Mirror openOnce's same excess check so a closed queue\n // is always left compacted at rest.\n if (this.opCount > this.state.entries.size + this.state.meta.size) await this.compact();\n this.closed = true;\n await this.handle?.close();\n this.handle = undefined;\n });\n return this.tail;\n }\n}\n\n/** Dirs opened (and locked) by THIS process — makes a same-process double-open deterministic and\n * distinguishes a genuinely-live own-pid lock from one left by a previous boot that recycled our\n * pid. Keyed by resolved dir path. */\nconst openDirs = new Set<string>();\n/** Lockfile paths for the best-effort exit hook (SIGKILL still leaks; the next open steals). */\nconst liveLocks = new Set<string>();\nlet exitHookRegistered = false;\n\nexport class OutboxLockHeldError extends Error {\n readonly code = \"OUTBOX_LOCK_HELD\";\n constructor(holder: number) {\n super(`outbox dir is locked by live pid ${holder} — one writer per dir; falling back to memory`);\n this.name = \"OutboxLockHeldError\";\n }\n}\n\n/** Residual risk (standard for this unlink-based pidfile lock family, not a bug to fix here): a\n * dead-pid classification and the `rm` that steals it are two separate steps — a THIRD process\n * could win a fresh `wx` lock in the gap between them, and this attempt's `rm` would then delete\n * that third process's live lockfile out from under it (TOCTOU). The receipts (exact-match\n * `(identity, clientId, seq)` dedup atomic with commit) remain the actual safety net regardless;\n * this lock is a best-effort single-writer optimization, not the correctness boundary. */\nasync function acquireLock(dir: string): Promise<string> {\n const lockPath = join(dir, \"lock\");\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n await writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), { flag: \"wx\" });\n liveLocks.add(lockPath);\n if (!exitHookRegistered) {\n exitHookRegistered = true;\n process.once(\"exit\", () => {\n for (const p of liveLocks) {\n try {\n unlinkSync(p);\n } catch {\n /* already gone */\n }\n }\n });\n }\n return lockPath;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n let stale = false;\n try {\n const { pid } = JSON.parse(await readFile(lockPath, \"utf8\")) as { pid?: unknown };\n if (typeof pid !== \"number\") stale = true;\n // Recycled pid from a prior boot vs. a genuinely-live lock THIS process already holds:\n // keyed on `liveLocks` (only ever populated by this function's own happy-path write below,\n // i.e. a lock we actually acquired), not `openDirs` — `openDirs` is registered synchronously\n // at the top of fsOutbox()'s probe (before this function is even called), so by the time an\n // own-pid EEXIST lands here `openDirs` already contains `dir` for THIS very attempt and\n // would misclassify every fresh own-pid lockfile as \"live\" instead of \"recycled\".\n else if (pid === process.pid) stale = !liveLocks.has(lockPath);\n else {\n try {\n process.kill(pid, 0); // signal 0: existence probe only\n } catch (killErr) {\n if ((killErr as NodeJS.ErrnoException).code === \"ESRCH\") stale = true;\n else throw new OutboxLockHeldError(pid); // EPERM: alive but not ours\n }\n if (!stale) throw new OutboxLockHeldError(pid);\n }\n } catch (parseOrHeld) {\n if (parseOrHeld instanceof OutboxLockHeldError) throw parseOrHeld;\n stale = true; // unreadable/garbage\n }\n if (!stale) throw new OutboxLockHeldError(-1);\n await rm(lockPath, { force: true });\n // loop: one retry of wx\n }\n }\n throw new OutboxLockHeldError(-1);\n}\n\n/** The final shape: an async probe (lock the dir, hydrate the journal) wrapped in the same\n * resolved/ready delegation `indexedDBOutbox()` uses (`outbox-storage.ts`) — every call made\n * before the probe settles queues behind it; every call after routes directly. A same-process\n * double-open on the same dir, a live foreign-pid lock, or any `openOnce()` failure (a truncate\n * error, disk-full, etc.) all degrade to a fresh `memoryOutbox()` rather than throwing — a durable\n * outbox that can't get its lock still has to behave like *an* outbox (verdict-consistent\n * probe-and-fallback), just without cross-reload durability. `onFallback` fires exactly once for\n * whichever reason won. */\nexport function fsOutbox(opts: FsOutboxOptions): OutboxStorage & { stats: { flushes: number; fsyncs: number } } {\n const dir = resolve(opts.dir);\n const stats = { flushes: 0, fsyncs: 0 };\n\n let resolved: OutboxStorage | undefined;\n const ready: Promise<OutboxStorage> = (async (): Promise<OutboxStorage> => {\n if (openDirs.has(dir)) throw new OutboxLockHeldError(process.pid); // same-process double-open\n // Register synchronously, before the first await: two same-process fsOutbox() calls in the\n // same synchronous frame would otherwise both pass the `has()` check above (neither has\n // registered yet), racing each other for the lock instead of one deterministically falling\n // back via the registry hit. Every failure path below deregisters on its way out.\n openDirs.add(dir);\n let lockPath: string | undefined;\n try {\n await mkdir(dir, { recursive: true });\n lockPath = await acquireLock(dir);\n const store = new FsOutboxStorage(dir, opts.fsync !== false, stats);\n // Eager hydrate: openOnce() runs lazily-but-awaited here rather than being left to the first\n // method call AFTER this probe has already resolved to `store` — that shape would fail-stop\n // the instance on an open/hydrate error instead of degrading to memoryOutbox() as the spec's\n // error table mandates. Hydrating here, inside this try, routes that failure through the same\n // catch below as a lock failure: lock released, dir deregistered, then onFallback + memory\n // fallback.\n await store.open();\n const innerClose = store.close.bind(store);\n store.close = async () => {\n try {\n await innerClose();\n } finally {\n // Fail-stop: release the lock and deregister the dir even if the inner (tail-chained)\n // close rejected — a poisoned journal tail must not wedge this dir locked forever. The\n // original rejection, if any, still propagates to the caller after this finally runs.\n openDirs.delete(dir);\n liveLocks.delete(lockPath!);\n await unlink(lockPath!).catch(() => {});\n }\n };\n return store;\n } catch (err) {\n // Any failure after the dir was registered (mkdir, lock acquisition, or the eager hydrate\n // above) must not leave the dir wedged: deregister it always, and release the lock too if\n // this attempt actually acquired one (a legitimately-held foreign lock is left untouched —\n // acquireLock() never assigns `lockPath` when it throws OutboxLockHeldError).\n openDirs.delete(dir);\n if (lockPath) {\n liveLocks.delete(lockPath);\n await unlink(lockPath).catch(() => {});\n }\n throw err;\n }\n })().catch((err: unknown) => {\n opts.onFallback?.(err);\n return memoryOutbox();\n });\n void ready.then((impl) => {\n resolved = impl;\n });\n const impl = async (): Promise<OutboxStorage> => resolved ?? ready;\n\n return {\n stats,\n append: async (entry) => (await impl()).append(entry),\n updateStatus: async (clientId, seq, status, error) => (await impl()).updateStatus(clientId, seq, status, error),\n dequeue: async (clientId, seq) => (await impl()).dequeue(clientId, seq),\n loadAll: async () => (await impl()).loadAll(),\n getMeta: async (clientId) => (await impl()).getMeta(clientId),\n setMeta: async (clientId, meta) => (await impl()).setMeta(clientId, meta),\n listMetaClientIds: async () => (await impl()).listMetaClientIds?.() ?? [],\n deleteMeta: async (clientId) => (await impl()).deleteMeta?.(clientId),\n persist: () => {\n void impl().then((i) => i.persist());\n },\n close: async () => (await impl()).close?.(),\n };\n}\n"],"mappings":"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","names":["impl"]}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { JSONValue } from '@helipod/values';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `OutboxStorage` seam — the client's first storage API (verdict §(d), `docs/dev/research/
|
|
5
|
+
* offline-outbox/verdict.md`). Two implementations: `memoryOutbox()` (the default — preserves
|
|
6
|
+
* today's behavior byte-for-byte; nothing persists across a reload) and `indexedDBOutbox()`
|
|
7
|
+
* (probe-and-fallback: real durability in a browser, transparently degrades to `memoryOutbox()`
|
|
8
|
+
* wherever IndexedDB is unavailable or fails to open — Node, private-mode Safari, a corrupt
|
|
9
|
+
* origin). Durability is opt-in constructor config (`new HelipodClient(transport, { outbox:
|
|
10
|
+
* indexedDBOutbox() })`); a client constructed without `outbox` never touches this file's runtime
|
|
11
|
+
* branches that matter — `memoryOutbox()`'s Maps are just as ephemeral as the pre-outbox client.
|
|
12
|
+
*
|
|
13
|
+
* Persisted record shape is verdict §(d) verbatim: `{clientId, seq, requestId, udfPath, args, seed,
|
|
14
|
+
* order, status, identityFingerprint, outboxVersion, enqueuedAt}`. `order` is an explicit column —
|
|
15
|
+
* Map/array insertion order does not survive IndexedDB, and the drain (a later task) needs a
|
|
16
|
+
* persisted total order across the WHOLE shared queue (every clientId sharing this database), not
|
|
17
|
+
* just within one clientId's entries.
|
|
18
|
+
*
|
|
19
|
+
* Identity (verdict §(d) "Identity", hazard 8): **one clientId per tab-session, minted at client
|
|
20
|
+
* construction, never reused across a reload.** A fresh `HelipodClient` instance always mints a
|
|
21
|
+
* BRAND NEW clientId — there is no reseed protocol, so there is nothing to get wrong (hazard 8:
|
|
22
|
+
* "reload resets counters — dissolved structurally"). Entries persisted under an OLDER clientId
|
|
23
|
+
* from a previous session are untouched by minting a new one; they hydrate and drain under their
|
|
24
|
+
* own **recorded** `(clientId, seq)` pair later (a later task's drain). The "-or-loaded" half of
|
|
25
|
+
* `mintIdentity` concerns `nextSeq`, not `clientId`: a fresh clientId's meta row does not exist, so
|
|
26
|
+
* `nextSeq` starts at 0 — but the loader still calls `getMeta` first rather than assuming absence,
|
|
27
|
+
* so a colliding clientId (astronomically unlikely — see `defaultMintClientId`) resumes from its
|
|
28
|
+
* recorded `nextSeq` instead of silently reusing a seq that already named a payload (verdict §(b)'s
|
|
29
|
+
* governing invariant: the map `(clientId, seq) -> payload` is written exactly once).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
type OutboxEntryStatus = "unsent" | "inflight" | "parked" | "completed" | "failed";
|
|
33
|
+
/** T5 (R9): the terminal verdict recorded alongside a `"failed"` durable entry — surfaced through
|
|
34
|
+
* `client.pendingMutations()`/`usePendingMutations()` and `onMutationFailed`. */
|
|
35
|
+
interface OutboxEntryError {
|
|
36
|
+
message: string;
|
|
37
|
+
code?: string;
|
|
38
|
+
}
|
|
39
|
+
/** One durable outbox record — the persisted twin of `PendingMutation` (`./mutation-log`), plus
|
|
40
|
+
* the fields only a durable queue needs (`clientId`, `seq`, `order`, `identityFingerprint`,
|
|
41
|
+
* `outboxVersion`, `enqueuedAt`). `args`/`seed` are the fields whose omission would make a
|
|
42
|
+
* hydrated replay non-deterministic (verdict D2) — everything else (`touched`, the `update`
|
|
43
|
+
* closure) is recomputed or looked up in the optimistic-updater registry, never persisted. */
|
|
44
|
+
interface OutboxEntry {
|
|
45
|
+
clientId: string;
|
|
46
|
+
seq: number;
|
|
47
|
+
requestId: string;
|
|
48
|
+
udfPath: string;
|
|
49
|
+
args: JSONValue;
|
|
50
|
+
seed: {
|
|
51
|
+
entropy: string;
|
|
52
|
+
now: number;
|
|
53
|
+
};
|
|
54
|
+
/** Global position across the WHOLE shared queue (every clientId) — the drain's FIFO key. */
|
|
55
|
+
order: number;
|
|
56
|
+
status: OutboxEntryStatus;
|
|
57
|
+
/** SHA-256 of the `SetAuth` token at enqueue time; absent for an unauthenticated mutation. Set
|
|
58
|
+
* at flush-time by a later task; carried as a plain field here so the schema is stable from
|
|
59
|
+
* day one (verdict §(g) hazard 9). */
|
|
60
|
+
identityFingerprint?: string;
|
|
61
|
+
outboxVersion: number;
|
|
62
|
+
enqueuedAt: number;
|
|
63
|
+
/** T5 (R9): set (via `updateStatus`'s optional 4th argument) when `status` transitions to
|
|
64
|
+
* `"failed"` — a terminal, server-recorded verdict a live `mutation()` promise may have no
|
|
65
|
+
* awaiter for (a hydrated cross-reload entry, or a retried one). Absent for every other status. */
|
|
66
|
+
error?: OutboxEntryError;
|
|
67
|
+
}
|
|
68
|
+
interface OutboxMeta {
|
|
69
|
+
/** In-memory-serial cursor for this clientId's NEXT mutation, loaded once at mint time. */
|
|
70
|
+
nextSeq: number;
|
|
71
|
+
deployment?: string;
|
|
72
|
+
}
|
|
73
|
+
/** The result of a full-queue hydrate. `dropped` holds entries whose `outboxVersion` didn't match
|
|
74
|
+
* the running code's `OUTBOX_VERSION` — removed from storage as a side effect of the hydrate
|
|
75
|
+
* itself, returned so the caller can settle them with a terminal verdict rather than silently
|
|
76
|
+
* discarding a promise's fate (verdict §(g) hazard 10: "outboxVersion stamp, drop-with-verdict at
|
|
77
|
+
* hydrate"). */
|
|
78
|
+
interface HydrateResult {
|
|
79
|
+
/** Current-version entries, in persisted `order`. */
|
|
80
|
+
entries: OutboxEntry[];
|
|
81
|
+
/** Stale-version entries deleted during this hydrate. */
|
|
82
|
+
dropped: OutboxEntry[];
|
|
83
|
+
}
|
|
84
|
+
/** The seam. Every method is safe to call concurrently — the IndexedDB implementation
|
|
85
|
+
* write-behind-batches same-microtask calls into one transaction (see `outbox-idb.ts`). */
|
|
86
|
+
interface OutboxStorage {
|
|
87
|
+
/** Durably record a new entry. The caller must NOT await this before sending the mutation on the
|
|
88
|
+
* wire — "the send never waits for the append" (verdict §(d)); this promise exists so a caller
|
|
89
|
+
* CAN confirm durability later (e.g. park-eligibility at transport close). */
|
|
90
|
+
append(entry: OutboxEntry): Promise<void>;
|
|
91
|
+
/** Mutate only `status` (and, when given, `error` — T5's R9 terminal-failure record; every other
|
|
92
|
+
* field is preserved verbatim). Called with `status: "failed"` INSTEAD OF `dequeue()` on a
|
|
93
|
+
* terminal failure (verdict §(d) R9: "failed entries persist until dismissed/retried") — every
|
|
94
|
+
* other status transition (`"inflight"`/`"unsent"`/`"parked"`) omits `error`. */
|
|
95
|
+
updateStatus(clientId: string, seq: number, status: OutboxEntryStatus, error?: OutboxEntryError): Promise<void>;
|
|
96
|
+
/** Remove a fully-settled entry. */
|
|
97
|
+
dequeue(clientId: string, seq: number): Promise<void>;
|
|
98
|
+
/** Hydrate the whole shared queue, across every clientId, in persisted `order`. */
|
|
99
|
+
loadAll(): Promise<HydrateResult>;
|
|
100
|
+
getMeta(clientId: string): Promise<OutboxMeta | undefined>;
|
|
101
|
+
setMeta(clientId: string, meta: OutboxMeta): Promise<void>;
|
|
102
|
+
/** OPTIONAL (verdict §(g) hazard 1 / Task 4's dead-meta prune): every clientId with a meta row.
|
|
103
|
+
* The drain uses it at hydrate to reclaim rows for clientIds that are neither the current session
|
|
104
|
+
* nor have any live queue entries — one such tiny row otherwise accrues per prior tab-session and
|
|
105
|
+
* every `onClientReset`. Optional so a minimal `OutboxStorage` double (which never prunes) stays
|
|
106
|
+
* valid; both shipped backends implement it. */
|
|
107
|
+
listMetaClientIds?(): Promise<string[]>;
|
|
108
|
+
/** OPTIONAL companion to {@link listMetaClientIds} — delete one dead meta row. */
|
|
109
|
+
deleteMeta?(clientId: string): Promise<void>;
|
|
110
|
+
/** Advisory `navigator.storage.persist()` request — fire-and-forget, no return value, and no
|
|
111
|
+
* behavior anywhere ever branches on whether the grant is honored (verdict §(g) hazard 3: "zero
|
|
112
|
+
* behavior branches on the grant"). A no-op for the memory backend. */
|
|
113
|
+
persist(): void;
|
|
114
|
+
/** OPTIONAL: flush pending writes and release any backing resources (fs lock, IDB handle).
|
|
115
|
+
* Additive like `listMetaClientIds` — callers guard with `?.`. Nothing in the client core
|
|
116
|
+
* calls it; it exists for host shutdown/relaunch flows (Electron window cycling) and tests.
|
|
117
|
+
* A client that never calls it loses nothing (backends self-heal: exit hooks, stale-lock
|
|
118
|
+
* steal, IDB's own connection lifecycle). */
|
|
119
|
+
close?(): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
/** Default cap on how many outbox-tracked entries (`unsent`/`inflight`/`parked` — not yet fully
|
|
122
|
+
* settled) may sit in `client.ts`'s live log at once, per verdict §(d) "Enqueue": "bounded
|
|
123
|
+
* (default 1000)". Overridable via `new HelipodClient(transport, { outboxMaxQueueSize })`. */
|
|
124
|
+
declare const DEFAULT_OUTBOX_MAX_QUEUE_SIZE = 1000;
|
|
125
|
+
/** Thrown (as a rejected `mutation()` promise, never a synchronous throw) when the durable outbox
|
|
126
|
+
* is at capacity. Verdict §(d): the NEW enqueue is rejected, not the oldest queued one — "the new
|
|
127
|
+
* write has a live awaiter [this very call]; the oldest durable promise may not [e.g. it survived
|
|
128
|
+
* a reload, where there is no live JS promise for it at all until the registry rebuilds a layer]."
|
|
129
|
+
* A coded error (`.code`) so callers can distinguish this from every other mutation failure. */
|
|
130
|
+
declare class OutboxOverflowError extends Error {
|
|
131
|
+
readonly code = "OUTBOX_OVERFLOW";
|
|
132
|
+
constructor(message?: string);
|
|
133
|
+
}
|
|
134
|
+
/** Rejection for a parked mutation the server disowned on `ConnectAck{known: false}` — the client's
|
|
135
|
+
* presented history matched neither a record nor a floor (a swept/foreign/reset timeline), so the
|
|
136
|
+
* client re-mints its identity (`onClientReset`). A parked entry was in-flight when the socket
|
|
137
|
+
* dropped: its outcome is genuinely unknowable and, since the server has no dedup record for it,
|
|
138
|
+
* a blind resend under a fresh clientId could double-apply — so it rejects LOUDLY (verdict §(d)
|
|
139
|
+
* Retention: "parked entries reject loudly"). Coded so apps can distinguish it from every other
|
|
140
|
+
* failure. `unsent` entries (never hit the wire) are safe to re-enqueue instead and are NOT
|
|
141
|
+
* rejected. */
|
|
142
|
+
declare class OfflineClientResetError extends Error {
|
|
143
|
+
readonly code = "OFFLINE_CLIENT_RESET";
|
|
144
|
+
constructor(message?: string);
|
|
145
|
+
}
|
|
146
|
+
/** The in-memory default — what a client gets when it passes no `outbox` at all. Nothing here
|
|
147
|
+
* survives past this `HelipodClient` instance's lifetime, which is exactly today's (pre-outbox)
|
|
148
|
+
* behavior: a reload has no durable queue to hydrate, because there never was one. */
|
|
149
|
+
declare function memoryOutbox(): OutboxStorage;
|
|
150
|
+
interface IndexedDBOutboxOptions {
|
|
151
|
+
/** Injectable for tests / non-default globals — defaults to `globalThis.indexedDB`. */
|
|
152
|
+
indexedDB?: IDBFactory;
|
|
153
|
+
dbName?: string;
|
|
154
|
+
/** Best-effort, fire-and-forget notification whenever the adapter falls back to memory — e.g.
|
|
155
|
+
* no IndexedDB in this runtime, or `open()` failed (private-mode Safari, a corrupt origin). The
|
|
156
|
+
* fallback itself is never gated on this callback existing or succeeding. */
|
|
157
|
+
onFallback?: (reason: unknown) => void;
|
|
158
|
+
}
|
|
159
|
+
/** Probe-and-fallback (verdict §(g) hazard 5): if IndexedDB isn't available in this runtime at
|
|
160
|
+
* all, or `open()` fails for any reason, every method transparently delegates to a fresh
|
|
161
|
+
* `memoryOutbox()` instead — same interface, same call sites, only durability is lost. The probe
|
|
162
|
+
* is asynchronous (an IDB `open()` can only fail asynchronously), so calls made before the
|
|
163
|
+
* outcome is known queue behind the open attempt; every call after routes directly with no added
|
|
164
|
+
* latency. */
|
|
165
|
+
declare function indexedDBOutbox(opts?: IndexedDBOutboxOptions): OutboxStorage;
|
|
166
|
+
/** Not `crypto.randomUUID()` unconditionally — some test/SSR runtimes lack it. Collision odds are
|
|
167
|
+
* irrelevant either way: `mintIdentity` re-reads `getMeta` for whatever id comes out, so even a
|
|
168
|
+
* freak collision resumes from a recorded `nextSeq` rather than silently reusing a seq.
|
|
169
|
+
*
|
|
170
|
+
* Exported (beyond `mintIdentity`'s internal default) so `client.ts` can mint a clientId
|
|
171
|
+
* SYNCHRONOUSLY at construction — `mutation()` must stay fully synchronous (T1's open concern),
|
|
172
|
+
* so the clientId every entry stamps cannot wait on `mintIdentity`'s async `getMeta`/`setMeta`
|
|
173
|
+
* round-trip. `client.ts` mints with this directly, then feeds the SAME id into `mintIdentity` via
|
|
174
|
+
* `opts.mintClientId` so the durable meta row it persists names the id actually in use. */
|
|
175
|
+
declare function defaultMintClientId(): string;
|
|
176
|
+
/**
|
|
177
|
+
* Mint this tab-session's clientId and establish its `nextSeq` cursor in the outbox's meta store.
|
|
178
|
+
* Per the file doc's identity model: ALWAYS mints a fresh clientId — never reuses one from a prior
|
|
179
|
+
* session. Returns `{clientId, nextSeq}`; the caller (client construction) keeps `nextSeq` as an
|
|
180
|
+
* in-memory serial counter from here on (verdict §(d): "seqs minted serially in-memory per tab") —
|
|
181
|
+
* it is not re-read from storage again this session.
|
|
182
|
+
*/
|
|
183
|
+
declare function mintIdentity(storage: OutboxStorage, opts?: {
|
|
184
|
+
deployment?: string;
|
|
185
|
+
mintClientId?: () => string;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
clientId: string;
|
|
188
|
+
nextSeq: number;
|
|
189
|
+
}>;
|
|
190
|
+
|
|
191
|
+
export { DEFAULT_OUTBOX_MAX_QUEUE_SIZE as D, type HydrateResult as H, type IndexedDBOutboxOptions as I, type OutboxEntry as O, type OutboxStorage as a, OfflineClientResetError as b, type OutboxEntryError as c, type OutboxEntryStatus as d, type OutboxMeta as e, OutboxOverflowError as f, defaultMintClientId as g, mintIdentity as h, indexedDBOutbox as i, memoryOutbox as m };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Value } from '@helipod/values';
|
|
4
|
+
import { H as HelipodClient, j as OptimisticLocalStore, c as AnyFunctionReference, F as FunctionArgs, h as FunctionReturnType, g as FunctionReference, r as PendingMutationEntry } from './client-mJZjEkhK.js';
|
|
5
|
+
import '@helipod/sync';
|
|
6
|
+
import './outbox-storage-C6VHkXs9.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `useNotifications()` / `<Inbox>` — the typed reactive in-app inbox helper (Global Constraints:
|
|
10
|
+
* "the reactive in-app inbox is the flagship"). Wraps the well-known `@helipod/notifications`
|
|
11
|
+
* component query/mutation paths (`notifications:inbox`/`unreadCount`/`markRead`/`markAllRead`) so
|
|
12
|
+
* consumers get a live feed + unread count + typed mark-read callbacks with zero per-app codegen.
|
|
13
|
+
* Component functions aren't in an app's generated `Api`, so typing lives HERE (well-known paths),
|
|
14
|
+
* while the server-side `ctx.notifications.send` is typed via the component's `contextType`
|
|
15
|
+
* augmentation.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** An inbox row as delivered to the UI (mirrors the server `InboxItem`). */
|
|
19
|
+
interface InboxNotification {
|
|
20
|
+
_id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
body: string;
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
read: boolean;
|
|
25
|
+
readAt?: number;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
messageId: string;
|
|
28
|
+
}
|
|
29
|
+
interface UseNotificationsResult {
|
|
30
|
+
notifications: InboxNotification[];
|
|
31
|
+
unreadCount: number;
|
|
32
|
+
markRead: (id: string) => Promise<void>;
|
|
33
|
+
markAllRead: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Live inbox feed + unread count + mark-read callbacks. `undefined` first-frame results coalesce to
|
|
36
|
+
* `[]`/`0` so consumers never branch on the loading sentinel. */
|
|
37
|
+
declare function useNotifications(opts?: {
|
|
38
|
+
limit?: number;
|
|
39
|
+
}): UseNotificationsResult;
|
|
40
|
+
interface InboxProps {
|
|
41
|
+
limit?: number;
|
|
42
|
+
/** Render prop — headless: you own the markup, we own the reactive data + callbacks. */
|
|
43
|
+
children: (state: UseNotificationsResult) => ReactNode;
|
|
44
|
+
}
|
|
45
|
+
/** A headless `<Inbox>` render helper: `<Inbox>{({ notifications, unreadCount, markRead }) => …}</Inbox>`. */
|
|
46
|
+
declare function Inbox(props: InboxProps): ReactNode;
|
|
47
|
+
/** N3 — a caller's own `(category, channel|category-wide)` preference row, as delivered to the UI
|
|
48
|
+
* (mirrors the server `getPreferences` return; `channel` absent = category-wide). */
|
|
49
|
+
interface NotificationPreference {
|
|
50
|
+
category: string;
|
|
51
|
+
channel?: "email" | "sms" | "in_app" | "push";
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
}
|
|
54
|
+
interface UseNotificationPreferencesResult {
|
|
55
|
+
preferences: NotificationPreference[];
|
|
56
|
+
setPreference: (args: NotificationPreference) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/** N3 — live view of the caller's own notification preferences + a setter. Server-resolved identity
|
|
59
|
+
* (same ownership model as `useNotifications`'s inbox) — there is no `userId` arg to pass. */
|
|
60
|
+
declare function useNotificationPreferences(): UseNotificationPreferencesResult;
|
|
61
|
+
/** Register this device's push token for the CURRENT authenticated caller (self-only, server-
|
|
62
|
+
* resolved — see `docs/superpowers/specs/2026-04-13-notifications-push-channel-design.md`).
|
|
63
|
+
* Acquiring the actual OS token (Expo `getExpoPushTokenAsync()`, a native FCM/APNs SDK, or a web
|
|
64
|
+
* `PushManager.subscribe`) is the caller's responsibility — this is a thin wire call, nothing
|
|
65
|
+
* more, matching `useNotifications`'s scope boundary for the inbox. A plain async function (not a
|
|
66
|
+
* hook): registration typically happens once at app-boot/permission-grant time, not on every
|
|
67
|
+
* render. */
|
|
68
|
+
declare function registerForPush(client: HelipodClient, args: {
|
|
69
|
+
token: string;
|
|
70
|
+
provider: "expo" | "fcm" | "apns";
|
|
71
|
+
platform?: "ios" | "android" | "web";
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
/** Unregister this device's push token (e.g. on sign-out / permission revoke). */
|
|
74
|
+
declare function unregisterForPush(client: HelipodClient, args: {
|
|
75
|
+
token: string;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
|
|
78
|
+
declare function HelipodProvider(props: {
|
|
79
|
+
client: HelipodClient;
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}): react.JSX.Element;
|
|
82
|
+
declare function useHelipodClient(): HelipodClient;
|
|
83
|
+
/** Subscribe to a reactive query; returns `undefined` until the first result arrives. */
|
|
84
|
+
declare function useQuery<Q extends AnyFunctionReference<any, any>>(ref: Q, args?: FunctionArgs<Q>): FunctionReturnType<Q> | undefined;
|
|
85
|
+
declare function useQuery<T = Value>(ref: FunctionReference | string, args?: Record<string, Value>): T | undefined;
|
|
86
|
+
/** The optimistic-update closure a `withOptimisticUpdate` call receives — the typed public store. */
|
|
87
|
+
type OptimisticUpdateHandler<Args> = (store: OptimisticLocalStore, args: Args) => void;
|
|
88
|
+
/**
|
|
89
|
+
* The callable `useMutation` returns: calling it runs the mutation; `.withOptimisticUpdate(fn)`
|
|
90
|
+
* returns a NEW callable with `fn` bound (Convex-verbatim — it does not mutate the receiver, so
|
|
91
|
+
* `useMutation(ref)` itself stays reusable without an optimistic update).
|
|
92
|
+
*/
|
|
93
|
+
interface MutationCallback<Args = Record<string, Value>, Returns = Value> {
|
|
94
|
+
(args?: Args): Promise<Returns>;
|
|
95
|
+
withOptimisticUpdate(updater: OptimisticUpdateHandler<Args>): MutationCallback<Args, Returns>;
|
|
96
|
+
}
|
|
97
|
+
/** Returns a callable that runs a mutation and resolves with its return value; `.withOptimisticUpdate(fn)` chains an optimistic update (verdict §(b)). */
|
|
98
|
+
declare function useMutation<Q extends AnyFunctionReference<any, any>>(ref: Q): MutationCallback<FunctionArgs<Q>, FunctionReturnType<Q>>;
|
|
99
|
+
declare function useMutation<T = Value>(ref: FunctionReference | string): MutationCallback<Record<string, Value>, T>;
|
|
100
|
+
/** Returns a callback that runs an action and resolves with its return value. Not reactive — mirrors `useMutation`, not `useQuery`. */
|
|
101
|
+
declare function useAction<Q extends AnyFunctionReference<any, any>>(ref: Q): (args?: FunctionArgs<Q>) => Promise<FunctionReturnType<Q>>;
|
|
102
|
+
declare function useAction<T = Value>(ref: FunctionReference | string): (args?: Record<string, Value>) => Promise<T>;
|
|
103
|
+
/**
|
|
104
|
+
* T5 (R9) — a live snapshot of the durable outbox: `[]` until the first read resolves (and forever,
|
|
105
|
+
* without an `outbox` configured — `HelipodClient.pendingMutations()`'s own no-outbox behavior).
|
|
106
|
+
* Re-reads on every `client.onOutboxChange` notification — every local outbox-mutating op AND, when
|
|
107
|
+
* a `BroadcastChannel` is available, an incoming cross-tab nudge from ANOTHER tab sharing the same
|
|
108
|
+
* durable store (verdict §(d) "Observability": "usePendingMutations() reactive... a BroadcastChannel
|
|
109
|
+
* nudge cross-tab"). See `docs/enduser/offline.md`'s pending-tray recipe for the documented pattern
|
|
110
|
+
* (`packages/client/test/pending-tray-recipe.test.tsx` is its compiling fixture).
|
|
111
|
+
*/
|
|
112
|
+
declare function usePendingMutations(): PendingMutationEntry[];
|
|
113
|
+
|
|
114
|
+
export { HelipodProvider, Inbox, type InboxNotification, type InboxProps, type MutationCallback, type NotificationPreference, type OptimisticUpdateHandler, type UseNotificationPreferencesResult, type UseNotificationsResult, registerForPush, unregisterForPush, useAction, useHelipodClient, useMutation, useNotificationPreferences, useNotifications, usePendingMutations, useQuery };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFunctionPath
|
|
3
|
+
} from "./chunk-I7ZJFW4E.js";
|
|
4
|
+
import "./chunk-DW55SNHW.js";
|
|
5
|
+
|
|
6
|
+
// src/react.tsx
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { convexToJson } from "@helipod/values";
|
|
9
|
+
|
|
10
|
+
// src/notifications.tsx
|
|
11
|
+
var INBOX_PATH = "notifications:inbox";
|
|
12
|
+
var UNREAD_PATH = "notifications:unreadCount";
|
|
13
|
+
var MARK_READ_PATH = "notifications:markRead";
|
|
14
|
+
var MARK_ALL_PATH = "notifications:markAllRead";
|
|
15
|
+
function useNotifications(opts) {
|
|
16
|
+
const notifications = useQuery(INBOX_PATH, { limit: opts?.limit ?? 50 }) ?? [];
|
|
17
|
+
const unreadCount = useQuery(UNREAD_PATH, {}) ?? 0;
|
|
18
|
+
const markReadFn = useMutation(MARK_READ_PATH);
|
|
19
|
+
const markAllFn = useMutation(MARK_ALL_PATH);
|
|
20
|
+
return {
|
|
21
|
+
notifications,
|
|
22
|
+
unreadCount,
|
|
23
|
+
markRead: async (id) => {
|
|
24
|
+
await markReadFn({ id });
|
|
25
|
+
},
|
|
26
|
+
markAllRead: async () => {
|
|
27
|
+
await markAllFn({});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function Inbox(props) {
|
|
32
|
+
return props.children(useNotifications({ limit: props.limit }));
|
|
33
|
+
}
|
|
34
|
+
var PREFS_GET = "notifications:getPreferences";
|
|
35
|
+
var PREFS_SET = "notifications:setPreference";
|
|
36
|
+
function useNotificationPreferences() {
|
|
37
|
+
const preferences = useQuery(PREFS_GET, {}) ?? [];
|
|
38
|
+
const setFn = useMutation(PREFS_SET);
|
|
39
|
+
return {
|
|
40
|
+
preferences,
|
|
41
|
+
setPreference: async (args) => {
|
|
42
|
+
await setFn(args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var REGISTER_PUSH_PATH = "notifications:registerPushToken";
|
|
47
|
+
var UNREGISTER_PUSH_PATH = "notifications:unregisterPushToken";
|
|
48
|
+
async function registerForPush(client, args) {
|
|
49
|
+
await client.mutation(REGISTER_PUSH_PATH, args);
|
|
50
|
+
}
|
|
51
|
+
async function unregisterForPush(client, args) {
|
|
52
|
+
await client.mutation(UNREGISTER_PUSH_PATH, args);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/react.tsx
|
|
56
|
+
import { jsx } from "react/jsx-runtime";
|
|
57
|
+
var ClientContext = createContext(null);
|
|
58
|
+
function HelipodProvider(props) {
|
|
59
|
+
return /* @__PURE__ */ jsx(ClientContext.Provider, { value: props.client, children: props.children });
|
|
60
|
+
}
|
|
61
|
+
function useHelipodClient() {
|
|
62
|
+
const client = useContext(ClientContext);
|
|
63
|
+
if (!client) throw new Error("useHelipodClient must be used within <HelipodProvider>");
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
function argsKey(ref, args) {
|
|
67
|
+
return `${getFunctionPath(ref)}|${JSON.stringify(convexToJson(args))}`;
|
|
68
|
+
}
|
|
69
|
+
function useQuery(ref, args = {}) {
|
|
70
|
+
const client = useHelipodClient();
|
|
71
|
+
const [value, setValue] = useState(void 0);
|
|
72
|
+
const key = argsKey(ref, args);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setValue(void 0);
|
|
75
|
+
const unsubscribe = client.subscribe(ref, args, (v) => setValue(v));
|
|
76
|
+
return unsubscribe;
|
|
77
|
+
}, [client, key]);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function createMutationCallback(client, path, updater) {
|
|
81
|
+
const withUpdateCache = /* @__PURE__ */ new WeakMap();
|
|
82
|
+
const call = ((args) => client.mutation(
|
|
83
|
+
path,
|
|
84
|
+
args ?? {},
|
|
85
|
+
updater ? { optimisticUpdate: updater } : {}
|
|
86
|
+
));
|
|
87
|
+
call.withOptimisticUpdate = (next) => {
|
|
88
|
+
let bound = withUpdateCache.get(next);
|
|
89
|
+
if (!bound) {
|
|
90
|
+
bound = createMutationCallback(client, path, next);
|
|
91
|
+
withUpdateCache.set(next, bound);
|
|
92
|
+
}
|
|
93
|
+
return bound;
|
|
94
|
+
};
|
|
95
|
+
return call;
|
|
96
|
+
}
|
|
97
|
+
function useMutation(ref) {
|
|
98
|
+
const client = useHelipodClient();
|
|
99
|
+
const path = getFunctionPath(ref);
|
|
100
|
+
return useMemo(() => createMutationCallback(client, path), [client, path]);
|
|
101
|
+
}
|
|
102
|
+
function useAction(ref) {
|
|
103
|
+
const client = useHelipodClient();
|
|
104
|
+
const path = getFunctionPath(ref);
|
|
105
|
+
return useCallback((args = {}) => client.action(path, args), [client, path]);
|
|
106
|
+
}
|
|
107
|
+
function usePendingMutations() {
|
|
108
|
+
const client = useHelipodClient();
|
|
109
|
+
const [entries, setEntries] = useState([]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
const refresh = () => {
|
|
113
|
+
void client.pendingMutations().then((next) => {
|
|
114
|
+
if (!cancelled) setEntries(next);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
refresh();
|
|
118
|
+
const unsubscribe = client.onOutboxChange(refresh);
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
unsubscribe();
|
|
122
|
+
};
|
|
123
|
+
}, [client]);
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
HelipodProvider,
|
|
128
|
+
Inbox,
|
|
129
|
+
registerForPush,
|
|
130
|
+
unregisterForPush,
|
|
131
|
+
useAction,
|
|
132
|
+
useHelipodClient,
|
|
133
|
+
useMutation,
|
|
134
|
+
useNotificationPreferences,
|
|
135
|
+
useNotifications,
|
|
136
|
+
usePendingMutations,
|
|
137
|
+
useQuery
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx","../src/notifications.tsx"],"sourcesContent":["/**\n * React bindings — `useQuery` subscribes a component to a reactive query (re-rendering on\n * every server push), `useMutation` returns a callback that runs a mutation. The headline DX:\n * a component that calls `useQuery(api.messages.list, { conversationId })` updates live when\n * anyone sends a message, with zero manual wiring.\n *\n * T5 adds `useMutation(ref).withOptimisticUpdate(fn)` (Convex-verbatim chaining, verdict §(b)) and\n * threads T3's `FunctionArgs`/`FunctionReturnType` generics through all three hooks: passing a\n * codegen-generated ref (`api.messages.send`, typed against the app's `_generated/api.d.ts`) infers\n * typed args/return; the client's own untyped `{ __path }` ref or a raw string path fall back to\n * the pre-existing `Record<string, Value>`/`Value` shape with an explicit `T` override, unchanged.\n */\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { convexToJson, type Value } from \"@helipod/values\";\nimport { HelipodClient, type PendingMutationEntry } from \"./client\";\nimport { getFunctionPath, type AnyFunctionRef, type FunctionReference } from \"./api\";\nimport type { AnyFunctionReference, FunctionArgs, FunctionReturnType } from \"./function-types\";\nimport type { OptimisticLocalStore } from \"./optimistic-store\";\nimport type { OptimisticUpdate } from \"./layered-store\";\n\nconst ClientContext = createContext<HelipodClient | null>(null);\n\nexport function HelipodProvider(props: { client: HelipodClient; children: ReactNode }) {\n return <ClientContext.Provider value={props.client}>{props.children}</ClientContext.Provider>;\n}\n\nexport function useHelipodClient(): HelipodClient {\n const client = useContext(ClientContext);\n if (!client) throw new Error(\"useHelipodClient must be used within <HelipodProvider>\");\n return client;\n}\n\nfunction argsKey(ref: AnyFunctionRef, args: Record<string, Value>): string {\n return `${getFunctionPath(ref)}|${JSON.stringify(convexToJson(args as Value))}`;\n}\n\n/** Subscribe to a reactive query; returns `undefined` until the first result arrives. */\nexport function useQuery<Q extends AnyFunctionReference<any, any>>(ref: Q, args?: FunctionArgs<Q>): FunctionReturnType<Q> | undefined;\nexport function useQuery<T = Value>(ref: FunctionReference | string, args?: Record<string, Value>): T | undefined;\nexport function useQuery(ref: AnyFunctionRef, args: Record<string, Value> = {}): unknown {\n const client = useHelipodClient();\n const [value, setValue] = useState<Value | undefined>(undefined);\n const key = argsKey(ref, args);\n\n useEffect(() => {\n setValue(undefined);\n const unsubscribe = client.subscribe(ref as FunctionReference | string, args, (v) => setValue(v));\n return unsubscribe;\n // `key` captures ref+args identity; re-subscribe only when it changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, key]);\n\n return value;\n}\n\n/** The optimistic-update closure a `withOptimisticUpdate` call receives — the typed public store. */\nexport type OptimisticUpdateHandler<Args> = (store: OptimisticLocalStore, args: Args) => void;\n\n/**\n * The callable `useMutation` returns: calling it runs the mutation; `.withOptimisticUpdate(fn)`\n * returns a NEW callable with `fn` bound (Convex-verbatim — it does not mutate the receiver, so\n * `useMutation(ref)` itself stays reusable without an optimistic update).\n */\nexport interface MutationCallback<Args = Record<string, Value>, Returns = Value> {\n (args?: Args): Promise<Returns>;\n withOptimisticUpdate(updater: OptimisticUpdateHandler<Args>): MutationCallback<Args, Returns>;\n}\n\n/**\n * Builds one mutation callable. A `WeakMap` cache keyed by the `updater` function reference lives\n * inside this closure (persisted across renders via the `useMemo` below) — calling\n * `.withOptimisticUpdate(sameUpdaterRef)` again on a later render returns the SAME bound callable\n * (identity does not churn) as long as the caller's `updater` reference is itself stable (e.g.\n * module-scoped or `useCallback`-memoized); a fresh inline closure every render churns by\n * necessity — there is no way to detect two closures are \"the same update\" without a reference.\n */\nfunction createMutationCallback<Args, Returns>(\n client: HelipodClient,\n path: string,\n updater?: OptimisticUpdateHandler<Args>,\n): MutationCallback<Args, Returns> {\n const withUpdateCache = new WeakMap<OptimisticUpdateHandler<Args>, MutationCallback<Args, Returns>>();\n const call = ((args?: Args) =>\n client.mutation(\n path,\n (args ?? {}) as Record<string, Value>,\n updater ? { optimisticUpdate: updater as unknown as OptimisticUpdate } : {},\n ) as Promise<Returns>) as MutationCallback<Args, Returns>;\n call.withOptimisticUpdate = (next: OptimisticUpdateHandler<Args>) => {\n let bound = withUpdateCache.get(next);\n if (!bound) {\n bound = createMutationCallback<Args, Returns>(client, path, next);\n withUpdateCache.set(next, bound);\n }\n return bound;\n };\n return call;\n}\n\n/** Returns a callable that runs a mutation and resolves with its return value; `.withOptimisticUpdate(fn)` chains an optimistic update (verdict §(b)). */\nexport function useMutation<Q extends AnyFunctionReference<any, any>>(\n ref: Q,\n): MutationCallback<FunctionArgs<Q>, FunctionReturnType<Q>>;\nexport function useMutation<T = Value>(ref: FunctionReference | string): MutationCallback<Record<string, Value>, T>;\nexport function useMutation(ref: AnyFunctionRef): MutationCallback<any, any> {\n const client = useHelipodClient();\n const path = getFunctionPath(ref);\n return useMemo(() => createMutationCallback(client, path), [client, path]);\n}\n\n/** Returns a callback that runs an action and resolves with its return value. Not reactive — mirrors `useMutation`, not `useQuery`. */\nexport function useAction<Q extends AnyFunctionReference<any, any>>(ref: Q): (args?: FunctionArgs<Q>) => Promise<FunctionReturnType<Q>>;\nexport function useAction<T = Value>(ref: FunctionReference | string): (args?: Record<string, Value>) => Promise<T>;\nexport function useAction(ref: AnyFunctionRef): (args?: Record<string, Value>) => Promise<Value> {\n const client = useHelipodClient();\n const path = getFunctionPath(ref);\n return useCallback((args: Record<string, Value> = {}) => client.action(path, args), [client, path]);\n}\n\n/**\n * T5 (R9) — a live snapshot of the durable outbox: `[]` until the first read resolves (and forever,\n * without an `outbox` configured — `HelipodClient.pendingMutations()`'s own no-outbox behavior).\n * Re-reads on every `client.onOutboxChange` notification — every local outbox-mutating op AND, when\n * a `BroadcastChannel` is available, an incoming cross-tab nudge from ANOTHER tab sharing the same\n * durable store (verdict §(d) \"Observability\": \"usePendingMutations() reactive... a BroadcastChannel\n * nudge cross-tab\"). See `docs/enduser/offline.md`'s pending-tray recipe for the documented pattern\n * (`packages/client/test/pending-tray-recipe.test.tsx` is its compiling fixture).\n */\nexport function usePendingMutations(): PendingMutationEntry[] {\n const client = useHelipodClient();\n const [entries, setEntries] = useState<PendingMutationEntry[]>([]);\n\n useEffect(() => {\n let cancelled = false;\n const refresh = () => {\n void client.pendingMutations().then((next) => {\n if (!cancelled) setEntries(next);\n });\n };\n refresh();\n const unsubscribe = client.onOutboxChange(refresh);\n return () => {\n cancelled = true;\n unsubscribe();\n };\n }, [client]);\n\n return entries;\n}\n\n// Reactive in-app inbox helper (@helipod/notifications) — see ./notifications.\nexport { useNotifications, Inbox, useNotificationPreferences, registerForPush, unregisterForPush } from \"./notifications\";\nexport type { InboxNotification, UseNotificationsResult, InboxProps, NotificationPreference, UseNotificationPreferencesResult } from \"./notifications\";\n","/**\n * `useNotifications()` / `<Inbox>` — the typed reactive in-app inbox helper (Global Constraints:\n * \"the reactive in-app inbox is the flagship\"). Wraps the well-known `@helipod/notifications`\n * component query/mutation paths (`notifications:inbox`/`unreadCount`/`markRead`/`markAllRead`) so\n * consumers get a live feed + unread count + typed mark-read callbacks with zero per-app codegen.\n * Component functions aren't in an app's generated `Api`, so typing lives HERE (well-known paths),\n * while the server-side `ctx.notifications.send` is typed via the component's `contextType`\n * augmentation.\n */\nimport type { ReactNode } from \"react\";\nimport type { Value } from \"@helipod/values\";\nimport { useQuery, useMutation } from \"./react\";\nimport type { HelipodClient } from \"./client\";\n\n/** An inbox row as delivered to the UI (mirrors the server `InboxItem`). */\nexport interface InboxNotification {\n _id: string;\n title: string;\n body: string;\n data?: Record<string, unknown>;\n read: boolean;\n readAt?: number;\n createdAt: number;\n messageId: string;\n}\n\nexport interface UseNotificationsResult {\n notifications: InboxNotification[];\n unreadCount: number;\n markRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n}\n\nconst INBOX_PATH = \"notifications:inbox\";\nconst UNREAD_PATH = \"notifications:unreadCount\";\nconst MARK_READ_PATH = \"notifications:markRead\";\nconst MARK_ALL_PATH = \"notifications:markAllRead\";\n\n/** Live inbox feed + unread count + mark-read callbacks. `undefined` first-frame results coalesce to\n * `[]`/`0` so consumers never branch on the loading sentinel. */\nexport function useNotifications(opts?: { limit?: number }): UseNotificationsResult {\n const notifications = useQuery<InboxNotification[]>(INBOX_PATH, { limit: opts?.limit ?? 50 }) ?? [];\n const unreadCount = useQuery<number>(UNREAD_PATH, {}) ?? 0;\n const markReadFn = useMutation<null>(MARK_READ_PATH);\n const markAllFn = useMutation<null>(MARK_ALL_PATH);\n return {\n notifications,\n unreadCount,\n markRead: async (id: string) => { await markReadFn({ id }); },\n markAllRead: async () => { await markAllFn({}); },\n };\n}\n\nexport interface InboxProps {\n limit?: number;\n /** Render prop — headless: you own the markup, we own the reactive data + callbacks. */\n children: (state: UseNotificationsResult) => ReactNode;\n}\n\n/** A headless `<Inbox>` render helper: `<Inbox>{({ notifications, unreadCount, markRead }) => …}</Inbox>`. */\nexport function Inbox(props: InboxProps): ReactNode {\n return props.children(useNotifications({ limit: props.limit }));\n}\n\n/** N3 — a caller's own `(category, channel|category-wide)` preference row, as delivered to the UI\n * (mirrors the server `getPreferences` return; `channel` absent = category-wide). */\nexport interface NotificationPreference {\n category: string;\n channel?: \"email\" | \"sms\" | \"in_app\" | \"push\";\n enabled: boolean;\n}\n\nexport interface UseNotificationPreferencesResult {\n preferences: NotificationPreference[];\n setPreference: (args: NotificationPreference) => Promise<void>;\n}\n\nconst PREFS_GET = \"notifications:getPreferences\";\nconst PREFS_SET = \"notifications:setPreference\";\n\n/** N3 — live view of the caller's own notification preferences + a setter. Server-resolved identity\n * (same ownership model as `useNotifications`'s inbox) — there is no `userId` arg to pass. */\nexport function useNotificationPreferences(): UseNotificationPreferencesResult {\n const preferences = useQuery<NotificationPreference[]>(PREFS_GET, {}) ?? [];\n const setFn = useMutation<null>(PREFS_SET);\n return {\n preferences,\n setPreference: async (args: NotificationPreference) => {\n await setFn(args as unknown as Record<string, Value>);\n },\n };\n}\n\nconst REGISTER_PUSH_PATH = \"notifications:registerPushToken\";\nconst UNREGISTER_PUSH_PATH = \"notifications:unregisterPushToken\";\n\n/** Register this device's push token for the CURRENT authenticated caller (self-only, server-\n * resolved — see `docs/superpowers/specs/2026-04-13-notifications-push-channel-design.md`).\n * Acquiring the actual OS token (Expo `getExpoPushTokenAsync()`, a native FCM/APNs SDK, or a web\n * `PushManager.subscribe`) is the caller's responsibility — this is a thin wire call, nothing\n * more, matching `useNotifications`'s scope boundary for the inbox. A plain async function (not a\n * hook): registration typically happens once at app-boot/permission-grant time, not on every\n * render. */\nexport async function registerForPush(client: HelipodClient, args: { token: string; provider: \"expo\" | \"fcm\" | \"apns\"; platform?: \"ios\" | \"android\" | \"web\" }): Promise<void> {\n await client.mutation(REGISTER_PUSH_PATH, args as unknown as Record<string, Value>);\n}\n\n/** Unregister this device's push token (e.g. on sign-out / permission revoke). */\nexport async function unregisterForPush(client: HelipodClient, args: { token: string }): Promise<void> {\n await client.mutation(UNREGISTER_PUSH_PATH, args as unknown as Record<string, Value>);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helipod/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./react": {
|
|
16
|
+
"types": "./dist/react.d.ts",
|
|
17
|
+
"default": "./dist/react.js"
|
|
18
|
+
},
|
|
19
|
+
"./outbox-fs": {
|
|
20
|
+
"types": "./dist/outbox-fs.d.ts",
|
|
21
|
+
"default": "./dist/outbox-fs.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"clean": "rm -rf dist .turbo"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"react": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@helipod/id-codec": "0.1.0",
|
|
43
|
+
"@helipod/index-key-codec": "0.1.0",
|
|
44
|
+
"@helipod/sync": "0.1.0",
|
|
45
|
+
"@helipod/values": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@helipod/docstore-sqlite": "0.1.0",
|
|
49
|
+
"@helipod/executor": "0.1.0",
|
|
50
|
+
"@helipod/query-engine": "0.1.0",
|
|
51
|
+
"@helipod/runtime-embedded": "0.1.0",
|
|
52
|
+
"@testing-library/react": "^16.1.0",
|
|
53
|
+
"@types/node": "^22.10.5",
|
|
54
|
+
"@types/react": "^18.3.18",
|
|
55
|
+
"fake-indexeddb": "^6.0.0",
|
|
56
|
+
"jsdom": "^25.0.1",
|
|
57
|
+
"react": "^18.3.1",
|
|
58
|
+
"react-dom": "^18.3.1",
|
|
59
|
+
"tsup": "^8.3.5",
|
|
60
|
+
"typescript": "^5.7.2",
|
|
61
|
+
"vitest": "^2.1.8"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/helipod-sh/helipod.git",
|
|
69
|
+
"directory": "packages/client"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://github.com/helipod-sh/helipod"
|
|
72
|
+
}
|