@lunora/db 1.0.0-alpha.26 → 1.0.0-alpha.260

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +38 -2
  2. package/dist/collections/index.d.mts +2 -2
  3. package/dist/collections/index.d.ts +2 -2
  4. package/dist/collections/index.mjs +1 -2
  5. package/dist/index.d.mts +144 -5
  6. package/dist/index.d.ts +144 -5
  7. package/dist/index.mjs +1 -8
  8. package/dist/mutators/index.d.mts +2 -2
  9. package/dist/mutators/index.d.ts +2 -2
  10. package/dist/mutators/index.mjs +1 -1
  11. package/dist/packem_shared/CHECKPOINT_FALLBACK_MS-Dnkx44kO.mjs +1 -0
  12. package/dist/packem_shared/DIRECT_TRANSACTION_METADATA_KEY-BGf7W7d4.mjs +1 -0
  13. package/dist/packem_shared/OUTBOX_MUTATION_FN_NAME-rGyegnLw.mjs +1 -0
  14. package/dist/packem_shared/applyPlanToCollections-DRwloPsc.mjs +1 -0
  15. package/dist/packem_shared/collection-options.d-C2FdJw06.d.mts +411 -0
  16. package/dist/packem_shared/collection-options.d-C2FdJw06.d.ts +411 -0
  17. package/dist/packem_shared/defineCollections-D4Y0GiBS.mjs +1 -0
  18. package/dist/packem_shared/index.d-9usNsVAH.d.mts +245 -0
  19. package/dist/packem_shared/{define-collections.d-Dkzt5xPY.d.ts → index.d-B_2dXw1Z.d.mts} +23 -7
  20. package/dist/packem_shared/index.d-t0PhXOHV.d.ts +245 -0
  21. package/dist/packem_shared/{define-collections.d-C9paBiXl.d.mts → index.d-ypscb9Qp.d.ts} +23 -7
  22. package/package.json +3 -3
  23. package/dist/packem_shared/OUTBOX_MUTATION_FN_NAME-9fqK7rND.mjs +0 -131
  24. package/dist/packem_shared/bindMutators-B_RaNgel.mjs +0 -70
  25. package/dist/packem_shared/collection-options.d-BPVkj5FW.d.mts +0 -227
  26. package/dist/packem_shared/collection-options.d-BPVkj5FW.d.ts +0 -227
  27. package/dist/packem_shared/createCheckpointRegistry-BtIEm2Kh.mjs +0 -136
  28. package/dist/packem_shared/define-mutators.d-DTU85KL6.d.mts +0 -80
  29. package/dist/packem_shared/define-mutators.d-DukVT0Y3.d.ts +0 -80
  30. package/dist/packem_shared/defineCollections-C_F-42rS.mjs +0 -109
