@lunora/db 1.0.0-alpha.3 → 1.0.0-alpha.30

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.
@@ -0,0 +1,148 @@
1
+ import { LunoraClient } from '@lunora/client';
2
+ import { Collection, Transaction } from '@tanstack/db';
3
+ import { C as CheckpointRegistry, R as Row } from "./collection-options.d-C03Rjjxn.js";
4
+ /**
5
+ * TanStack DB's "direct transaction" marker.
6
+ *
7
+ * A completed transaction's optimistic rows are discarded as **stale** unless
8
+ * either a synced transaction for the same key is already queued, or the
9
+ * transaction carried this flag (`CollectionStateManager.recomputeOptimisticState`
10
+ * → `pendingOptimisticDirectUpserts`). Marked rows instead survive until a sync
11
+ * operation for that key actually lands, which is precisely the semantics a Lunora
12
+ * custom mutator needs: the server is the linearization point, so the prediction
13
+ * must stay visible until the authoritative row arrives. Without it, a text edit
14
+ * visibly reverts to the last synced value the moment the push is acked.
15
+ *
16
+ * The literal is pinned here because `@tanstack/db` does not re-export the constant
17
+ * from its package root (it lives in the unexported
18
+ * `collection/transaction-metadata` module). `__tests__/define-mutators.test.ts`
19
+ * reads that module off disk and fails if the upstream value ever changes.
20
+ */
21
+ declare const DIRECT_TRANSACTION_METADATA_KEY = "__tanstack_db_direct";
22
+ /** The local store a client mutator's optimistic body writes against. */
23
+ interface ClientMutatorContext {
24
+ /** The wired collections, keyed by name — apply optimistic inserts/updates/deletes here. */
25
+ collections: Record<string, Collection<Row, string>>;
26
+ }
27
+ /**
28
+ * A generated mutator reference (`api.mutators.sendMessage`), accepted by
29
+ * {@link defineMutator} in place of a hand-written path string.
30
+ *
31
+ * Declared structurally rather than imported from `@lunora/client` so this module
32
+ * keeps its narrow dependency surface; the shape matches `FunctionReference`, and
33
+ * the phantom marker carries the server mutator's arg type so the client body's
34
+ * args are **inferred** instead of restated.
35
+ */
36
+ interface MutatorReference<TArgs = unknown> {
37
+ readonly __lunoraPhantom?: {
38
+ args: TArgs;
39
+ kind: unknown;
40
+ returns: unknown;
41
+ };
42
+ readonly __lunoraRef: string;
43
+ }
44
+ /** Args type carried by a {@link MutatorReference}. */
45
+ type ArgsOfReference<R> = R extends MutatorReference<infer A> ? A : never;
46
+ /** A client-side custom mutator: an optimistic body plus the path of its authoritative server impl. */
47
+ interface ClientMutatorDef<TArgs> {
48
+ /** Brand so codegen / `bindMutators` can recognize a mutator definition. */
49
+ __lunoraClientMutator: true;
50
+ /** The optimistic update applied to the local collections before the server confirms. */
51
+ apply: (context: ClientMutatorContext, args: TArgs) => void;
52
+ /** The Lunora function path of the server-authoritative mutator (`defineMutator` on the server). */
53
+ serverRef: string;
54
+ }
55
+ /**
56
+ * Declare a client-side custom mutator. `apply` runs optimistically against the
57
+ * local TanStack collections; `serverRef` names the authoritative server mutator
58
+ * the write is pushed to over the watermark protocol. The server impl is the
59
+ * linearization point — this body is a prediction the server can override.
60
+ *
61
+ * **Pass a generated reference, not a string.** `serverRef: api.mutators.sendMessage`
62
+ * both binds the path at compile time — a rename, a typo, or a moved file becomes a
63
+ * type error instead of a mutation that silently fails at runtime — and **infers
64
+ * `TArgs` from the server mutator's own validators**, so the arg type is declared
65
+ * once on the server rather than restated in every client body:
66
+ *
67
+ * ```ts
68
+ * // Typed + checked: args inferred from the server mutator.
69
+ * defineMutator({
70
+ * apply: ({ collections }, args) => { … }, // args: { channelId: Id<"channels">; text: string }
71
+ * serverRef: api.mutators.sendMessage,
72
+ * });
73
+ *
74
+ * // Escape hatch: a path string still works, but nothing checks it and you must
75
+ * // restate the args yourself.
76
+ * defineMutator<{ text: string }>({ apply, serverRef: "mutators:sendMessage" });
77
+ * ```
78
+ */
79
+ declare const defineMutator: {
80
+ <TArgs = Record<string, unknown>>(definition: {
81
+ apply: (context: ClientMutatorContext, args: TArgs) => void;
82
+ serverRef: string;
83
+ }): ClientMutatorDef<TArgs>;
84
+ <R extends MutatorReference<never>>(definition: {
85
+ apply: (context: ClientMutatorContext, args: ArgsOfReference<R>) => void;
86
+ serverRef: R;
87
+ }): ClientMutatorDef<ArgsOfReference<R>>;
88
+ };
89
+ type AnyMutatorMap = Record<string, ClientMutatorDef<any>>;
90
+ /** Args type of a mutator definition. */
91
+ type ArgsOf<M> = M extends ClientMutatorDef<infer A> ? A : never;
92
+ /** Inputs `bindMutators` needs to run a mutator: the local store + how the overlay drops. */
93
+ interface BindMutatorsContext {
94
+ /**
95
+ * Resolves the optimistic-overlay drop against confirmed server watermarks: a
96
+ * mutation's overlay is held until the sync stream echoes
97
+ * `lastMutationId >= clientSeq` (via {@link CheckpointRegistry.resolve}), so the
98
+ * row never flashes out and back.
99
+ *
100
+ * Defaults to the shared per-shard registry for `client` + {@link shardKey}
101
+ * ({@link getShardCheckpoints}) — the same one
102
+ * {@link import("./collection-options").lunoraCollectionOptions} defaults to, so
103
+ * a shard's collections and its mutators gate on one watermark line without the
104
+ * caller wiring them together. Pass `false` to drop the overlay as soon as the
105
+ * server accepts the write (the by-value sync diff then converges the synced row
106
+ * in place).
107
+ */
108
+ checkpoints?: CheckpointRegistry | false;
109
+ /** The wired collections the optimistic bodies write against. */
110
+ collections: Record<string, Collection<Row, string>>;
111
+ /** Optional shard key the mutator's server push is routed to. */
112
+ shardKey?: string;
113
+ }
114
+ /** Calling a bound mutator runs the optimistic body + pushes the server write; returns the TanStack transaction. */
115
+ type BoundMutators<M extends AnyMutatorMap> = { [K in keyof M]: (args: ArgsOf<M[K]>) => Transaction; };
116
+ /**
117
+ * Bind a set of client mutators to a client + local store. Each returned handle,
118
+ * when called, opens a TanStack optimistic transaction: the mutator's `apply`
119
+ * body writes the predicted rows into the collections, and the transaction's
120
+ * `mutationFn` pushes the authoritative write through
121
+ * {@link LunoraClient.callMutator} under a monotonic per-client `clientSeq`.
122
+ *
123
+ * Rebase-on-poke is free — TanStack DB re-derives every pending optimistic overlay
124
+ * over the latest synced base on each sync tick. The overlay is dropped when the
125
+ * server confirms the write (and, if `checkpoints` is supplied, once it echoes the
126
+ * matching watermark so the synced row has landed).
127
+ *
128
+ * The `clientSeq` generator is seeded from the server's echoed watermark
129
+ * ({@link LunoraClient.confirmedMutationWatermark}) on every issue, so a reload —
130
+ * which resets this in-memory counter while the server keeps a durable per-client
131
+ * watermark — never reissues a sequence the DO has already applied. As a backstop
132
+ * for the very first push of a fresh session (before any ack has taught the client
133
+ * the watermark), a push the DO swallows as a replay (`applied === false`) is
134
+ * reissued above the now-known watermark instead of being mistaken for a confirmed
135
+ * write — closing the silent-drop window without risking a double-apply (a fresh
136
+ * session's first stale push provably cannot be an honest replay).
137
+ *
138
+ * Pushes are **serialized per binding** (a FIFO chain): the DO rejects any push
139
+ * with `clientSeq > watermark + 1` as `OUT_OF_ORDER` and drops the write, so two
140
+ * mutators fired concurrently must not race the network into a gap. Each push
141
+ * waits for the previous one's ack and assigns its `clientSeq` *inside* the
142
+ * critical section — from the live watermark — so the sequence is always exactly
143
+ * `watermark + 1`. Because a failed mutation never advances the server watermark,
144
+ * a permanently-rejected predecessor can't wedge the chain: the next push simply
145
+ * reclaims the same `watermark + 1` instead of leaving a hole the DO waits on.
146
+ */
147
+ declare const bindMutators: <M extends AnyMutatorMap>(client: LunoraClient, context: BindMutatorsContext, mutators: M) => BoundMutators<M>;
148
+ export { BindMutatorsContext as B, ClientMutatorContext as C, DIRECT_TRANSACTION_METADATA_KEY as D, MutatorReference as M, BoundMutators as a, ClientMutatorDef as b, bindMutators as c, defineMutator as d };
@@ -0,0 +1,148 @@
1
+ import { LunoraClient } from '@lunora/client';
2
+ import { Collection, Transaction } from '@tanstack/db';
3
+ import { C as CheckpointRegistry, R as Row } from "./collection-options.d-C03Rjjxn.mjs";
4
+ /**
5
+ * TanStack DB's "direct transaction" marker.
6
+ *
7
+ * A completed transaction's optimistic rows are discarded as **stale** unless
8
+ * either a synced transaction for the same key is already queued, or the
9
+ * transaction carried this flag (`CollectionStateManager.recomputeOptimisticState`
10
+ * → `pendingOptimisticDirectUpserts`). Marked rows instead survive until a sync
11
+ * operation for that key actually lands, which is precisely the semantics a Lunora
12
+ * custom mutator needs: the server is the linearization point, so the prediction
13
+ * must stay visible until the authoritative row arrives. Without it, a text edit
14
+ * visibly reverts to the last synced value the moment the push is acked.
15
+ *
16
+ * The literal is pinned here because `@tanstack/db` does not re-export the constant
17
+ * from its package root (it lives in the unexported
18
+ * `collection/transaction-metadata` module). `__tests__/define-mutators.test.ts`
19
+ * reads that module off disk and fails if the upstream value ever changes.
20
+ */
21
+ declare const DIRECT_TRANSACTION_METADATA_KEY = "__tanstack_db_direct";
22
+ /** The local store a client mutator's optimistic body writes against. */
23
+ interface ClientMutatorContext {
24
+ /** The wired collections, keyed by name — apply optimistic inserts/updates/deletes here. */
25
+ collections: Record<string, Collection<Row, string>>;
26
+ }
27
+ /**
28
+ * A generated mutator reference (`api.mutators.sendMessage`), accepted by
29
+ * {@link defineMutator} in place of a hand-written path string.
30
+ *
31
+ * Declared structurally rather than imported from `@lunora/client` so this module
32
+ * keeps its narrow dependency surface; the shape matches `FunctionReference`, and
33
+ * the phantom marker carries the server mutator's arg type so the client body's
34
+ * args are **inferred** instead of restated.
35
+ */
36
+ interface MutatorReference<TArgs = unknown> {
37
+ readonly __lunoraPhantom?: {
38
+ args: TArgs;
39
+ kind: unknown;
40
+ returns: unknown;
41
+ };
42
+ readonly __lunoraRef: string;
43
+ }
44
+ /** Args type carried by a {@link MutatorReference}. */
45
+ type ArgsOfReference<R> = R extends MutatorReference<infer A> ? A : never;
46
+ /** A client-side custom mutator: an optimistic body plus the path of its authoritative server impl. */
47
+ interface ClientMutatorDef<TArgs> {
48
+ /** Brand so codegen / `bindMutators` can recognize a mutator definition. */
49
+ __lunoraClientMutator: true;
50
+ /** The optimistic update applied to the local collections before the server confirms. */
51
+ apply: (context: ClientMutatorContext, args: TArgs) => void;
52
+ /** The Lunora function path of the server-authoritative mutator (`defineMutator` on the server). */
53
+ serverRef: string;
54
+ }
55
+ /**
56
+ * Declare a client-side custom mutator. `apply` runs optimistically against the
57
+ * local TanStack collections; `serverRef` names the authoritative server mutator
58
+ * the write is pushed to over the watermark protocol. The server impl is the
59
+ * linearization point — this body is a prediction the server can override.
60
+ *
61
+ * **Pass a generated reference, not a string.** `serverRef: api.mutators.sendMessage`
62
+ * both binds the path at compile time — a rename, a typo, or a moved file becomes a
63
+ * type error instead of a mutation that silently fails at runtime — and **infers
64
+ * `TArgs` from the server mutator's own validators**, so the arg type is declared
65
+ * once on the server rather than restated in every client body:
66
+ *
67
+ * ```ts
68
+ * // Typed + checked: args inferred from the server mutator.
69
+ * defineMutator({
70
+ * apply: ({ collections }, args) => { … }, // args: { channelId: Id<"channels">; text: string }
71
+ * serverRef: api.mutators.sendMessage,
72
+ * });
73
+ *
74
+ * // Escape hatch: a path string still works, but nothing checks it and you must
75
+ * // restate the args yourself.
76
+ * defineMutator<{ text: string }>({ apply, serverRef: "mutators:sendMessage" });
77
+ * ```
78
+ */
79
+ declare const defineMutator: {
80
+ <TArgs = Record<string, unknown>>(definition: {
81
+ apply: (context: ClientMutatorContext, args: TArgs) => void;
82
+ serverRef: string;
83
+ }): ClientMutatorDef<TArgs>;
84
+ <R extends MutatorReference<never>>(definition: {
85
+ apply: (context: ClientMutatorContext, args: ArgsOfReference<R>) => void;
86
+ serverRef: R;
87
+ }): ClientMutatorDef<ArgsOfReference<R>>;
88
+ };
89
+ type AnyMutatorMap = Record<string, ClientMutatorDef<any>>;
90
+ /** Args type of a mutator definition. */
91
+ type ArgsOf<M> = M extends ClientMutatorDef<infer A> ? A : never;
92
+ /** Inputs `bindMutators` needs to run a mutator: the local store + how the overlay drops. */
93
+ interface BindMutatorsContext {
94
+ /**
95
+ * Resolves the optimistic-overlay drop against confirmed server watermarks: a
96
+ * mutation's overlay is held until the sync stream echoes
97
+ * `lastMutationId >= clientSeq` (via {@link CheckpointRegistry.resolve}), so the
98
+ * row never flashes out and back.
99
+ *
100
+ * Defaults to the shared per-shard registry for `client` + {@link shardKey}
101
+ * ({@link getShardCheckpoints}) — the same one
102
+ * {@link import("./collection-options").lunoraCollectionOptions} defaults to, so
103
+ * a shard's collections and its mutators gate on one watermark line without the
104
+ * caller wiring them together. Pass `false` to drop the overlay as soon as the
105
+ * server accepts the write (the by-value sync diff then converges the synced row
106
+ * in place).
107
+ */
108
+ checkpoints?: CheckpointRegistry | false;
109
+ /** The wired collections the optimistic bodies write against. */
110
+ collections: Record<string, Collection<Row, string>>;
111
+ /** Optional shard key the mutator's server push is routed to. */
112
+ shardKey?: string;
113
+ }
114
+ /** Calling a bound mutator runs the optimistic body + pushes the server write; returns the TanStack transaction. */
115
+ type BoundMutators<M extends AnyMutatorMap> = { [K in keyof M]: (args: ArgsOf<M[K]>) => Transaction; };
116
+ /**
117
+ * Bind a set of client mutators to a client + local store. Each returned handle,
118
+ * when called, opens a TanStack optimistic transaction: the mutator's `apply`
119
+ * body writes the predicted rows into the collections, and the transaction's
120
+ * `mutationFn` pushes the authoritative write through
121
+ * {@link LunoraClient.callMutator} under a monotonic per-client `clientSeq`.
122
+ *
123
+ * Rebase-on-poke is free — TanStack DB re-derives every pending optimistic overlay
124
+ * over the latest synced base on each sync tick. The overlay is dropped when the
125
+ * server confirms the write (and, if `checkpoints` is supplied, once it echoes the
126
+ * matching watermark so the synced row has landed).
127
+ *
128
+ * The `clientSeq` generator is seeded from the server's echoed watermark
129
+ * ({@link LunoraClient.confirmedMutationWatermark}) on every issue, so a reload —
130
+ * which resets this in-memory counter while the server keeps a durable per-client
131
+ * watermark — never reissues a sequence the DO has already applied. As a backstop
132
+ * for the very first push of a fresh session (before any ack has taught the client
133
+ * the watermark), a push the DO swallows as a replay (`applied === false`) is
134
+ * reissued above the now-known watermark instead of being mistaken for a confirmed
135
+ * write — closing the silent-drop window without risking a double-apply (a fresh
136
+ * session's first stale push provably cannot be an honest replay).
137
+ *
138
+ * Pushes are **serialized per binding** (a FIFO chain): the DO rejects any push
139
+ * with `clientSeq > watermark + 1` as `OUT_OF_ORDER` and drops the write, so two
140
+ * mutators fired concurrently must not race the network into a gap. Each push
141
+ * waits for the previous one's ack and assigns its `clientSeq` *inside* the
142
+ * critical section — from the live watermark — so the sequence is always exactly
143
+ * `watermark + 1`. Because a failed mutation never advances the server watermark,
144
+ * a permanently-rejected predecessor can't wedge the chain: the next push simply
145
+ * reclaims the same `watermark + 1` instead of leaving a hole the DO waits on.
146
+ */
147
+ declare const bindMutators: <M extends AnyMutatorMap>(client: LunoraClient, context: BindMutatorsContext, mutators: M) => BoundMutators<M>;
148
+ export { BindMutatorsContext as B, ClientMutatorContext as C, DIRECT_TRANSACTION_METADATA_KEY as D, MutatorReference as M, BoundMutators as a, ClientMutatorDef as b, bindMutators as c, defineMutator as d };
@@ -0,0 +1 @@
1
+ import{createCollection as I,safeRandomUUID as R}from"@tanstack/db";import{startOfflineExecutor as U,NonRetriableError as w}from"@tanstack/offline-transactions";import{lunoraCollectionOptions as _}from"./CHECKPOINT_FALLBACK_MS-CtPxAU9J.mjs";import{createOutboxCarrier as b,createOptimisticOnlineDetector as E,OUTBOX_MUTATION_FN_NAME as C,registerOutboxCarrier as M,runOutboxMutation as N}from"./OUTBOX_MUTATION_FN_NAME-CebgkYw2.mjs";const S=(a,K,e={})=>{const c={},p={},l={},y=Object.entries(K);for(const[t,o]of y){const n=o.insert,{config:d,scope:m}=_({client:a,getKey:o.getKey,id:t,list:o.list,...o.load===void 0?{}:{load:o.load},onError:o.onError,scopeBy:o.scopeBy,shardKey:o.shardKey});c[t]=I(d),o.scopeBy!==void 0&&(p[t]=m),n&&(l[t]=async({idempotencyKey:r,transaction:i})=>{for(const[f,x]of i.mutations.entries()){const O=x.modified,F=`${r}:${String(f)}`;try{await N(()=>a.mutation(n.mutation,n.toArgs(O),{mutationId:F}))}catch(u){if(u instanceof w&&e.onWriteRejected)try{e.onWriteRejected({code:u.code,collection:t,error:u,row:O})}catch{}throw u}}})}l[C]=async({transaction:t})=>{const o=t.metadata;if(o){if(o.identity!==a.currentIdentity())throw new w("outbox write dropped: identity changed since it was queued");await N(()=>a.mutation({__lunoraRef:o.functionPath},o.args,{mutationId:o.idempotencyKey,shardKey:o.shardKey}))}};const g=b(),s=U({collections:{...c,[C]:g},mutationFns:l,onlineDetector:E(),...e.onLeadershipChange?{onLeadershipChange:e.onLeadershipChange}:{},...e.onStorageFailure?{onStorageFailure:e.onStorageFailure}:{},onUnknownMutationFn:(t,o)=>{try{e.onWriteRejected?.({code:"UNKNOWN_MUTATION_FN",collection:t,error:new Error(`offline write dropped: mutation "${t}" no longer exists (removed or renamed in a deploy?)`),row:o.mutations[0]?.modified})}catch{}}});M(s,g);const h={};for(const[t,o]of y){const n=o.insert,d=c[t];if(!n||!d)continue;const m=s.createOfflineAction({mutationFnName:t,onMutate:({id:r,input:i})=>{d.insert(n.optimistic(i,r))}});h[t]=r=>{const i=R(),f=m({id:i,input:r});return{id:i,transaction:f}}}return{actions:h,collections:c,executor:s,pendingCount:()=>s.getPendingCount(),scope:p}};export{S as defineCollections};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/db",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.30",
4
4
  "description": "TanStack DB binding: typed, live-synced collections and a durable offline outbox over the Lunora client",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -40,13 +40,22 @@
