@lunora/db 1.0.0-alpha.28 → 1.0.0-alpha.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +79 -5
- package/dist/index.d.ts +79 -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-CtPxAU9J.mjs +1 -0
- package/dist/packem_shared/DIRECT_TRANSACTION_METADATA_KEY-Cvm6vs5S.mjs +1 -0
- package/dist/packem_shared/OUTBOX_MUTATION_FN_NAME-CebgkYw2.mjs +1 -0
- package/dist/packem_shared/applyPlanToCollections-C29s6OoX.mjs +1 -0
- package/dist/packem_shared/{collection-options.d-BPVkj5FW.d.mts → collection-options.d-C03Rjjxn.d.mts} +120 -10
- package/dist/packem_shared/{collection-options.d-BPVkj5FW.d.ts → collection-options.d-C03Rjjxn.d.ts} +120 -10
- package/dist/packem_shared/{define-collections.d-Dkzt5xPY.d.ts → define-collections.d-Ds-5C0M9.d.ts} +1 -1
- package/dist/packem_shared/{define-collections.d-C9paBiXl.d.mts → define-collections.d-Y9AaX0yt.d.mts} +1 -1
- package/dist/packem_shared/{define-mutators.d-DTU85KL6.d.mts → define-mutators.d-DqkWcDks.d.ts} +80 -12
- package/dist/packem_shared/{define-mutators.d-DukVT0Y3.d.ts → define-mutators.d-tPb72T4I.d.mts} +80 -12
- package/dist/packem_shared/defineCollections-CnFdW1Ff.mjs +1 -0
- package/package.json +2 -2
- 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/createCheckpointRegistry-BtIEm2Kh.mjs +0 -136
- package/dist/packem_shared/defineCollections-C_F-42rS.mjs +0 -109
package/dist/packem_shared/{define-mutators.d-DTU85KL6.d.mts → define-mutators.d-DqkWcDks.d.ts}
RENAMED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
import { LunoraClient } from '@lunora/client';
|
|
2
2
|
import { Collection, Transaction } from '@tanstack/db';
|
|
3
|
-
import { C as CheckpointRegistry, R as Row } from "./collection-options.d-
|
|
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";
|
|
4
22
|
/** The local store a client mutator's optimistic body writes against. */
|
|
5
23
|
interface ClientMutatorContext {
|
|
6
24
|
/** The wired collections, keyed by name — apply optimistic inserts/updates/deletes here. */
|
|
7
25
|
collections: Record<string, Collection<Row, string>>;
|
|
8
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;
|
|
9
46
|
/** A client-side custom mutator: an optimistic body plus the path of its authoritative server impl. */
|
|
10
47
|
interface ClientMutatorDef<TArgs> {
|
|
11
48
|
/** Brand so codegen / `bindMutators` can recognize a mutator definition. */
|
|
@@ -20,24 +57,55 @@ interface ClientMutatorDef<TArgs> {
|
|
|
20
57
|
* local TanStack collections; `serverRef` names the authoritative server mutator
|
|
21
58
|
* the write is pushed to over the watermark protocol. The server impl is the
|
|
22
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
|
+
* ```
|
|
23
78
|
*/
|
|
24
|
-
declare const defineMutator:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
};
|
|
28
89
|
type AnyMutatorMap = Record<string, ClientMutatorDef<any>>;
|
|
29
90
|
/** Args type of a mutator definition. */
|
|
30
91
|
type ArgsOf<M> = M extends ClientMutatorDef<infer A> ? A : never;
|
|
31
92
|
/** Inputs `bindMutators` needs to run a mutator: the local store + how the overlay drops. */
|
|
32
93
|
interface BindMutatorsContext {
|
|
33
94
|
/**
|
|
34
|
-
* Resolves the optimistic-overlay drop against confirmed server watermarks
|
|
35
|
-
*
|
|
36
|
-
* `lastMutationId >= clientSeq` (via {@link CheckpointRegistry.resolve})
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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).
|
|
39
107
|
*/
|
|
40
|
-
checkpoints?: CheckpointRegistry;
|
|
108
|
+
checkpoints?: CheckpointRegistry | false;
|
|
41
109
|
/** The wired collections the optimistic bodies write against. */
|
|
42
110
|
collections: Record<string, Collection<Row, string>>;
|
|
43
111
|
/** Optional shard key the mutator's server push is routed to. */
|
|
@@ -77,4 +145,4 @@ type BoundMutators<M extends AnyMutatorMap> = { [K in keyof M]: (args: ArgsOf<M[
|
|
|
77
145
|
* reclaims the same `watermark + 1` instead of leaving a hole the DO waits on.
|
|
78
146
|
*/
|
|
79
147
|
declare const bindMutators: <M extends AnyMutatorMap>(client: LunoraClient, context: BindMutatorsContext, mutators: M) => BoundMutators<M>;
|
|
80
|
-
export { BindMutatorsContext as B, ClientMutatorContext as C, BoundMutators as a, ClientMutatorDef as b, bindMutators as c, defineMutator as d };
|
|
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 };
|
package/dist/packem_shared/{define-mutators.d-DukVT0Y3.d.ts → define-mutators.d-tPb72T4I.d.mts}
RENAMED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
import { LunoraClient } from '@lunora/client';
|
|
2
2
|
import { Collection, Transaction } from '@tanstack/db';
|
|
3
|
-
import { C as CheckpointRegistry, R as Row } from "./collection-options.d-
|
|
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";
|
|
4
22
|
/** The local store a client mutator's optimistic body writes against. */
|
|
5
23
|
interface ClientMutatorContext {
|
|
6
24
|
/** The wired collections, keyed by name — apply optimistic inserts/updates/deletes here. */
|
|
7
25
|
collections: Record<string, Collection<Row, string>>;
|
|
8
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;
|
|
9
46
|
/** A client-side custom mutator: an optimistic body plus the path of its authoritative server impl. */
|
|
10
47
|
interface ClientMutatorDef<TArgs> {
|
|
11
48
|
/** Brand so codegen / `bindMutators` can recognize a mutator definition. */
|
|
@@ -20,24 +57,55 @@ interface ClientMutatorDef<TArgs> {
|
|
|
20
57
|
* local TanStack collections; `serverRef` names the authoritative server mutator
|
|
21
58
|
* the write is pushed to over the watermark protocol. The server impl is the
|
|
22
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
|
+
* ```
|
|
23
78
|
*/
|
|
24
|
-
declare const defineMutator:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
};
|
|
28
89
|
type AnyMutatorMap = Record<string, ClientMutatorDef<any>>;
|
|
29
90
|
/** Args type of a mutator definition. */
|
|
30
91
|
type ArgsOf<M> = M extends ClientMutatorDef<infer A> ? A : never;
|
|
31
92
|
/** Inputs `bindMutators` needs to run a mutator: the local store + how the overlay drops. */
|
|
32
93
|
interface BindMutatorsContext {
|
|
33
94
|
/**
|
|
34
|
-
* Resolves the optimistic-overlay drop against confirmed server watermarks
|
|
35
|
-
*
|
|
36
|
-
* `lastMutationId >= clientSeq` (via {@link CheckpointRegistry.resolve})
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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).
|
|
39
107
|
*/
|
|
40
|
-
checkpoints?: CheckpointRegistry;
|
|
108
|
+
checkpoints?: CheckpointRegistry | false;
|
|
41
109
|
/** The wired collections the optimistic bodies write against. */
|
|
42
110
|
collections: Record<string, Collection<Row, string>>;
|
|
43
111
|
/** Optional shard key the mutator's server push is routed to. */
|
|
@@ -77,4 +145,4 @@ type BoundMutators<M extends AnyMutatorMap> = { [K in keyof M]: (args: ArgsOf<M[
|
|
|
77
145
|
* reclaims the same `watermark + 1` instead of leaving a hole the DO waits on.
|
|
78
146
|
*/
|
|
79
147
|
declare const bindMutators: <M extends AnyMutatorMap>(client: LunoraClient, context: BindMutatorsContext, mutators: M) => BoundMutators<M>;
|
|
80
|
-
export { BindMutatorsContext as B, ClientMutatorContext as C, BoundMutators as a, ClientMutatorDef as b, bindMutators as c, defineMutator as d };
|
|
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
|
+
"version": "1.0.0-alpha.29",
|
|
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,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lunora/client": "1.0.0-alpha.
|
|
57
|
+
"@lunora/client": "1.0.0-alpha.29",
|
|
58
58
|
"@lunora/errors": "1.0.0-alpha.8"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
@@ -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 };
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { BTreeIndex } from '@tanstack/db';
|
|
3
|
-
import { toMap, makeDiffEmit } from './OUTBOX_MUTATION_FN_NAME-9fqK7rND.mjs';
|
|
4
|
-
|
|
5
|
-
const createGate = () => {
|
|
6
|
-
let highest = Number.NEGATIVE_INFINITY;
|
|
7
|
-
const waiters = [];
|
|
8
|
-
return {
|
|
9
|
-
advance: (value) => {
|
|
10
|
-
if (value <= highest) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
highest = value;
|
|
14
|
-
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
15
|
-
const waiter = waiters[index];
|
|
16
|
-
if (waiter && waiter.threshold <= highest) {
|
|
17
|
-
waiter.resolve();
|
|
18
|
-
waiters.splice(index, 1);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
await: (threshold) => {
|
|
23
|
-
if (threshold <= highest) {
|
|
24
|
-
return Promise.resolve();
|
|
25
|
-
}
|
|
26
|
-
return new Promise((resolve) => {
|
|
27
|
-
waiters.push({ resolve, threshold });
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
const createCheckpointRegistry = () => {
|
|
33
|
-
const checkpointGate = createGate();
|
|
34
|
-
const mutationGate = createGate();
|
|
35
|
-
return {
|
|
36
|
-
awaitCheckpoint: (cursor) => checkpointGate.await(cursor),
|
|
37
|
-
awaitMutationId: (id) => mutationGate.await(id),
|
|
38
|
-
resolve: ({ checkpoint, mutationId }) => {
|
|
39
|
-
if (checkpoint !== void 0) {
|
|
40
|
-
checkpointGate.advance(checkpoint);
|
|
41
|
-
}
|
|
42
|
-
if (mutationId !== void 0) {
|
|
43
|
-
mutationGate.advance(mutationId);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
const lunoraCollectionOptions = (options) => {
|
|
49
|
-
if (options.list === void 0 === (options.shape === void 0)) {
|
|
50
|
-
throw new LunoraError("INTERNAL", "lunoraCollectionOptions: pass exactly one of `list` or `shape`");
|
|
51
|
-
}
|
|
52
|
-
const getKey = options.getKey ?? ((row) => row._id);
|
|
53
|
-
const checkpoints = createCheckpointRegistry();
|
|
54
|
-
const syncedJson = /* @__PURE__ */ new Map();
|
|
55
|
-
let emit;
|
|
56
|
-
let unsubscribe;
|
|
57
|
-
let onErrorHandler;
|
|
58
|
-
let scopedArgs;
|
|
59
|
-
const openSubscription = (args, onReady) => {
|
|
60
|
-
const onRows = (data) => {
|
|
61
|
-
emit?.(toMap(data, getKey));
|
|
62
|
-
onReady?.();
|
|
63
|
-
if (options.shape === void 0) {
|
|
64
|
-
checkpoints.resolve({ mutationId: options.client.confirmedMutationWatermark(options.shardKey) });
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const onError = (error) => onErrorHandler?.(error);
|
|
68
|
-
const onCheckpoint = (watermark) => {
|
|
69
|
-
checkpoints.resolve(watermark);
|
|
70
|
-
};
|
|
71
|
-
if (options.shape !== void 0) {
|
|
72
|
-
return options.client.subscribeShape({ args, name: options.shape.name }, onRows, {
|
|
73
|
-
onCheckpoint,
|
|
74
|
-
onError,
|
|
75
|
-
shardKey: options.shape.shardKey
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return options.client.subscribe(options.list, args, onRows, { onCheckpoint, onError, shardKey: options.shardKey });
|
|
79
|
-
};
|
|
80
|
-
const config = {
|
|
81
|
-
// Auto-build ordered (B-tree) indexes for whatever the app's live queries
|
|
82
|
-
// join / filter / sort on, so they stay fast as the dataset grows.
|
|
83
|
-
autoIndex: "eager",
|
|
84
|
-
defaultIndexType: BTreeIndex,
|
|
85
|
-
getKey,
|
|
86
|
-
id: options.id ?? options.list?.__lunoraRef ?? `shape:${options.shape?.name ?? ""}`,
|
|
87
|
-
// `"eager"` syncs at creation; omitted otherwise so the wire stays
|
|
88
|
-
// byte-identical to the lazy default (sync on first subscriber).
|
|
89
|
-
...options.load === "eager" ? { startSync: true } : {},
|
|
90
|
-
sync: {
|
|
91
|
-
sync: (writer) => {
|
|
92
|
-
emit = makeDiffEmit(syncedJson, writer);
|
|
93
|
-
const onError = (error) => {
|
|
94
|
-
writer.markReady();
|
|
95
|
-
options.onError?.(error);
|
|
96
|
-
};
|
|
97
|
-
onErrorHandler = onError;
|
|
98
|
-
if (options.scopeBy === void 0) {
|
|
99
|
-
unsubscribe = openSubscription(options.shape?.args ?? {}, () => {
|
|
100
|
-
writer.markReady();
|
|
101
|
-
});
|
|
102
|
-
} else {
|
|
103
|
-
writer.markReady();
|
|
104
|
-
if (scopedArgs !== void 0) {
|
|
105
|
-
unsubscribe = openSubscription(scopedArgs, void 0);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return () => {
|
|
109
|
-
emit = void 0;
|
|
110
|
-
onErrorHandler = void 0;
|
|
111
|
-
unsubscribe?.();
|
|
112
|
-
unsubscribe = void 0;
|
|
113
|
-
syncedJson.clear();
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const scope = (args) => {
|
|
119
|
-
if (options.scopeBy === void 0) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
scopedArgs = args;
|
|
123
|
-
unsubscribe?.();
|
|
124
|
-
unsubscribe = void 0;
|
|
125
|
-
emit?.(/* @__PURE__ */ new Map());
|
|
126
|
-
if (args === void 0) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (emit !== void 0) {
|
|
130
|
-
unsubscribe = openSubscription(args, void 0);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
return { checkpoints, config, scope };
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export { createCheckpointRegistry, lunoraCollectionOptions };
|