package/README.md CHANGED
@@ -65,7 +65,7 @@ export const createCollections = (client: LunoraClient) =>
65
65
  defineCollections(client, {
66
66
  messages: {
67
67
  list: api.messages.list,
68
- scopeBy: "channelId", // sharded — re-point with db.scope.messages({ channelId })
68
+ scopeBy: "channelId", // the .shardBy column — db.scope.messages({ channelId }) syncs that channel's shard
69
69
  insert: {
70
70
  mutation: api.messages.send,
71
71
  optimistic: (input: Omit<Doc<"messages">, "_id" | "_creationTime">, id) => ({
@@ -85,6 +85,12 @@ export const createCollections = (client: LunoraClient) =>
85
85
 
86
86
  > `vis generate lunora-collections` scaffolds this from your `schema.ts` + functions.
87
87
 
88
+ Keep a **single `db` instance**, and treat its collections as the one source of
89
+ truth per table — don't mirror rows into your own store. A derived index (a tree,
90
+ a search index, an undo capture) built from a copy of the rows can silently read
91
+ stale data while the UI renders through `useLiveQuery`. See [One source of truth
92
+ per table](https://lunora.sh/docs/packages/db#one-source-of-truth-per-table).
93
+
88
94
  ### Local-first sync engine
89
95
 
90
96
  Beyond whole-table collections, the `@lunora/db/collections` and
@@ -97,7 +103,37 @@ every sync tick). The framework adapters add a `useMutator` / `createMutator` /
97
103
  `mutator` hook over a bound handle. See the
98
104
  **[local-first guide](https://lunora.sh/docs/concepts/local-first)**.
99
105
 
100
- > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/addons/db)**.
106
+ On a `scopeBy` shape collection, give `bindMutators` the same `scopeBy` so each
107
+ push reaches the shard its args name — the shard the collection syncs:
108
+
109
+ ```ts
110
+ const { checkpoints, config, scope } = lunoraCollectionOptions({ client, shape: { name: "messagesByChannel" }, scopeBy: "channelId" });
111
+ const send = bindMutators(client, { checkpoints, collections, scopeBy: "channelId" }, mutators);
112
+ ```
113
+
114
+ ## Upgrading: scoped collections now use their shard
115
+
116
+ Earlier alphas sent every subscription and insert of a `scopeBy` collection with
117
+ no `shardKey` to the default shard (`__root__`); they now go to the shard the
118
+ scoped value names.
119
+
120
+ - **Authorize shard access.** Configure `authorizeShard` on the worker (or
121
+ `allowUnauthenticatedShardAccess: true` when every table has row-level
122
+ security); otherwise scoped subscriptions and writes fail with `403`
123
+ `FORBIDDEN_SHARD`.
124
+ - **Move existing rows.** Rows written before the upgrade are still in the
125
+ default shard, and scoped reads do not look there. Before the upgraded client
126
+ writes to any channel shard, run `lunora export --tables <table>` (it reaches
127
+ the default shard while the table has no registered shard keys) and then
128
+ `lunora import` the file: import re-buckets each row by the table's `.shardBy`
129
+ field and keeps its `_id`, skipping any `_id` the target shard already holds.
130
+ Import deletes nothing, so then remove the default shard's copies with a
131
+ one-off mutation run via `lunora run <fn> --shard __root__`.
132
+
133
+ See [Upgrading](https://lunora.sh/docs/packages/db#upgrading-scoped-collections-now-use-their-shard)
134
+ for the full recipe.
135
+
136
+ > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/packages/db)**.
101
137
 
102
138
  ## Related
103
139
 
@@ -1,5 +1,5 @@
1
- export { type C as CheckpointRegistry, type L as LunoraCollectionConfig, type a as LunoraCollectionOptions, c as createCheckpointRegistry, l as lunoraCollectionOptions } from "../packem_shared/collection-options.d-BPVkj5FW.mjs";
2
- export { type C as CollectionDef, type I as InsertBinding, type L as LunoraDb, d as defineCollections } from "../packem_shared/define-collections.d-C9paBiXl.mjs";
1
+ export { type b as CheckpointRegistry, type L as LunoraCollectionConfig, type f as LunoraCollectionOptions, j as createCheckpointRegistry, n as lunoraCollectionOptions } from "../packem_shared/collection-options.d-C2FdJw06.mjs";
2
+ export { type C as CollectionDef, type I as InsertBinding, type L as LunoraDb, d as defineCollections } from "../packem_shared/index.d-B_2dXw1Z.mjs";
3
3
  import '@lunora/client';
4
4
  import '@tanstack/db';
5
5
  import '@tanstack/offline-transactions';
@@ -1,5 +1,5 @@
1
- export { type C as CheckpointRegistry, type L as LunoraCollectionConfig, type a as LunoraCollectionOptions, c as createCheckpointRegistry, l as lunoraCollectionOptions } from "../packem_shared/collection-options.d-BPVkj5FW.js";
2
- export { type C as CollectionDef, type I as InsertBinding, type L as LunoraDb, d as defineCollections } from "../packem_shared/define-collections.d-Dkzt5xPY.js";
1
+ export { type b as CheckpointRegistry, type L as LunoraCollectionConfig, type f as LunoraCollectionOptions, j as createCheckpointRegistry, n as lunoraCollectionOptions } from "../packem_shared/collection-options.d-C2FdJw06.js";
2
+ export { type C as CollectionDef, type I as InsertBinding, type L as LunoraDb, d as defineCollections } from "../packem_shared/index.d-ypscb9Qp.js";
3
3
  import '@lunora/client';
4
4
  import '@tanstack/db';
5
5
  import '@tanstack/offline-transactions';
@@ -1,2 +1 @@
1
- export { createCheckpointRegistry, lunoraCollectionOptions } from '../packem_shared/createCheckpointRegistry-BtIEm2Kh.mjs';
2
- export { defineCollections } from '../packem_shared/defineCollections-C_F-42rS.mjs';
1
+ import{createCheckpointRegistry as t,lunoraCollectionOptions as r}from"../packem_shared/CHECKPOINT_FALLBACK_MS-Dnkx44kO.mjs";import{defineCollections as n}from"../packem_shared/defineCollections-D4Y0GiBS.mjs";export{t as createCheckpointRegistry,n as defineCollections,r as lunoraCollectionOptions};
package/dist/index.d.mts CHANGED
@@ -1,8 +1,147 @@
1
- export { type C as CheckpointRegistry, type E as ExecutorOutboxSinkOptions, type L as LunoraCollectionConfig, type a as LunoraCollectionOptions, O as OUTBOX_MUTATION_FN_NAME, type b as OutboxExecutor, type d as OutboxMutationMetadata, type R as Row, type S as SyncWriter, c as createCheckpointRegistry, e as createExecutorOutboxSink, f as createOptimisticOnlineDetector, l as lunoraCollectionOptions, m as makeDiffEmit, r as runOutboxMutation, t as toMap } from "./packem_shared/collection-options.d-BPVkj5FW.mjs";
2
- export { type C as CollectionDef, type D as DefineCollectionsOptions, type I as InsertBinding, type L as LunoraDb, type W as WriteRejectedEvent, d as defineCollections } from "./packem_shared/define-collections.d-C9paBiXl.mjs";
3
- export { type B as BindMutatorsContext, type a as BoundMutators, type C as ClientMutatorContext, type b as ClientMutatorDef, c as bindMutators, d as defineMutator } from "./packem_shared/define-mutators.d-DTU85KL6.mjs";
1
+ import { Collection } from '@tanstack/db';
2
+ import { R as Row } from "./packem_shared/collection-options.d-C2FdJw06.mjs";
3
+ export { C as CHECKPOINT_FALLBACK_MS, type a as CheckpointFallbackEvent, type b as CheckpointRegistry, type c as CheckpointRegistryOptions, type d as CheckpointRegistryStats, type e as CheckpointWatermark, type E as ExecutorOutboxSinkOptions, type L as LunoraCollectionConfig, type f as LunoraCollectionOptions, type g as LunoraCollectionUtils, O as OUTBOX_MUTATION_FN_NAME, type h as OutboxExecutor, type i as OutboxMutationMetadata, type S as SyncWriter, j as createCheckpointRegistry, k as createExecutorOutboxSink, l as createOptimisticOnlineDetector, m as getShardCheckpoints, n as lunoraCollectionOptions, o as makeDiffEmit, r as releaseShardCheckpoints, p as runOutboxMutation, t as toMap } from "./packem_shared/collection-options.d-C2FdJw06.mjs";
4
+ export { type C as CollectionDef, type D as DefineCollectionsOptions, type I as InsertBinding, type L as LunoraDb, type W as WriteRejectedEvent, d as defineCollections } from "./packem_shared/index.d-B_2dXw1Z.mjs";
5
+ export { type B as BindMutatorsContext, type a as BoundMutatorApi, type b as BoundMutators, type C as ClientMutatorContext, type c as ClientMutatorDef, type d as CollectionMap, D as DIRECT_TRANSACTION_METADATA_KEY, type M as MutatorReference, type e as MutatorRejectedEvent, f as bindMutators, g as defineMutator, i as initMutators } from "./packem_shared/index.d-9usNsVAH.mjs";
4
6
  import '@lunora/client';
5
- import '@tanstack/db';
6
7
  import '@tanstack/offline-transactions';
8
+ /** One row to insert. `_id` may be pre-minted client-side so the optimistic row keys match the persisted one. */
9
+ interface PlanInsert {
10
+ /** Row body. Include `_id` to key the row yourself (the server honors it as the `clientId`). */
11
+ row: Record<string, unknown> & {
12
+ _id?: string;
13
+ };
14
+ table: string;
15
+ }
16
+ /** One row to patch, by id. */
17
+ interface PlanPatch {
18
+ fields: Record<string, unknown>;
19
+ id: string;
20
+ table: string;
21
+ }
22
+ /** One row to delete, by id. */
23
+ interface PlanDelete {
24
+ id: string;
25
+ table: string;
26
+ }
27
+ /**
28
+ * A change plan: what a mutator intends to write.
29
+ *
30
+ * Applied in a fixed order — **deletes, then patches, then inserts** — by both
31
+ * appliers. The order is part of the contract, not an implementation detail: a plan
32
+ * that deletes a row and inserts its replacement under the same natural key only
33
+ * behaves the same on both sides if both sides agree which happens first.
34
+ */
35
+ interface ChangePlan {
36
+ deletes?: ReadonlyArray<PlanDelete>;
37
+ inserts?: ReadonlyArray<PlanInsert>;
38
+ patches?: ReadonlyArray<PlanPatch>;
39
+ }
40
+ /**
41
+ * The `ctx.db` methods {@link applyPlanToDb} needs — structural, so any writer
42
+ * satisfies it.
43
+ *
44
+ * The `never` parameter positions are deliberate, not laziness. `ctx.db.delete` is
45
+ * `<T extends string>(table: T, id: Id<T>) => …` over the **branded** `Id<T>`, and under
46
+ * `strictFunctionTypes` a parameter is contravariant — so declaring `id: string` here
47
+ * would make the real `ctx.db` un-assignable to `PlanWriter` (`string` is not
48
+ * assignable to `string & { __table }`), and every caller would need a cast at the
49
+ * call site instead. `never` accepts any branded id, which keeps `applyPlanToDb(ctx.db,
50
+ * plan)` cast-free for the caller and confines the two `as never` casts to this module.
51
+ *
52
+ * The trade-off is real: argument checking inside `applyPlanToDb` is erased, so a plan
53
+ * naming a table the schema doesn't have is a runtime error. Use `ctx.db.asId(table,
54
+ * id)` when building the plan to catch a malformed id at the boundary.
55
+ */
56
+ interface PlanWriter {
57
+ delete: (table: never, id: never) => Promise<void>;
58
+ insert: (tableName: never, document: Record<string, unknown>, options?: {
59
+ clientId?: string;
60
+ }) => Promise<unknown>;
61
+ patch: (table: never, id: never, patch: Record<string, unknown>) => Promise<void>;
62
+ }
63
+ /**
64
+ * Apply `plan` to a map of TanStack collections — the client (optimistic) half.
65
+ *
66
+ * A plan naming a table with no wired collection is **skipped, not an error**: an app
67
+ * legitimately syncs a subset of the tables its mutators write (a server-only audit
68
+ * row has no client collection), and throwing would make the optimistic body fail on
69
+ * a write the server handles fine.
70
+ */
71
+ declare const applyPlanToCollections: (collections: Record<string, Collection<Row, string>>, plan: ChangePlan) => void;
72
+ /**
73
+ * Apply `plan` to a `ctx.db` writer — the server (authoritative) half.
74
+ *
75
+ * Sequential by design: the shard's SQLite is single-threaded and a mutation runs
76
+ * inside one BEGIN/COMMIT span, so ordering is observable and a mid-plan failure
77
+ * rolls the whole plan back. An insert carrying an `_id` forwards it as `clientId`,
78
+ * which is how a client-minted key becomes the persisted primary key.
79
+ */
80
+ declare const applyPlanToDb: (db: PlanWriter, plan: ChangePlan) => Promise<void>;
7
81
  declare const VERSION = "0.0.0";
8
- export { VERSION };
82
+ export {
83
+ /**
84
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
85
+ *
86
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
87
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
88
+ * offline-transactions outbox (writes) in one declaration. The lower-level
89
+ * helpers are exported for testing and advanced composition.
90
+ *
91
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
92
+ * consuming app pins their versions and supplies React bindings
93
+ * (`@tanstack/react-db`) itself.
94
+ */
95
+ type ChangePlan,
96
+ /**
97
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
98
+ *
99
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
100
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
101
+ * offline-transactions outbox (writes) in one declaration. The lower-level
102
+ * helpers are exported for testing and advanced composition.
103
+ *
104
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
105
+ * consuming app pins their versions and supplies React bindings
106
+ * (`@tanstack/react-db`) itself.
107
+ */
108
+ type PlanDelete,
109
+ /**
110
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
111
+ *
112
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
113
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
114
+ * offline-transactions outbox (writes) in one declaration. The lower-level
115
+ * helpers are exported for testing and advanced composition.
116
+ *
117
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
118
+ * consuming app pins their versions and supplies React bindings
119
+ * (`@tanstack/react-db`) itself.
120
+ */
121
+ type PlanInsert,
122
+ /**
123
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
124
+ *
125
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
126
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
127
+ * offline-transactions outbox (writes) in one declaration. The lower-level
128
+ * helpers are exported for testing and advanced composition.
129
+ *
130
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
131
+ * consuming app pins their versions and supplies React bindings
132
+ * (`@tanstack/react-db`) itself.
133
+ */
134
+ type PlanPatch,
135
+ /**
136
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
137
+ *
138
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
139
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
140
+ * offline-transactions outbox (writes) in one declaration. The lower-level
141
+ * helpers are exported for testing and advanced composition.
142
+ *
143
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
144
+ * consuming app pins their versions and supplies React bindings
145
+ * (`@tanstack/react-db`) itself.
146
+ */
147
+ type PlanWriter, type Row, VERSION, applyPlanToCollections, applyPlanToDb };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,147 @@
1
- export { type C as CheckpointRegistry, type E as ExecutorOutboxSinkOptions, type L as LunoraCollectionConfig, type a as LunoraCollectionOptions, O as OUTBOX_MUTATION_FN_NAME, type b as OutboxExecutor, type d as OutboxMutationMetadata, type R as Row, type S as SyncWriter, c as createCheckpointRegistry, e as createExecutorOutboxSink, f as createOptimisticOnlineDetector, l as lunoraCollectionOptions, m as makeDiffEmit, r as runOutboxMutation, t as toMap } from "./packem_shared/collection-options.d-BPVkj5FW.js";
2
- export { type C as CollectionDef, type D as DefineCollectionsOptions, type I as InsertBinding, type L as LunoraDb, type W as WriteRejectedEvent, d as defineCollections } from "./packem_shared/define-collections.d-Dkzt5xPY.js";
3
- export { type B as BindMutatorsContext, type a as BoundMutators, type C as ClientMutatorContext, type b as ClientMutatorDef, c as bindMutators, d as defineMutator } from "./packem_shared/define-mutators.d-DukVT0Y3.js";
1
+ import { Collection } from '@tanstack/db';
2
+ import { R as Row } from "./packem_shared/collection-options.d-C2FdJw06.js";
3
+ export { C as CHECKPOINT_FALLBACK_MS, type a as CheckpointFallbackEvent, type b as CheckpointRegistry, type c as CheckpointRegistryOptions, type d as CheckpointRegistryStats, type e as CheckpointWatermark, type E as ExecutorOutboxSinkOptions, type L as LunoraCollectionConfig, type f as LunoraCollectionOptions, type g as LunoraCollectionUtils, O as OUTBOX_MUTATION_FN_NAME, type h as OutboxExecutor, type i as OutboxMutationMetadata, type S as SyncWriter, j as createCheckpointRegistry, k as createExecutorOutboxSink, l as createOptimisticOnlineDetector, m as getShardCheckpoints, n as lunoraCollectionOptions, o as makeDiffEmit, r as releaseShardCheckpoints, p as runOutboxMutation, t as toMap } from "./packem_shared/collection-options.d-C2FdJw06.js";
4
+ export { type C as CollectionDef, type D as DefineCollectionsOptions, type I as InsertBinding, type L as LunoraDb, type W as WriteRejectedEvent, d as defineCollections } from "./packem_shared/index.d-ypscb9Qp.js";
5
+ export { type B as BindMutatorsContext, type a as BoundMutatorApi, type b as BoundMutators, type C as ClientMutatorContext, type c as ClientMutatorDef, type d as CollectionMap, D as DIRECT_TRANSACTION_METADATA_KEY, type M as MutatorReference, type e as MutatorRejectedEvent, f as bindMutators, g as defineMutator, i as initMutators } from "./packem_shared/index.d-t0PhXOHV.js";
4
6
  import '@lunora/client';
5
- import '@tanstack/db';
6
7
  import '@tanstack/offline-transactions';
8
+ /** One row to insert. `_id` may be pre-minted client-side so the optimistic row keys match the persisted one. */
9
+ interface PlanInsert {
10
+ /** Row body. Include `_id` to key the row yourself (the server honors it as the `clientId`). */
11
+ row: Record<string, unknown> & {
12
+ _id?: string;
13
+ };
14
+ table: string;
15
+ }
16
+ /** One row to patch, by id. */
17
+ interface PlanPatch {
18
+ fields: Record<string, unknown>;
19
+ id: string;
20
+ table: string;
21
+ }
22
+ /** One row to delete, by id. */
23
+ interface PlanDelete {
24
+ id: string;
25
+ table: string;
26
+ }
27
+ /**
28
+ * A change plan: what a mutator intends to write.
29
+ *
30
+ * Applied in a fixed order — **deletes, then patches, then inserts** — by both
31
+ * appliers. The order is part of the contract, not an implementation detail: a plan
32
+ * that deletes a row and inserts its replacement under the same natural key only
33
+ * behaves the same on both sides if both sides agree which happens first.
34
+ */
35
+ interface ChangePlan {
36
+ deletes?: ReadonlyArray<PlanDelete>;
37
+ inserts?: ReadonlyArray<PlanInsert>;
38
+ patches?: ReadonlyArray<PlanPatch>;
39
+ }
40
+ /**
41
+ * The `ctx.db` methods {@link applyPlanToDb} needs — structural, so any writer
42
+ * satisfies it.
43
+ *
44
+ * The `never` parameter positions are deliberate, not laziness. `ctx.db.delete` is
45
+ * `<T extends string>(table: T, id: Id<T>) => …` over the **branded** `Id<T>`, and under
46
+ * `strictFunctionTypes` a parameter is contravariant — so declaring `id: string` here
47
+ * would make the real `ctx.db` un-assignable to `PlanWriter` (`string` is not
48
+ * assignable to `string & { __table }`), and every caller would need a cast at the
49
+ * call site instead. `never` accepts any branded id, which keeps `applyPlanToDb(ctx.db,
50
+ * plan)` cast-free for the caller and confines the two `as never` casts to this module.
51
+ *
52
+ * The trade-off is real: argument checking inside `applyPlanToDb` is erased, so a plan
53
+ * naming a table the schema doesn't have is a runtime error. Use `ctx.db.asId(table,
54
+ * id)` when building the plan to catch a malformed id at the boundary.
55
+ */
56
+ interface PlanWriter {
57
+ delete: (table: never, id: never) => Promise<void>;
58
+ insert: (tableName: never, document: Record<string, unknown>, options?: {
59
+ clientId?: string;
60
+ }) => Promise<unknown>;
61
+ patch: (table: never, id: never, patch: Record<string, unknown>) => Promise<void>;
62
+ }
63
+ /**
64
+ * Apply `plan` to a map of TanStack collections — the client (optimistic) half.
65
+ *
66
+ * A plan naming a table with no wired collection is **skipped, not an error**: an app
67
+ * legitimately syncs a subset of the tables its mutators write (a server-only audit
68
+ * row has no client collection), and throwing would make the optimistic body fail on
69
+ * a write the server handles fine.
70
+ */
71
+ declare const applyPlanToCollections: (collections: Record<string, Collection<Row, string>>, plan: ChangePlan) => void;
72
+ /**
73
+ * Apply `plan` to a `ctx.db` writer — the server (authoritative) half.
74
+ *
75
+ * Sequential by design: the shard's SQLite is single-threaded and a mutation runs
76
+ * inside one BEGIN/COMMIT span, so ordering is observable and a mid-plan failure
77
+ * rolls the whole plan back. An insert carrying an `_id` forwards it as `clientId`,
78
+ * which is how a client-minted key becomes the persisted primary key.
79
+ */
80
+ declare const applyPlanToDb: (db: PlanWriter, plan: ChangePlan) => Promise<void>;
7
81
  declare const VERSION = "0.0.0";
8
- export { VERSION };
82
+ export {
83
+ /**
84
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
85
+ *
86
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
87
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
88
+ * offline-transactions outbox (writes) in one declaration. The lower-level
89
+ * helpers are exported for testing and advanced composition.
90
+ *
91
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
92
+ * consuming app pins their versions and supplies React bindings
93
+ * (`@tanstack/react-db`) itself.
94
+ */
95
+ type ChangePlan,
96
+ /**
97
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
98
+ *
99
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
100
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
101
+ * offline-transactions outbox (writes) in one declaration. The lower-level
102
+ * helpers are exported for testing and advanced composition.
103
+ *
104
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
105
+ * consuming app pins their versions and supplies React bindings
106
+ * (`@tanstack/react-db`) itself.
107
+ */
108
+ type PlanDelete,
109
+ /**
110
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
111
+ *
112
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
113
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
114
+ * offline-transactions outbox (writes) in one declaration. The lower-level
115
+ * helpers are exported for testing and advanced composition.
116
+ *
117
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
118
+ * consuming app pins their versions and supplies React bindings
119
+ * (`@tanstack/react-db`) itself.
120
+ */
121
+ type PlanInsert,
122
+ /**
123
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
124
+ *
125
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
126
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
127
+ * offline-transactions outbox (writes) in one declaration. The lower-level
128
+ * helpers are exported for testing and advanced composition.
129
+ *
130
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
131
+ * consuming app pins their versions and supplies React bindings
132
+ * (`@tanstack/react-db`) itself.
133
+ */
134
+ type PlanPatch,
135
+ /**
136
+ * `@lunora/db` — a TanStack DB binding for the Lunora client.
137
+ *
138
+ * `defineCollections(client, { … })` wires a set of Lunora tables into live,
139
+ * auto-indexed TanStack DB collections (reads) plus a durable, retried
140
+ * offline-transactions outbox (writes) in one declaration. The lower-level
141
+ * helpers are exported for testing and advanced composition.
142
+ *
143
+ * Peer-depends on `@tanstack/db` and `@tanstack/offline-transactions`; the
144
+ * consuming app pins their versions and supplies React bindings
145
+ * (`@tanstack/react-db`) itself.
146
+ */
147
+ type PlanWriter, type Row, VERSION, applyPlanToCollections, applyPlanToDb };
package/dist/index.mjs CHANGED
@@ -1,8 +1 @@
1
- export { createCheckpointRegistry, lunoraCollectionOptions } from './packem_shared/createCheckpointRegistry-BtIEm2Kh.mjs';
2
- export { defineCollections } from './packem_shared/defineCollections-C_F-42rS.mjs';
3
- export { bindMutators, defineMutator } from './packem_shared/bindMutators-B_RaNgel.mjs';
4
- export { OUTBOX_MUTATION_FN_NAME, createExecutorOutboxSink, createOptimisticOnlineDetector, makeDiffEmit, runOutboxMutation, toMap } from './packem_shared/OUTBOX_MUTATION_FN_NAME-9fqK7rND.mjs';
5
-
6
- const VERSION = "0.0.0";
7
-
8
- export { VERSION };
1
+ import{applyPlanToCollections as r,applyPlanToDb as i}from"./packem_shared/applyPlanToCollections-DRwloPsc.mjs";import{CHECKPOINT_FALLBACK_MS as a,createCheckpointRegistry as p,getShardCheckpoints as c,lunoraCollectionOptions as l,releaseShardCheckpoints as s}from"./packem_shared/CHECKPOINT_FALLBACK_MS-Dnkx44kO.mjs";import{defineCollections as O}from"./packem_shared/defineCollections-D4Y0GiBS.mjs";import{DIRECT_TRANSACTION_METADATA_KEY as f,bindMutators as u,defineMutator as x,initMutators as A}from"./packem_shared/DIRECT_TRANSACTION_METADATA_KEY-BGf7W7d4.mjs";import{OUTBOX_MUTATION_FN_NAME as m,createExecutorOutboxSink as E,createOptimisticOnlineDetector as _,makeDiffEmit as N,runOutboxMutation as S,toMap as d}from"./packem_shared/OUTBOX_MUTATION_FN_NAME-rGyegnLw.mjs";const t="0.0.0";export{a as CHECKPOINT_FALLBACK_MS,f as DIRECT_TRANSACTION_METADATA_KEY,m as OUTBOX_MUTATION_FN_NAME,t as VERSION,r as applyPlanToCollections,i as applyPlanToDb,u as bindMutators,p as createCheckpointRegistry,E as createExecutorOutboxSink,_ as createOptimisticOnlineDetector,O as defineCollections,x as defineMutator,c as getShardCheckpoints,A as initMutators,l as lunoraCollectionOptions,N as makeDiffEmit,s as releaseShardCheckpoints,S as runOutboxMutation,d as toMap};
@@ -1,5 +1,5 @@
1
- export { type B as BindMutatorsContext, type a as BoundMutators, type C as ClientMutatorContext, type b as ClientMutatorDef, c as bindMutators, d as defineMutator } from "../packem_shared/define-mutators.d-DTU85KL6.mjs";
1
+ export { type B as BindMutatorsContext, type a as BoundMutatorApi, type b as BoundMutators, type C as ClientMutatorContext, type c as ClientMutatorDef, type d as CollectionMap, type e as MutatorRejectedEvent, f as bindMutators, g as defineMutator, i as initMutators } from "../packem_shared/index.d-9usNsVAH.mjs";
2
2
  import '@lunora/client';
3
3
  import '@tanstack/db';
4
- import "../packem_shared/collection-options.d-BPVkj5FW.mjs";
4
+ import "../packem_shared/collection-options.d-C2FdJw06.mjs";
5
5
  import '@tanstack/offline-transactions';
@@ -1,5 +1,5 @@
1
- export { type B as BindMutatorsContext, type a as BoundMutators, type C as ClientMutatorContext, type b as ClientMutatorDef, c as bindMutators, d as defineMutator } from "../packem_shared/define-mutators.d-DukVT0Y3.js";
1
+ export { type B as BindMutatorsContext, type a as BoundMutatorApi, type b as BoundMutators, type C as ClientMutatorContext, type c as ClientMutatorDef, type d as CollectionMap, type e as MutatorRejectedEvent, f as bindMutators, g as defineMutator, i as initMutators } from "../packem_shared/index.d-t0PhXOHV.js";
2
2
  import '@lunora/client';
3
3
  import '@tanstack/db';
4
- import "../packem_shared/collection-options.d-BPVkj5FW.js";
4
+ import "../packem_shared/collection-options.d-C2FdJw06.js";
5
5
  import '@tanstack/offline-transactions';
@@ -1 +1 @@
1
- export { bindMutators, defineMutator } from '../packem_shared/bindMutators-B_RaNgel.mjs';
1
+ import{bindMutators as r,defineMutator as i,initMutators as a}from"../packem_shared/DIRECT_TRANSACTION_METADATA_KEY-BGf7W7d4.mjs";export{r as bindMutators,i as defineMutator,a as initMutators};
@@ -0,0 +1 @@
1
+ import{LunoraError as K}from"@lunora/errors";import{BTreeIndex as W}from"@tanstack/db";import{toMap as x,makeDiffEmit as O}from"./OUTBOX_MUTATION_FN_NAME-rGyegnLw.mjs";const $=()=>{let e;const o=new Set;return{setError:t=>{if(e!==t){e=t;for(const r of o)r()}},utils:{error:()=>e,isError:()=>e!==void 0,subscribeError:t=>(o.add(t),()=>{o.delete(t)})}}},T=()=>{let e=Number.NEGATIVE_INFINITY;const o=[];return{advance:t=>{if(!(t<=e)){e=t;for(let r=o.length-1;r>=0;r-=1){const i=o[r];i&&i.threshold<=e&&(i.resolve(),o.splice(r,1))}}},await:t=>t<=e?Promise.resolve():new Promise(r=>{o.push({resolve:r,threshold:t})}),passed:t=>t<=e,reset:()=>{for(const t of o.splice(0))t.resolve();e=Number.NEGATIVE_INFINITY},waiting:()=>o.length}};let F=!1;const G=e=>{F||(F=!0,console.warn(`[@lunora/db] released an optimistic overlay via the ${String(e.waitedMs)}ms checkpoint fallback: the server confirmed ${e.kind} ${String(e.watermark)} but no sync frame ever echoed it. A dropped shape poke or \`settled\` frame is the usual cause — inspect the subscription rather than raising \`fallbackMs\`. (Reported once per process.)`))},g=new WeakMap,R=new WeakMap,y=new WeakMap,C=new WeakMap,L=e=>{const o=(y.get(e)??0)-1;if(o>0){y.set(e,o);return}y.delete(e),C.get(e)?.()},A=new WeakSet,P=e=>{const o={acknowledge:t=>{e().acknowledge(t)},awaitCheckpoint:async t=>e().awaitCheckpoint(t),awaitMutationId:async t=>e().awaitMutationId(t),dispose:()=>{e().dispose()},resolve:t=>{e().resolve(t)},stats:()=>e().stats()};return A.add(o),o},V=3e3,Y=(e={})=>{const o=e.fallbackMs??V,t=e.onFallback??G,r=T(),i=T();let l=0;const d=new Set,f=(s,n)=>{for(const c of d)c.kind===s&&c.threshold<=n&&(clearTimeout(c.handle),d.delete(c))},u=(s,n)=>{if(o<=0)return;const c=s==="checkpoint"?r:i;if(c.passed(n))return;for(const p of d)if(p.kind===s&&p.threshold>=n)return;const k={handle:setTimeout(()=>{if(d.delete(k),!c.passed(n)){l+=1,c.advance(n);try{t({kind:s,waitedMs:o,watermark:n})}catch{}}},o),kind:s,threshold:n};d.add(k)},v={acknowledge:({checkpoint:s,mutationId:n})=>{s!==void 0&&u("checkpoint",s),n!==void 0&&u("mutationId",n)},awaitCheckpoint:s=>r.await(s),awaitMutationId:s=>i.await(s),dispose:()=>{for(const s of d)clearTimeout(s.handle);d.clear()},resolve:({checkpoint:s,mutationId:n})=>{s!==void 0&&(r.advance(s),f("checkpoint",s)),n!==void 0&&(i.advance(n),f("mutationId",n))},stats:()=>({fallbacks:l,pendingCheckpointWaiters:r.waiting(),pendingMutationWaiters:i.waiting()})};return C.set(v,()=>{v.dispose(),r.reset(),i.reset()}),v},z=e=>{const o=g.get(e);if(o){for(const t of o.values())t.resolve({checkpoint:Number.POSITIVE_INFINITY,mutationId:Number.POSITIVE_INFINITY}),t.dispose();g.delete(e)}},D=e=>{const o=e.currentIdentity();if(g.has(e)&&R.get(e)!==o)for(const t of g.get(e)?.values()??[])C.get(t)?.();R.set(e,o)},H=(e,o,t)=>{D(e);let r=g.get(e);r||(r=new Map,g.set(e,r));const i=o??"",l=r.get(i);if(l)return l;const d=Y(t);return r.set(i,d),d},U=(e,o,t)=>{const r=o[e];if(typeof r=="string"||typeof r=="number")return String(r);throw new K("BAD_REQUEST",`@lunora/db: ${t} has no "${e}" — a \`scopeBy\` collection routes by that field's value`)},X=e=>A.has(e),J=e=>{y.set(e,(y.get(e)??0)+1)},Z=e=>(y.get(e)??0)>0,ee=e=>{if(e.list===void 0==(e.shape===void 0))throw new K("INTERNAL","lunoraCollectionOptions: pass exactly one of `list` or `shape`");const o=e.getKey??(a=>a._id),t=e.shape?.shardKey??e.shardKey,r=e.scopeBy!==void 0&&t===void 0;let i;const l=()=>t??i,d=()=>e.checkpoints??H(e.client,l());let f;const u=()=>{const a=d();return a!==f&&(f=a,J(a)),a},v=()=>{f!==void 0&&e.checkpoints===void 0&&L(f),f=void 0};r||u();const s=new Map;let n,c,k;const{setError:p,utils:E}=$();let b,I;const S=(a,m)=>{u();const w=h=>{p(void 0),n?.(x(h,o)),m?.(),e.shape===void 0&&(u().resolve({mutationId:I??e.client.confirmedMutationWatermark(l())}),I=void 0)},M=h=>k?.(h),N=h=>{h.rowsFollow===!0&&h.mutationId!==void 0&&(I=h.mutationId),u().resolve(h)};return e.shape!==void 0?e.client.subscribeShape({args:a,name:e.shape.name},w,{onCheckpoint:N,onError:M,shardKey:l()}):e.client.subscribe(e.list,a,w,{onCheckpoint:N,onError:M,shardKey:l()})},_={autoIndex:"eager",defaultIndexType:W,getKey:o,id:e.id??e.list?.__lunoraRef??`shape:${e.shape?.name??""}`,utils:E,...e.load==="eager"?{startSync:!0}:{},sync:{sync:a=>(n=O(s,a),k=w=>{p(w),a.markReady(),e.onError?.(w)},e.scopeBy===void 0?c=S(e.shape?.args??{},()=>{a.markReady()}):(a.markReady(),b!==void 0&&(c=S(b,void 0))),()=>{n=void 0,k=void 0,p(void 0),v(),c?.(),c=void 0,s.clear()})}},B=a=>{if(e.scopeBy===void 0)return;const m=r&&a!==void 0?U(e.scopeBy,a,"scope args"):i;r&&(a===void 0||m!==i)&&v(),b=a,i=m,c?.(),c=void 0,p(void 0),I=void 0,n?.(new Map),a!==void 0&&n!==void 0&&(c=S(a,void 0))};return{checkpoints:r&&e.checkpoints===void 0?P(d):d(),config:_,scope:B,utils:E}};export{V as CHECKPOINT_FALLBACK_MS,Y as createCheckpointRegistry,H as getShardCheckpoints,Z as hasCheckpointsAttached,X as isScopeFollowingCheckpoints,ee as lunoraCollectionOptions,J as markCheckpointsAttached,z as releaseShardCheckpoints,U as scopedShardKey,D as syncShardCheckpointIdentity};
@@ -0,0 +1 @@
1
+ import{isAuthReplayFailure as C}from"@lunora/client";import{LunoraError as d}from"@lunora/errors";import{createTransaction as E}from"@tanstack/db";import{isScopeFollowingCheckpoints as R,scopedShardKey as A,getShardCheckpoints as I,hasCheckpointsAttached as T,syncShardCheckpointIdentity as g}from"./CHECKPOINT_FALLBACK_MS-Dnkx44kO.mjs";import{assertSecureRandom as b,runOutboxMutation as N,toNonRetriable as B}from"./OUTBOX_MUTATION_FN_NAME-rGyegnLw.mjs";const O="__tanstack_db_direct",F="defineMutator: `serverRef` must be a generated mutator reference (api.mutators.*) or a 'namespace:fn' string",j=r=>{const e=typeof r=="string"?r:r?.__lunoraRef;if(typeof e!="string"||e.length===0)throw new d("INTERNAL",F);return e},q=r=>({__lunoraClientMutator:!0,apply:r.apply,serverRef:j(r.serverRef)}),D=(r,e,k)=>{b("bindMutators");const h=32;if(e.checkpoints&&R(e.checkpoints)&&e.scopeBy===void 0&&e.shardKey===void 0)throw new d("BAD_REQUEST","bindMutators: `checkpoints` from a `scopeBy` collection needs the same `scopeBy` (or a pinned `shardKey`) here, so each push reaches the shard it gates on");const w=t=>e.shardKey??(e.scopeBy===void 0?void 0:A(e.scopeBy,t,"the mutator args")),u=new Map;let m=r.currentIdentity();const f=()=>{g(r);const t=r.currentIdentity();t!==m&&(u.clear(),m=t)},v=t=>{f();const o=Math.max(u.get(t??"")??0,r.confirmedMutationWatermark(t))+1;return u.set(t??"",o),o};let l=Promise.resolve();const M=(t,o,n)=>{const c=l.then(async()=>{for(let i=0;;i+=1){const a=v(n);try{const{applied:s}=await r.callMutator(t,o,{clientSeq:a,shardKey:n});if(s)return a}catch(s){throw f(),u.set(n??"",r.confirmedMutationWatermark(n)),s}if(i>=h)throw new d("INTERNAL",`lunora: custom mutator "${t}" could not claim a fresh client sequence after ${String(h)} attempts`)}});return l=c.then(()=>{},()=>{}),c},_=t=>{if(e.checkpoints===!1)return;if(e.checkpoints&&!R(e.checkpoints))return e.checkpoints;const o=I(r,t);return T(o)?o:void 0},S=(t,o,n,c)=>{const{onWriteRejected:i}=e;i&&t.isPersisted.promise.catch(a=>{const s=a instanceof Error?a:new Error(String(a));try{i({args:c,code:s.code,error:s,mutator:o,serverRef:n})}catch{}})},y={};for(const[t,o]of Object.entries(k))y[t]=n=>{const c=w(n),i=E({autoCommit:!0,metadata:{[O]:!0,serverRef:o.serverRef},mutationFn:async()=>{let a=0;try{await N(async()=>{a=await M(o.serverRef,n,c)})}catch(p){throw C(p)?B(p):p}const s=_(c);s&&(s.acknowledge({mutationId:a}),await s.awaitMutationId(a))}});return i.mutate(()=>{o.apply({collections:e.collections},n)}),S(i,t,o.serverRef,n),i};return y},z=()=>({bindMutators:D,defineMutator:q});export{O as DIRECT_TRANSACTION_METADATA_KEY,D as bindMutators,q as defineMutator,z as initMutators};
@@ -0,0 +1 @@
1
+ import{isAuthReplayFailure as p}from"@lunora/client";import{createCollection as b,safeRandomUUID as h}from"@tanstack/db";import{NonRetriableError as s}from"@tanstack/offline-transactions";const g=1e3,I=e=>{const t=globalThis.crypto;if(!(typeof t?.randomUUID=="function"||typeof t?.getRandomValues=="function"))throw new Error(`${e}: this runtime has no WebCrypto (\`crypto.randomUUID\` / \`crypto.getRandomValues\`), which every optimistic write needs to mint its client-side id. On React Native / Expo, install \`expo-crypto\` (or \`react-native-get-random-values\`) and import it ONCE at the top of your entry file, before anything that reaches \`@lunora/db\` or \`@tanstack/db\`.`)},u="__lunora_outbox__",m=new WeakMap,R=()=>b({getKey:e=>e._id,id:`${u}:${h()}`,startSync:!0,sync:{sync:e=>(e.markReady(),()=>{})}}),_=(e,t)=>{m.set(e,t)};let f=!1;const c=new Map,O=e=>{e.onRejected&&c.set(e.idempotencyKey,e.onRejected)},v=e=>{const t=c.get(e);return c.delete(e),t},N=(e,t={})=>{const o=t.maxItems??1e3,a=t.mutationFnName??u;return{pending:()=>e.getPendingCount()>0,enqueue(n){if(e.getPendingCount()>=o){const d=new Error("offline outbox is full");return d.code="OFFLINE_QUEUE_OVERFLOW",Promise.reject(d)}O(n);const r={args:n.args,baselineSeq:n.baselineSeq,clientId:n.clientId,functionPath:n.functionPath,idempotencyKey:n.idempotencyKey,identity:n.identity,mutationId:n.mutationId,shardKey:n.shardKey},i=m.get(e);!i&&!f&&(f=!0,console.warn("[@lunora/db] createExecutorOutboxSink: no outbox carrier is registered for this executor. On a TanStack OfflineExecutor a zero-mutation transaction is silently dropped, so offline writes may be lost — wire the sink to the executor returned by defineCollections()."));const l=e.createOfflineTransaction({autoCommit:!1,idempotencyKey:n.idempotencyKey,metadata:r,mutationFnName:a});return l.mutate(()=>{i?.insert({_id:n.idempotencyKey})}),l.commit?.().catch(()=>{}),Promise.resolve()}}},k=(e,t)=>new Map(e.map(o=>[t(o),o])),C=(e,t)=>o=>{t.begin();const a=new Map;for(const[n,r]of o){const i=JSON.stringify(r);a.set(n,i),e.has(n)?e.get(n)!==i&&t.write({type:"update",value:r}):t.write({type:"insert",value:r})}for(const n of e.keys())o.has(n)||t.write({key:n,type:"delete"});t.commit(),e.clear();for(const[n,r]of a)e.set(n,r)},y=e=>{if(e instanceof s)return e;const t=new s(e instanceof Error?e.message:String(e)),{code:o}=e;return typeof o=="string"&&(t.code=o),t},S=async e=>{try{await e()}catch(t){throw t.code==="IDENTITY_MISMATCH"||p(t)?t:typeof t.code=="string"?y(t):t}},M=e=>{if(e instanceof s)return e;const t=e instanceof Error?e:new Error(String(e));if(t.name==="AbortError"||["400","401","403","422"].some(o=>t.message.includes(o)))return y(e)},T=()=>{const e=new Set;return{dispose:()=>{for(const t of e)clearInterval(t);e.clear()},isOnline:()=>!0,notifyOnline:()=>{},subscribe:t=>{const o=setInterval(t,g);return e.add(o),()=>{clearInterval(o),e.delete(o)}}}};export{u as OUTBOX_MUTATION_FN_NAME,I as assertSecureRandom,N as createExecutorOutboxSink,T as createOptimisticOnlineDetector,R as createOutboxCarrier,C as makeDiffEmit,_ as registerOutboxCarrier,S as runOutboxMutation,v as takeOutboxRejection,M as terminalVerdict,k as toMap,y as toNonRetriable};
@@ -0,0 +1 @@
1
+ const h=t=>{if(t===null||typeof t!="object")return!1;const n=Object.getPrototypeOf(t);return n===null||n===Object.prototype};class a extends Error{}const p=t=>t.includes("."),O=t=>{const n=t.split(".");if(n.length>4||n.includes(""))throw new a(`field path "${t}" must have 1 to ${String(4)} non-empty segments`);return n},b=(t,n,e,s)=>{const[o,...r]=n;if(o===void 0)throw new a(`field path "${s}" is empty`);if(t!=null&&!h(t))throw new a(`cannot set "${s}": "${o}" sits inside a value that is not an object`);const i=t??{};return{...i,[o]:r.length===0?e:b(i[o],r,e,s)}},$=(t,n)=>{const e=Object.keys(t).filter(o=>p(o));if(e.length===0)return t;for(const o of e){const r=e.find(i=>i!==o&&i.startsWith(`${o}.`));if(r!==void 0)throw new a(`patch sets both "${o}" and "${r}", which overlap; set one or the other`)}const s={};for(const[o,r]of Object.entries(t))p(o)||(s[o]=r);for(const o of e){const[r="",...i]=O(o);if(Object.hasOwn(t,r))throw new a(`patch sets both "${r}" and "${o}"; set one or the other`);s[r]=b(Object.hasOwn(s,r)?s[r]:n[r],i,t[o],o)}return s},f={decrement:{apply:(t,n)=>t-n,kind:"number"},divide:{apply:(t,n)=>t/n,kind:"number"},increment:{apply:(t,n)=>t+n,kind:"number"},multiply:{apply:(t,n)=>t*n,kind:"number"},toggle:{kind:"boolean"}};class c extends Error{}const y=t=>{if(!h(t))return;const n=Object.keys(t),[e]=n;if(!(n.length!==1||e===void 0||!Object.hasOwn(f,e)))return{operand:t[e],operator:e}},w=t=>y(t)!==void 0,g=(t,n,e,s)=>{const o=f[n];if(o.apply===void 0){if(e!==!0)throw new c(`patch: use { toggle: true } on "${t}".`);if(typeof s!="boolean")throw new c(`patch: cannot toggle "${t}" — its current value is ${String(s)}; set a boolean first.`);return!s}if(typeof e!="number"||!Number.isFinite(e))throw new c(`patch: "${n}" on "${t}" needs a finite number, got ${String(e)}.`);if(typeof s!="number"||!Number.isFinite(s))throw new c(`patch: cannot ${n} "${t}" — its current value is ${String(s)}; set a number first.`);if(n==="divide"&&e===0)throw new c(`patch: cannot divide "${t}" by zero.`);const r=o.apply(s,e);if(!Number.isFinite(r))throw new c(`patch: "${n}" on "${t}" overflows to ${String(r)}.`);return r},u=(t,n)=>{const e=new Set;for(const[s,o]of Object.entries(t))n(s)!==void 0&&w(o)&&e.add(s);return e},m=(t,n,e)=>{let s;for(const o of u(t,e)){const{operand:r,operator:i}=y(t[o]),l=e(o);if(f[i].kind!==l)throw new c(`patch: "${i}" does not apply to the ${String(l)} column "${o}".`);s??={...t},s[o]=g(o,i,r,n[o])}return s??t},v=(t,n)=>{const e=o=>{const r=n[o];return typeof r=="number"||typeof r=="boolean"?typeof r:void 0},s={...t};for(const o of u(t,e))try{Object.assign(s,m({[o]:t[o]},n,e))}catch(r){if(!(r instanceof c))throw r}try{return $(s,n)}catch(o){if(o instanceof a)return s;throw o}},P=(t,n)=>{for(const e of n.deletes??[])t[e.table]?.delete(e.id);for(const e of n.patches??[]){const s=t[e.table];s&&s.update(e.id,o=>{const r=Object.entries(v(e.fields,o)).filter(([i,l])=>{const d=o[i];return!w(l)||typeof d=="object"&&d!==null});Object.assign(o,Object.fromEntries(r))})}for(const e of n.inserts??[]){const s=t[e.table];if(s){if(typeof e.row._id!="string")throw new TypeError(`applyPlanToCollections: insert into "${e.table}" needs an "_id" — mint it client-side so the optimistic row keys match the persisted one`);s.insert(e.row)}}},j=async(t,n)=>{for(const e of n.deletes??[])await t.delete(e.table,e.id);for(const e of n.patches??[])await t.patch(e.table,e.id,e.fields);for(const e of n.inserts??[]){const{_id:s,...o}=e.row;await t.insert(e.table,o,...typeof s=="string"?[{clientId:s}]:[])}};export{P as applyPlanToCollections,j as applyPlanToDb};