40
40
  "types": "./dist/index.d.ts",
41
41
  "import": "./dist/index.mjs"
42
42
  },
43
+ "./collections": {
44
+ "types": "./dist/collections/index.d.ts",
45
+ "import": "./dist/collections/index.mjs"
46
+ },
47
+ "./mutators": {
48
+ "types": "./dist/mutators/index.d.ts",
49
+ "import": "./dist/mutators/index.mjs"
50
+ },
43
51
  "./package.json": "./package.json"
44
52
  },
45
53
  "publishConfig": {
46
54
  "access": "public"
47
55
  },
48
56
  "dependencies": {
49
- "@lunora/client": "1.0.0-alpha.3"
57
+ "@lunora/client": "1.0.0-alpha.30",
58
+ "@lunora/errors": "1.0.0-alpha.8"
50
59
  },
51
60
  "peerDependencies": {
52
61
  "@tanstack/db": "^0.6.0",
@@ -1,65 +0,0 @@
1
- import { NonRetriableError } from '@tanstack/offline-transactions';
2
-
3
- const OUTBOX_DRAIN_INTERVAL_MS = 1e3;
4
- const toMap = (rows, getKey) => {
5
- const map = /* @__PURE__ */ new Map();
6
- for (const row of rows) {
7
- map.set(getKey(row), row);
8
- }
9
- return map;
10
- };
11
- const makeDiffEmit = (synced, writer) => (next) => {
12
- writer.begin();
13
- for (const [key, value] of next) {
14
- const previous = synced.get(key);
15
- if (previous === void 0) {
16
- writer.write({ type: "insert", value });
17
- } else if (JSON.stringify(previous) !== JSON.stringify(value)) {
18
- writer.write({ type: "update", value });
19
- }
20
- }
21
- for (const key of synced.keys()) {
22
- if (!next.has(key)) {
23
- writer.write({ key, type: "delete" });
24
- }
25
- }
26
- writer.commit();
27
- synced.clear();
28
- for (const [key, value] of next) {
29
- synced.set(key, value);
30
- }
31
- };
32
- const runOutboxMutation = async (mutate) => {
33
- try {
34
- await mutate();
35
- } catch (error) {
36
- if (typeof error.code === "string") {
37
- throw new NonRetriableError(error instanceof Error ? error.message : String(error));
38
- }
39
- throw error;
40
- }
41
- };
42
- const createOptimisticOnlineDetector = () => {
43
- const intervals = /* @__PURE__ */ new Set();
44
- return {
45
- dispose: () => {
46
- for (const handle of intervals) {
47
- clearInterval(handle);
48
- }
49
- intervals.clear();
50
- },
51
- isOnline: () => true,
52
- notifyOnline: () => {
53
- },
54
- subscribe: (callback) => {
55
- const handle = setInterval(callback, OUTBOX_DRAIN_INTERVAL_MS);
56
- intervals.add(handle);
57
- return () => {
58
- clearInterval(handle);
59
- intervals.delete(handle);
60
- };
61
- }
62
- };
63
- };
64
-
65
- export { createOptimisticOnlineDetector, makeDiffEmit, runOutboxMutation, toMap };
@@ -1,109 +0,0 @@
1
- import { createCollection, BTreeIndex } from '@tanstack/db';
2
- import { startOfflineExecutor } from '@tanstack/offline-transactions';
3
- import { toMap, createOptimisticOnlineDetector, makeDiffEmit, runOutboxMutation } from './createOptimisticOnlineDetector-CRulWqZ7.mjs';
4
-
5
- const defineCollections = (client, defs) => {
6
- const collections = {};
7
- const scope = {};
8
- const subscriptions = {};
9
- const emitters = {};
10
- const errorHandlers = {};
11
- const mutationFns = {};
12
- const entries = Object.entries(defs);
13
- for (const [name, definition] of entries) {
14
- const getKey = definition.getKey ?? ((row) => row._id);
15
- const insert = definition.insert;
16
- const synced = /* @__PURE__ */ new Map();
17
- collections[name] = createCollection({
18
- // Auto-build ordered (B-tree) indexes for whatever the app's live
19
- // queries join / filter / sort on, so they stay fast as data grows.
20
- autoIndex: "eager",
21
- defaultIndexType: BTreeIndex,
22
- getKey,
23
- id: name,
24
- sync: {
25
- sync: (writer) => {
26
- const emit = makeDiffEmit(synced, writer);
27
- emitters[name] = emit;
28
- const onError = (error) => {
29
- writer.markReady();
30
- definition.onError?.(error);
31
- };
32
- errorHandlers[name] = onError;
33
- if (definition.scopeBy === void 0) {
34
- subscriptions[name] = client.subscribe(
35
- definition.list,
36
- {},
37
- (rows) => {
38
- emit(toMap(rows, getKey));
39
- writer.markReady();
40
- },
41
- { onError }
42
- );
43
- } else {
44
- writer.markReady();
45
- }
46
- return () => {
47
- emitters[name] = void 0;
48
- errorHandlers[name] = void 0;
49
- subscriptions[name]?.();
50
- subscriptions[name] = void 0;
51
- };
52
- }
53
- }
54
- });
55
- if (definition.scopeBy !== void 0) {
56
- scope[name] = (args) => {
57
- subscriptions[name]?.();
58
- subscriptions[name] = void 0;
59
- emitters[name]?.(/* @__PURE__ */ new Map());
60
- if (args === void 0) {
61
- return;
62
- }
63
- subscriptions[name] = client.subscribe(
64
- definition.list,
65
- args,
66
- (rows) => {
67
- emitters[name]?.(toMap(rows, getKey));
68
- },
69
- { onError: (error) => errorHandlers[name]?.(error) }
70
- );
71
- };
72
- }
73
- if (insert) {
74
- mutationFns[name] = async ({ transaction }) => {
75
- for (const mutation of transaction.mutations) {
76
- const row = mutation.modified;
77
- await runOutboxMutation(() => client.mutation(insert.mutation, insert.toArgs(row)));
78
- }
79
- };
80
- }
81
- }
82
- const executor = startOfflineExecutor({
83
- collections,
84
- mutationFns,
85
- onlineDetector: createOptimisticOnlineDetector()
86
- });
87
- const actions = {};
88
- for (const [name, definition] of entries) {
89
- const insert = definition.insert;
90
- const collection = collections[name];
91
- if (!insert || !collection) {
92
- continue;
93
- }
94
- const action = executor.createOfflineAction({
95
- mutationFnName: name,
96
- onMutate: ({ id, input }) => {
97
- collection.insert(insert.optimistic(input, id));
98
- }
99
- });
100
- actions[name] = (input) => {
101
- const id = crypto.randomUUID();
102
- const transaction = action({ id, input });
103
- return { id, transaction };
104
- };
105
- }
106
- return { actions, collections, executor, scope };
107
- };
108
-
109
- export { defineCollections };