@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.
- package/README.md +38 -2
- package/dist/collections/index.d.mts +2 -2
- package/dist/collections/index.d.ts +2 -2
- package/dist/collections/index.mjs +1 -2
- package/dist/index.d.mts +144 -5
- package/dist/index.d.ts +144 -5
- package/dist/index.mjs +1 -8
- package/dist/mutators/index.d.mts +2 -2
- package/dist/mutators/index.d.ts +2 -2
- package/dist/mutators/index.mjs +1 -1
- package/dist/packem_shared/CHECKPOINT_FALLBACK_MS-Dnkx44kO.mjs +1 -0
- package/dist/packem_shared/DIRECT_TRANSACTION_METADATA_KEY-BGf7W7d4.mjs +1 -0
- package/dist/packem_shared/OUTBOX_MUTATION_FN_NAME-rGyegnLw.mjs +1 -0
- package/dist/packem_shared/applyPlanToCollections-DRwloPsc.mjs +1 -0
- package/dist/packem_shared/collection-options.d-C2FdJw06.d.mts +411 -0
- package/dist/packem_shared/collection-options.d-C2FdJw06.d.ts +411 -0
- package/dist/packem_shared/defineCollections-D4Y0GiBS.mjs +1 -0
- package/dist/packem_shared/index.d-9usNsVAH.d.mts +245 -0
- package/dist/packem_shared/{define-collections.d-Dkzt5xPY.d.ts → index.d-B_2dXw1Z.d.mts} +23 -7
- package/dist/packem_shared/index.d-t0PhXOHV.d.ts +245 -0
- package/dist/packem_shared/{define-collections.d-C9paBiXl.d.mts → index.d-ypscb9Qp.d.ts} +23 -7
- package/package.json +3 -3
- package/dist/packem_shared/OUTBOX_MUTATION_FN_NAME-9fqK7rND.mjs +0 -131
- package/dist/packem_shared/bindMutators-B_RaNgel.mjs +0 -70
- package/dist/packem_shared/collection-options.d-BPVkj5FW.d.mts +0 -227
- package/dist/packem_shared/collection-options.d-BPVkj5FW.d.ts +0 -227
- package/dist/packem_shared/createCheckpointRegistry-BtIEm2Kh.mjs +0 -136
- package/dist/packem_shared/define-mutators.d-DTU85KL6.d.mts +0 -80
- package/dist/packem_shared/define-mutators.d-DukVT0Y3.d.ts +0 -80
- package/dist/packem_shared/defineCollections-C_F-42rS.mjs +0 -109
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { R as Row } from "./collection-options.d-C2FdJw06.mjs";
|
|
1
2
|
import { FunctionReference, SubscriptionError, LunoraClient } from '@lunora/client';
|
|
2
3
|
import { Transaction, Collection } from '@tanstack/db';
|
|
3
4
|
import { OfflineExecutor, StorageDiagnostic } from '@tanstack/offline-transactions';
|
|
4
|
-
import { R as Row } from "./collection-options.d-BPVkj5FW.js";
|
|
5
5
|
/** Element type of an array (the row type a `list` query returns). */
|
|
6
6
|
type Element<T> = T extends ReadonlyArray<infer E> ? E : never;
|
|
7
7
|
/** `true` for the `any` type, `false` otherwise. */
|
|
@@ -46,20 +46,31 @@ interface CollectionDef<TList extends FunctionReference, TInput = never> {
|
|
|
46
46
|
* `loading` on error, and forwards the error here if supplied.
|
|
47
47
|
*/
|
|
48
48
|
onError?: (error: SubscriptionError) => void;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The table's `.shardBy(...)` column; makes the collection re-pointable via
|
|
51
|
+
* `scope`. Unless `shardKey` pins a shard, its value is the shard key:
|
|
52
|
+
* `scope({ channelId })` subscribes to that channel's shard, and each
|
|
53
|
+
* `insert` goes to the shard its own row's value names, captured when queued
|
|
54
|
+
* so an offline write replays there even after the app re-scopes. Scope args
|
|
55
|
+
* or an inserted row without the field throw.
|
|
56
|
+
*/
|
|
50
57
|
scopeBy?: string;
|
|
51
58
|
/**
|
|
52
|
-
*
|
|
53
|
-
* frames advance the checkpoint gate from) to
|
|
54
|
-
*
|
|
55
|
-
*
|
|
59
|
+
* Pins the `list` subscription (and the confirmed-mutation watermark its
|
|
60
|
+
* frames advance the checkpoint gate from) to one shard's DO — so a sharded
|
|
61
|
+
* collection's overlay gate compares against that shard's mutator sequence
|
|
62
|
+
* line, not the default ("") watermark bucket. `insert` writes are routed
|
|
63
|
+
* with it too: subscriptions and the writes they observe have to land on the
|
|
64
|
+
* same Durable Object, and the server derives no shard from a mutation's args.
|
|
65
|
+
* Takes precedence over `scopeBy`; leave it unset when the shard should
|
|
66
|
+
* follow the scope.
|
|
56
67
|
*/
|
|
57
68
|
shardKey?: string;
|
|
58
69
|
}
|
|
59
70
|
type AnyDef = CollectionDef<any, any>;
|
|
60
71
|
/**
|
|
61
72
|
* The public row type a collection exposes — the element type of its `list`
|
|
62
|
-
* query's return, with no `& Row`: a `Collection
|
|
73
|
+
* query's return, with no `& Row`: a `Collection<T>` is invariant in `T`, so the
|
|
63
74
|
* exposed type must be exactly the document type, not a subtype.
|
|
64
75
|
*/
|
|
65
76
|
type RowOf<C extends AnyDef> = C["list"] extends FunctionReference<infer _K, infer _A, infer R> ? Element<R> : never;
|
|
@@ -161,6 +172,11 @@ interface LunoraDb<D extends Record<string, AnyDef>> {
|
|
|
161
172
|
*
|
|
162
173
|
* This is the hand-written form; `@lunora/codegen` can emit a fully-typed call to
|
|
163
174
|
* it from `schema.ts`, so an app writes nothing.
|
|
175
|
+
*
|
|
176
|
+
* Keep a single instance and treat the returned collections as the one source of
|
|
177
|
+
* truth per table — do not mirror rows into a parallel store, or derived indexes
|
|
178
|
+
* built from the copy can silently read stale data (see the `@lunora/db` docs,
|
|
179
|
+
* "One source of truth per table").
|
|
164
180
|
*/
|
|
165
181
|
declare const defineCollections: <D extends Record<string, AnyDef>>(client: LunoraClient, defs: D, options?: DefineCollectionsOptions) => LunoraDb<D>;
|
|
166
182
|
export { CollectionDef as C, DefineCollectionsOptions as D, InsertBinding as I, LunoraDb as L, WriteRejectedEvent as W, defineCollections as d };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { LunoraClient } from '@lunora/client';
|
|
2
|
+
import { Collection, Transaction } from '@tanstack/db';
|
|
3
|
+
import { b as CheckpointRegistry } from "./collection-options.d-C2FdJw06.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
|
+
/**
|
|
23
|
+
* A map of wired collections, keyed by the name the optimistic bodies address them
|
|
24
|
+
* by.
|
|
25
|
+
*
|
|
26
|
+
* The row type is `any` on purpose. `Collection<T, string>` is **invariant** in
|
|
27
|
+
* `T` (its methods both consume and produce rows), so a concrete
|
|
28
|
+
* `Collection<Doc<"nodes"> & Row>` is NOT assignable to `Collection<Row>` — which
|
|
29
|
+
* is why the previous `Record<string, Collection<Row, string>>` forced an
|
|
30
|
+
* `as never` cast on every entry, and left `context.collections.<name>` too
|
|
31
|
+
* untyped to be worth using. Projects recover the concrete types by binding them
|
|
32
|
+
* once through {@link initMutators}.
|
|
33
|
+
*/
|
|
34
|
+
type CollectionMap = Record<string, Collection<any, string>>;
|
|
35
|
+
/** The local store a client mutator's optimistic body writes against. */
|
|
36
|
+
interface ClientMutatorContext<TCollections extends CollectionMap = CollectionMap> {
|
|
37
|
+
/** The wired collections, keyed by name — apply optimistic inserts/updates/deletes here. */
|
|
38
|
+
collections: TCollections;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A generated mutator reference (`api.mutators.sendMessage`), accepted by
|
|
42
|
+
* {@link defineMutator} in place of a hand-written path string.
|
|
43
|
+
*
|
|
44
|
+
* Declared structurally rather than imported from `@lunora/client` so this module
|
|
45
|
+
* keeps its narrow dependency surface; the shape matches `FunctionReference`, and
|
|
46
|
+
* the phantom marker carries the server mutator's arg type so the client body's
|
|
47
|
+
* args are **inferred** instead of restated.
|
|
48
|
+
*/
|
|
49
|
+
interface MutatorReference<TArgs = unknown> {
|
|
50
|
+
readonly __lunoraPhantom?: {
|
|
51
|
+
args: TArgs;
|
|
52
|
+
kind: unknown;
|
|
53
|
+
returns: unknown;
|
|
54
|
+
};
|
|
55
|
+
readonly __lunoraRef: string;
|
|
56
|
+
}
|
|
57
|
+
/** A client-side custom mutator: an optimistic body plus the path of its authoritative server impl. */
|
|
58
|
+
interface ClientMutatorDef<TArgs, TCollections extends CollectionMap = CollectionMap> {
|
|
59
|
+
/** Brand so codegen / `bindMutators` can recognize a mutator definition. */
|
|
60
|
+
__lunoraClientMutator: true;
|
|
61
|
+
/** The optimistic update applied to the local collections before the server confirms. */
|
|
62
|
+
apply: (context: ClientMutatorContext<TCollections>, args: TArgs) => void;
|
|
63
|
+
/** The Lunora function path of the server-authoritative mutator (`defineMutator` on the server). */
|
|
64
|
+
serverRef: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Declare a client-side custom mutator. `apply` runs optimistically against the
|
|
68
|
+
* local TanStack collections; `serverRef` names the authoritative server mutator
|
|
69
|
+
* the write is pushed to over the watermark protocol. The server impl is the
|
|
70
|
+
* linearization point — this body is a prediction the server can override.
|
|
71
|
+
*
|
|
72
|
+
* **Pass a generated reference, not a string.** `serverRef: api.mutators.sendMessage`
|
|
73
|
+
* both binds the path at compile time — a rename, a typo, or a moved file becomes a
|
|
74
|
+
* type error instead of a mutation that silently fails at runtime — and **infers
|
|
75
|
+
* `TArgs` from the server mutator's own validators**, so the arg type is declared
|
|
76
|
+
* once on the server rather than restated in every client body:
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* // Typed + checked: args inferred from the server mutator.
|
|
80
|
+
* defineMutator({
|
|
81
|
+
* apply: ({ collections }, args) => { … }, // args: { channelId: Id<"channels">; text: string }
|
|
82
|
+
* serverRef: api.mutators.sendMessage,
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* // Escape hatch: a path string still works, but nothing checks it and you must
|
|
86
|
+
* // restate the args yourself.
|
|
87
|
+
* defineMutator<{ text: string }>({ apply, serverRef: "mutators:sendMessage" });
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* `context.collections` is typed by whatever map it is bound to — which for this
|
|
91
|
+
* standalone form is the widest one. Bind the concrete collections once with
|
|
92
|
+
* {@link initMutators} to get `collections.<name>` typed inside `apply`.
|
|
93
|
+
*/
|
|
94
|
+
declare const defineMutator: {
|
|
95
|
+
<TArgs = Record<string, unknown>>(definition: {
|
|
96
|
+
apply: (context: ClientMutatorContext, args: TArgs) => void;
|
|
97
|
+
serverRef: string;
|
|
98
|
+
}): ClientMutatorDef<TArgs>;
|
|
99
|
+
<TArgs>(definition: {
|
|
100
|
+
apply: (context: ClientMutatorContext, args: TArgs) => void;
|
|
101
|
+
serverRef: MutatorReference<TArgs>;
|
|
102
|
+
}): ClientMutatorDef<TArgs>;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A mutator map whose defs are bound to `TCollections`, arg types erased — the
|
|
106
|
+
* `TCollections`-pinned counterpart of {@link AnyMutatorMap}, and `any` for the
|
|
107
|
+
* same variance reason.
|
|
108
|
+
*/
|
|
109
|
+
type MutatorMapFor<TCollections extends CollectionMap> = Record<string, ClientMutatorDef<any, TCollections>>;
|
|
110
|
+
type AnyMutatorMap = Record<string, ClientMutatorDef<any, any>>;
|
|
111
|
+
/** Args type of a mutator definition, whatever collections map it was bound to. */
|
|
112
|
+
type ArgsOf<M> = M extends ClientMutatorDef<infer A, infer _C> ? A : never;
|
|
113
|
+
/** A mutator write that was permanently rejected, passed to {@link BindMutatorsContext.onWriteRejected}. */
|
|
114
|
+
interface MutatorRejectedEvent {
|
|
115
|
+
/** The args the rejected call was made with. */
|
|
116
|
+
args: unknown;
|
|
117
|
+
/**
|
|
118
|
+
* The machine-readable reason when the server supplied one (e.g. `CONFLICT`,
|
|
119
|
+
* `FORBIDDEN`). Mirrors {@link import("./define-collections").WriteRejectedEvent.code}.
|
|
120
|
+
*/
|
|
121
|
+
code?: string;
|
|
122
|
+
/** The error that rejected the write. */
|
|
123
|
+
error: Error;
|
|
124
|
+
/** The bound mutator's key in the map passed to `bindMutators`. */
|
|
125
|
+
mutator: string;
|
|
126
|
+
/** The `namespace:fn` path the push targeted. */
|
|
127
|
+
serverRef: string;
|
|
128
|
+
}
|
|
129
|
+
interface BindMutatorsContext<TCollections extends CollectionMap = CollectionMap> {
|
|
130
|
+
/**
|
|
131
|
+
* Resolves the optimistic-overlay drop against confirmed server watermarks: a
|
|
132
|
+
* mutation's overlay is held until the sync stream echoes
|
|
133
|
+
* `lastMutationId >= clientSeq` (via {@link CheckpointRegistry.resolve}), so the
|
|
134
|
+
* row never flashes out and back.
|
|
135
|
+
*
|
|
136
|
+
* Defaults to the shared per-shard registry for `client` + {@link BindMutatorsContext.shardKey}
|
|
137
|
+
* ({@link getShardCheckpoints}) — the same one
|
|
138
|
+
* {@link import("./collection-options").lunoraCollectionOptions} defaults to, so
|
|
139
|
+
* a shard's collections and its mutators gate on one watermark line without the
|
|
140
|
+
* caller wiring them together. Pass `false` to drop the overlay as soon as the
|
|
141
|
+
* server accepts the write (the by-value sync diff then converges the synced row
|
|
142
|
+
* in place).
|
|
143
|
+
*/
|
|
144
|
+
checkpoints?: CheckpointRegistry | false;
|
|
145
|
+
/** The wired collections the optimistic bodies write against. */
|
|
146
|
+
collections: TCollections;
|
|
147
|
+
/**
|
|
148
|
+
* Called when a mutator's server push is permanently rejected — the aggregate
|
|
149
|
+
* failure channel for writes, symmetric with
|
|
150
|
+
* {@link import("./define-collections").DefineCollectionsOptions.onWriteRejected}
|
|
151
|
+
* on the outbox path.
|
|
152
|
+
*
|
|
153
|
+
* Supplying it also makes a **fire-and-forget** call safe: a bound handle
|
|
154
|
+
* returns a `Transaction` whose `isPersisted` promise rejects on failure, so a
|
|
155
|
+
* caller that neither awaits it nor attaches a `.catch` leaves an unhandled
|
|
156
|
+
* rejection. With this hook set, `bindMutators` consumes that rejection itself
|
|
157
|
+
* after reporting it — a caller that DOES await still sees the rejection, so
|
|
158
|
+
* per-call handling is unaffected.
|
|
159
|
+
*
|
|
160
|
+
* A throwing listener is swallowed: reporting a failure must not manufacture a
|
|
161
|
+
* second one.
|
|
162
|
+
*/
|
|
163
|
+
onWriteRejected?: (event: MutatorRejectedEvent) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Route each call to the shard its own args name: the value of this field
|
|
166
|
+
* (the table's `.shardBy(...)` column) is the shard key, exactly as for a
|
|
167
|
+
* `scopeBy` collection's inserts — so `sendMessage({ channelId, … })` lands
|
|
168
|
+
* on that channel's shard. Required when `checkpoints` comes from a
|
|
169
|
+
* `scopeBy` collection, unless `shardKey` pins one shard; a call whose args
|
|
170
|
+
* lack the field throws.
|
|
171
|
+
* {@link BindMutatorsContext.shardKey} takes precedence.
|
|
172
|
+
*/
|
|
173
|
+
scopeBy?: string;
|
|
174
|
+
/** Pins every push to one shard. Takes precedence over {@link BindMutatorsContext.scopeBy}. */
|
|
175
|
+
shardKey?: string;
|
|
176
|
+
}
|
|
177
|
+
/** Calling a bound mutator runs the optimistic body + pushes the server write; returns the TanStack transaction. */
|
|
178
|
+
type BoundMutators<M extends AnyMutatorMap> = { [K in keyof M]: (args: ArgsOf<M[K]>) => Transaction; };
|
|
179
|
+
/**
|
|
180
|
+
* Bind a set of client mutators to a client + local store. Each returned handle,
|
|
181
|
+
* when called, opens a TanStack optimistic transaction: the mutator's `apply`
|
|
182
|
+
* body writes the predicted rows into the collections, and the transaction's
|
|
183
|
+
* `mutationFn` pushes the authoritative write through
|
|
184
|
+
* {@link LunoraClient.callMutator} under a monotonic per-client `clientSeq`.
|
|
185
|
+
*
|
|
186
|
+
* Rebase-on-poke is free — TanStack DB re-derives every pending optimistic overlay
|
|
187
|
+
* over the latest synced base on each sync tick. The overlay is dropped when the
|
|
188
|
+
* server confirms the write (and, if `checkpoints` is supplied, once it echoes the
|
|
189
|
+
* matching watermark so the synced row has landed).
|
|
190
|
+
*
|
|
191
|
+
* The `clientSeq` generator is seeded from the server's echoed watermark
|
|
192
|
+
* ({@link LunoraClient.confirmedMutationWatermark}) on every issue, so a reload —
|
|
193
|
+
* which resets this in-memory counter while the server keeps a durable per-client
|
|
194
|
+
* watermark — never reissues a sequence the DO has already applied. As a backstop
|
|
195
|
+
* for the very first push of a fresh session (before any ack has taught the client
|
|
196
|
+
* the watermark), a push the DO swallows as a replay (`applied === false`) is
|
|
197
|
+
* reissued above the now-known watermark instead of being mistaken for a confirmed
|
|
198
|
+
* write — closing the silent-drop window without risking a double-apply (a fresh
|
|
199
|
+
* session's first stale push provably cannot be an honest replay).
|
|
200
|
+
*
|
|
201
|
+
* Pushes are **serialized per binding** (a FIFO chain): the DO rejects any push
|
|
202
|
+
* with `clientSeq > watermark + 1` as `OUT_OF_ORDER` and drops the write, so two
|
|
203
|
+
* mutators fired concurrently must not race the network into a gap. Each push
|
|
204
|
+
* waits for the previous one's ack and assigns its `clientSeq` *inside* the
|
|
205
|
+
* critical section — from the live watermark — so the sequence is always exactly
|
|
206
|
+
* `watermark + 1`. Because a failed mutation never advances the server watermark,
|
|
207
|
+
* a permanently-rejected predecessor can't wedge the chain: the next push simply
|
|
208
|
+
* reclaims the same `watermark + 1` instead of leaving a hole the DO waits on.
|
|
209
|
+
*/
|
|
210
|
+
declare const bindMutators: <M extends AnyMutatorMap, TCollections extends CollectionMap = CollectionMap>(client: LunoraClient, context: BindMutatorsContext<TCollections>, mutators: M) => BoundMutators<M>;
|
|
211
|
+
/** `defineMutator` + `bindMutators`, both bound to one project's collections map. */
|
|
212
|
+
interface BoundMutatorApi<TCollections extends CollectionMap> {
|
|
213
|
+
/** {@link bindMutators}, with `collections` pinned to `TCollections`. */
|
|
214
|
+
bindMutators: <M extends MutatorMapFor<TCollections>>(client: LunoraClient, context: BindMutatorsContext<TCollections>, mutators: M) => BoundMutators<M>;
|
|
215
|
+
/** {@link defineMutator}, with `context.collections` typed as `TCollections`. */
|
|
216
|
+
defineMutator: {
|
|
217
|
+
<TArgs = Record<string, unknown>>(definition: {
|
|
218
|
+
apply: (context: ClientMutatorContext<TCollections>, args: TArgs) => void;
|
|
219
|
+
serverRef: string;
|
|
220
|
+
}): ClientMutatorDef<TArgs, TCollections>;
|
|
221
|
+
<TArgs>(definition: {
|
|
222
|
+
apply: (context: ClientMutatorContext<TCollections>, args: TArgs) => void;
|
|
223
|
+
serverRef: MutatorReference<TArgs>;
|
|
224
|
+
}): ClientMutatorDef<TArgs, TCollections>;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Bind the mutator surface to **this project's** collections map, once.
|
|
229
|
+
*
|
|
230
|
+
* `Collection` is invariant in its row type, so a shared
|
|
231
|
+
* `Record<string, Collection<Row, string>>` could neither accept a generated
|
|
232
|
+
* collection without an `as never` cast nor hand a usable type back to `apply` —
|
|
233
|
+
* which is why optimistic bodies tended to ignore `context.collections` and close
|
|
234
|
+
* over module-scope collection variables instead. Declaring the map once here
|
|
235
|
+
* fixes both ends: `bindMutators` takes the concrete collections cast-free, and
|
|
236
|
+
* `apply` reads `collections.<name>` at its real row type.
|
|
237
|
+
*
|
|
238
|
+
* Runtime-identical to the standalone {@link defineMutator} / {@link bindMutators}
|
|
239
|
+
* (this returns those very functions); only the types narrow.
|
|
240
|
+
* @example
|
|
241
|
+
* const { bindMutators, defineMutator } = initMutators<{ nodes: typeof wholeOutlineCollection }>();
|
|
242
|
+
* const setText = defineMutator({ apply: ({ collections }, args) => collections.nodes.update(args.id, setter), serverRef: api.mutators.setText });
|
|
243
|
+
*/
|
|
244
|
+
declare const initMutators: <TCollections extends CollectionMap>() => BoundMutatorApi<TCollections>;
|
|
245
|
+
export { BindMutatorsContext as B, ClientMutatorContext as C, DIRECT_TRANSACTION_METADATA_KEY as D, MutatorReference as M, BoundMutatorApi as a, BoundMutators as b, ClientMutatorDef as c, CollectionMap as d, MutatorRejectedEvent as e, bindMutators as f, defineMutator as g, initMutators as i };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { R as Row } from "./collection-options.d-C2FdJw06.js";
|
|
1
2
|
import { FunctionReference, SubscriptionError, LunoraClient } from '@lunora/client';
|
|
2
3
|
import { Transaction, Collection } from '@tanstack/db';
|
|
3
4
|
import { OfflineExecutor, StorageDiagnostic } from '@tanstack/offline-transactions';
|
|
4
|
-
import { R as Row } from "./collection-options.d-BPVkj5FW.mjs";
|
|
5
5
|
/** Element type of an array (the row type a `list` query returns). */
|
|
6
6
|
type Element<T> = T extends ReadonlyArray<infer E> ? E : never;
|
|
7
7
|
/** `true` for the `any` type, `false` otherwise. */
|
|
@@ -46,20 +46,31 @@ interface CollectionDef<TList extends FunctionReference, TInput = never> {
|
|
|
46
46
|
* `loading` on error, and forwards the error here if supplied.
|
|
47
47
|
*/
|
|
48
48
|
onError?: (error: SubscriptionError) => void;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The table's `.shardBy(...)` column; makes the collection re-pointable via
|
|
51
|
+
* `scope`. Unless `shardKey` pins a shard, its value is the shard key:
|
|
52
|
+
* `scope({ channelId })` subscribes to that channel's shard, and each
|
|
53
|
+
* `insert` goes to the shard its own row's value names, captured when queued
|
|
54
|
+
* so an offline write replays there even after the app re-scopes. Scope args
|
|
55
|
+
* or an inserted row without the field throw.
|
|
56
|
+
*/
|
|
50
57
|
scopeBy?: string;
|
|
51
58
|
/**
|
|
52
|
-
*
|
|
53
|
-
* frames advance the checkpoint gate from) to
|
|
54
|
-
*
|
|
55
|
-
*
|
|
59
|
+
* Pins the `list` subscription (and the confirmed-mutation watermark its
|
|
60
|
+
* frames advance the checkpoint gate from) to one shard's DO — so a sharded
|
|
61
|
+
* collection's overlay gate compares against that shard's mutator sequence
|
|
62
|
+
* line, not the default ("") watermark bucket. `insert` writes are routed
|
|
63
|
+
* with it too: subscriptions and the writes they observe have to land on the
|
|
64
|
+
* same Durable Object, and the server derives no shard from a mutation's args.
|
|
65
|
+
* Takes precedence over `scopeBy`; leave it unset when the shard should
|
|
66
|
+
* follow the scope.
|
|
56
67
|
*/
|
|
57
68
|
shardKey?: string;
|
|
58
69
|
}
|
|
59
70
|
type AnyDef = CollectionDef<any, any>;
|
|
60
71
|
/**
|
|
61
72
|
* The public row type a collection exposes — the element type of its `list`
|
|
62
|
-
* query's return, with no `& Row`: a `Collection
|
|
73
|
+
* query's return, with no `& Row`: a `Collection<T>` is invariant in `T`, so the
|
|
63
74
|
* exposed type must be exactly the document type, not a subtype.
|
|
64
75
|
*/
|
|
65
76
|
type RowOf<C extends AnyDef> = C["list"] extends FunctionReference<infer _K, infer _A, infer R> ? Element<R> : never;
|
|
@@ -161,6 +172,11 @@ interface LunoraDb<D extends Record<string, AnyDef>> {
|
|
|
161
172
|
*
|
|
162
173
|
* This is the hand-written form; `@lunora/codegen` can emit a fully-typed call to
|
|
163
174
|
* it from `schema.ts`, so an app writes nothing.
|
|
175
|
+
*
|
|
176
|
+
* Keep a single instance and treat the returned collections as the one source of
|
|
177
|
+
* truth per table — do not mirror rows into a parallel store, or derived indexes
|
|
178
|
+
* built from the copy can silently read stale data (see the `@lunora/db` docs,
|
|
179
|
+
* "One source of truth per table").
|
|
164
180
|
*/
|
|
165
181
|
declare const defineCollections: <D extends Record<string, AnyDef>>(client: LunoraClient, defs: D, options?: DefineCollectionsOptions) => LunoraDb<D>;
|
|
166
182
|
export { CollectionDef as C, DefineCollectionsOptions as D, InsertBinding as I, LunoraDb as L, WriteRejectedEvent as W, defineCollections as d };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/db",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.260",
|
|
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",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lunora/client": "1.0.0-alpha.
|
|
58
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
57
|
+
"@lunora/client": "1.0.0-alpha.259",
|
|
58
|
+
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@tanstack/db": "^0.6.0",
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { createCollection, safeRandomUUID } from '@tanstack/db';
|
|
2
|
-
import { NonRetriableError } from '@tanstack/offline-transactions';
|
|
3
|
-
|
|
4
|
-
const OUTBOX_DRAIN_INTERVAL_MS = 1e3;
|
|
5
|
-
const OUTBOX_MUTATION_FN_NAME = "__lunora_outbox__";
|
|
6
|
-
const outboxCarriers = /* @__PURE__ */ new WeakMap();
|
|
7
|
-
const createOutboxCarrier = () => createCollection({
|
|
8
|
-
// eslint-disable-next-line no-underscore-dangle -- `_id` is the Lunora document-id field
|
|
9
|
-
getKey: (row) => row._id,
|
|
10
|
-
id: `${OUTBOX_MUTATION_FN_NAME}:${safeRandomUUID()}`,
|
|
11
|
-
startSync: true,
|
|
12
|
-
sync: {
|
|
13
|
-
// Nothing syncs into the carrier — mark it ready immediately.
|
|
14
|
-
sync: (writer) => {
|
|
15
|
-
writer.markReady();
|
|
16
|
-
return () => void 0;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
const registerOutboxCarrier = (executor, carrier) => {
|
|
21
|
-
outboxCarriers.set(executor, carrier);
|
|
22
|
-
};
|
|
23
|
-
let warnedMissingCarrier = false;
|
|
24
|
-
const createExecutorOutboxSink = (executor, options = {}) => {
|
|
25
|
-
const maxItems = options.maxItems ?? 1e3;
|
|
26
|
-
const mutationFunctionName = options.mutationFnName ?? OUTBOX_MUTATION_FN_NAME;
|
|
27
|
-
return {
|
|
28
|
-
enqueue(mutation) {
|
|
29
|
-
if (executor.getPendingCount() >= maxItems) {
|
|
30
|
-
const error = new Error("offline outbox is full");
|
|
31
|
-
error.code = "OFFLINE_QUEUE_OVERFLOW";
|
|
32
|
-
return Promise.reject(error);
|
|
33
|
-
}
|
|
34
|
-
const metadata = {
|
|
35
|
-
args: mutation.args,
|
|
36
|
-
clientId: mutation.clientId,
|
|
37
|
-
functionPath: mutation.functionPath,
|
|
38
|
-
// Persist the stable replay key so a committed-but-unacked retry
|
|
39
|
-
// resends the same `x-lunora-mutation-id` and the server dedups it.
|
|
40
|
-
idempotencyKey: mutation.idempotencyKey,
|
|
41
|
-
identity: mutation.identity,
|
|
42
|
-
mutationId: mutation.mutationId,
|
|
43
|
-
shardKey: mutation.shardKey
|
|
44
|
-
};
|
|
45
|
-
const carrier = outboxCarriers.get(executor);
|
|
46
|
-
if (!carrier && !warnedMissingCarrier) {
|
|
47
|
-
warnedMissingCarrier = true;
|
|
48
|
-
console.warn(
|
|
49
|
-
"[@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()."
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
const transaction = executor.createOfflineTransaction({
|
|
53
|
-
autoCommit: false,
|
|
54
|
-
idempotencyKey: mutation.idempotencyKey,
|
|
55
|
-
metadata,
|
|
56
|
-
mutationFnName: mutationFunctionName
|
|
57
|
-
});
|
|
58
|
-
transaction.mutate(() => {
|
|
59
|
-
carrier?.insert({ _id: mutation.idempotencyKey });
|
|
60
|
-
});
|
|
61
|
-
transaction.commit?.().catch(() => void 0);
|
|
62
|
-
return Promise.resolve();
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
const toMap = (rows, getKey) => {
|
|
67
|
-
const map = /* @__PURE__ */ new Map();
|
|
68
|
-
for (const row of rows) {
|
|
69
|
-
map.set(getKey(row), row);
|
|
70
|
-
}
|
|
71
|
-
return map;
|
|
72
|
-
};
|
|
73
|
-
const makeDiffEmit = (syncedJson, writer) => (next) => {
|
|
74
|
-
writer.begin();
|
|
75
|
-
const nextJson = /* @__PURE__ */ new Map();
|
|
76
|
-
for (const [key, value] of next) {
|
|
77
|
-
const valueJson = JSON.stringify(value);
|
|
78
|
-
nextJson.set(key, valueJson);
|
|
79
|
-
if (!syncedJson.has(key)) {
|
|
80
|
-
writer.write({ type: "insert", value });
|
|
81
|
-
} else if (syncedJson.get(key) !== valueJson) {
|
|
82
|
-
writer.write({ type: "update", value });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
for (const key of syncedJson.keys()) {
|
|
86
|
-
if (!next.has(key)) {
|
|
87
|
-
writer.write({ key, type: "delete" });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
writer.commit();
|
|
91
|
-
syncedJson.clear();
|
|
92
|
-
for (const [key, valueJson] of nextJson) {
|
|
93
|
-
syncedJson.set(key, valueJson);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
const runOutboxMutation = async (mutate) => {
|
|
97
|
-
try {
|
|
98
|
-
await mutate();
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (typeof error.code === "string") {
|
|
101
|
-
const nonRetriable = new NonRetriableError(error instanceof Error ? error.message : String(error));
|
|
102
|
-
nonRetriable.code = error.code;
|
|
103
|
-
throw nonRetriable;
|
|
104
|
-
}
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
const createOptimisticOnlineDetector = () => {
|
|
109
|
-
const intervals = /* @__PURE__ */ new Set();
|
|
110
|
-
return {
|
|
111
|
-
dispose: () => {
|
|
112
|
-
for (const handle of intervals) {
|
|
113
|
-
clearInterval(handle);
|
|
114
|
-
}
|
|
115
|
-
intervals.clear();
|
|
116
|
-
},
|
|
117
|
-
isOnline: () => true,
|
|
118
|
-
notifyOnline: () => {
|
|
119
|
-
},
|
|
120
|
-
subscribe: (callback) => {
|
|
121
|
-
const handle = setInterval(callback, OUTBOX_DRAIN_INTERVAL_MS);
|
|
122
|
-
intervals.add(handle);
|
|
123
|
-
return () => {
|
|
124
|
-
clearInterval(handle);
|
|
125
|
-
intervals.delete(handle);
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export { OUTBOX_MUTATION_FN_NAME, createExecutorOutboxSink, createOptimisticOnlineDetector, createOutboxCarrier, makeDiffEmit, registerOutboxCarrier, runOutboxMutation, toMap };
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { createTransaction } from '@tanstack/db';
|
|
3
|
-
import { runOutboxMutation } from './OUTBOX_MUTATION_FN_NAME-9fqK7rND.mjs';
|
|
4
|
-
|
|
5
|
-
const defineMutator = (definition) => {
|
|
6
|
-
return {
|
|
7
|
-
__lunoraClientMutator: true,
|
|
8
|
-
apply: definition.apply,
|
|
9
|
-
serverRef: definition.serverRef
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
const bindMutators = (client, context, mutators) => {
|
|
13
|
-
const maxReissues = 32;
|
|
14
|
-
let counter = 0;
|
|
15
|
-
const nextClientSeq = () => {
|
|
16
|
-
counter = Math.max(counter, client.confirmedMutationWatermark(context.shardKey)) + 1;
|
|
17
|
-
return counter;
|
|
18
|
-
};
|
|
19
|
-
let pushChain = Promise.resolve();
|
|
20
|
-
const pushSerialized = (serverRef, args) => {
|
|
21
|
-
const run = pushChain.then(async () => {
|
|
22
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
23
|
-
const clientSeq = nextClientSeq();
|
|
24
|
-
const { applied } = await client.callMutator(serverRef, args, {
|
|
25
|
-
clientSeq,
|
|
26
|
-
shardKey: context.shardKey
|
|
27
|
-
});
|
|
28
|
-
if (applied) {
|
|
29
|
-
return clientSeq;
|
|
30
|
-
}
|
|
31
|
-
if (attempt >= maxReissues) {
|
|
32
|
-
throw new LunoraError(
|
|
33
|
-
"INTERNAL",
|
|
34
|
-
`lunora: custom mutator "${serverRef}" could not claim a fresh client sequence after ${String(maxReissues)} attempts`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
pushChain = run.then(
|
|
40
|
-
() => void 0,
|
|
41
|
-
() => void 0
|
|
42
|
-
);
|
|
43
|
-
return run;
|
|
44
|
-
};
|
|
45
|
-
const bound = {};
|
|
46
|
-
for (const [name, mutator] of Object.entries(mutators)) {
|
|
47
|
-
bound[name] = (args) => {
|
|
48
|
-
const transaction = createTransaction({
|
|
49
|
-
autoCommit: true,
|
|
50
|
-
metadata: { serverRef: mutator.serverRef },
|
|
51
|
-
mutationFn: async () => {
|
|
52
|
-
let appliedSeq = 0;
|
|
53
|
-
await runOutboxMutation(async () => {
|
|
54
|
-
appliedSeq = await pushSerialized(mutator.serverRef, args);
|
|
55
|
-
});
|
|
56
|
-
if (context.checkpoints) {
|
|
57
|
-
await context.checkpoints.awaitMutationId(appliedSeq);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
transaction.mutate(() => {
|
|
62
|
-
mutator.apply({ collections: context.collections }, args);
|
|
63
|
-
});
|
|
64
|
-
return transaction;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return bound;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export { bindMutators, defineMutator };
|