taladb 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/dist/index.browser.mjs +298 -687
- package/dist/index.d.mts +229 -759
- package/dist/index.d.ts +229 -759
- package/dist/index.js +308 -733
- package/dist/index.mjs +304 -721
- package/dist/index.react-native.mjs +298 -687
- package/package.json +13 -12
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
+
/** The three mutation kinds a webhook reports, and the verb each one uses. */
|
|
2
|
+
type WebhookOp = 'insert' | 'update' | 'delete';
|
|
3
|
+
interface WebhookConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Enable outbound webhooks. Defaults to `false`, so a config block without
|
|
6
|
+
* `enabled: true` is inert — safe to commit and safe to ship.
|
|
7
|
+
*/
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
/** Endpoint receiving every event. Required when `enabled` is `true`. */
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
/** Headers sent with every request — typically `Authorization`. */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** Per-op endpoint overrides. Fall back to {@link WebhookConfig.endpoint}. */
|
|
14
|
+
insert_endpoint?: string;
|
|
15
|
+
update_endpoint?: string;
|
|
16
|
+
delete_endpoint?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Collections to report. Omit to report all of them. A `_`-prefixed
|
|
19
|
+
* (reserved) collection is never reported either way.
|
|
20
|
+
*/
|
|
21
|
+
collections?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Document fields stripped from every payload.
|
|
24
|
+
*
|
|
25
|
+
* The reason this exists on a vector database: a document carrying a 768-float
|
|
26
|
+
* embedding is ~9KB of JSON that a webhook receiver almost never wants, on
|
|
27
|
+
* every single write. Naming it here keeps it out of the request body.
|
|
28
|
+
*
|
|
29
|
+
* @example exclude_fields: ['embedding', 'clip_vector']
|
|
30
|
+
*/
|
|
31
|
+
exclude_fields?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Max events held in memory before new ones are dropped. Default 512.
|
|
34
|
+
* Back-pressure is a drop, never a block — a slow endpoint must not be able
|
|
35
|
+
* to stall the write path.
|
|
36
|
+
*/
|
|
37
|
+
max_queue?: number;
|
|
38
|
+
/** Retry attempts per event after the first try. Default 3. */
|
|
39
|
+
retries?: number;
|
|
40
|
+
/** `fetch` implementation. Defaults to the runtime global. */
|
|
41
|
+
fetch?: typeof fetch;
|
|
42
|
+
}
|
|
43
|
+
/** Counters for observability. Read via {@link WebhookDispatcher.stats}. */
|
|
44
|
+
interface WebhookStats {
|
|
45
|
+
/** Queued or in flight. */
|
|
46
|
+
pending: number;
|
|
47
|
+
/** Delivered with a 2xx. */
|
|
48
|
+
delivered: number;
|
|
49
|
+
/** Gave up after exhausting retries, or hit a permanent 4xx. */
|
|
50
|
+
failed: number;
|
|
51
|
+
/** Never attempted — the queue was full when the write committed. */
|
|
52
|
+
dropped: number;
|
|
53
|
+
}
|
|
54
|
+
type WebhookEvent = {
|
|
55
|
+
op: 'insert';
|
|
56
|
+
collection: string;
|
|
57
|
+
id: string;
|
|
58
|
+
document: Document;
|
|
59
|
+
committedAt?: number;
|
|
60
|
+
} | {
|
|
61
|
+
op: 'update';
|
|
62
|
+
collection: string;
|
|
63
|
+
id: string;
|
|
64
|
+
document: Document;
|
|
65
|
+
committedAt?: number;
|
|
66
|
+
} | {
|
|
67
|
+
op: 'delete';
|
|
68
|
+
collection: string;
|
|
69
|
+
id: string;
|
|
70
|
+
document?: Document | null;
|
|
71
|
+
committedAt?: number;
|
|
72
|
+
};
|
|
73
|
+
interface WebhookDispatcher {
|
|
74
|
+
/** True when this collection should report changes. Lets the write wrapper
|
|
75
|
+
* skip pre-image resolution entirely for collections nobody is watching. */
|
|
76
|
+
reports(collection: string): boolean;
|
|
77
|
+
/** Queue an event. Never throws, never blocks the caller. */
|
|
78
|
+
emit(event: WebhookEvent): void;
|
|
79
|
+
/** Resolve once the queue is empty or `timeoutMs` elapses. `true` if drained. */
|
|
80
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
81
|
+
stats(): WebhookStats;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate a webhook config. Throws on the first invalid endpoint.
|
|
85
|
+
*
|
|
86
|
+
* Warns — rather than throws — on plaintext HTTP to a non-localhost host: the
|
|
87
|
+
* payload carries document bodies, so shipping them unencrypted is a real
|
|
88
|
+
* disclosure, but `http://` against a local dev server is routine.
|
|
89
|
+
*/
|
|
90
|
+
declare function validateWebhookConfig(config: WebhookConfig): void;
|
|
91
|
+
/**
|
|
92
|
+
* Build a dispatcher, or return `null` when webhooks are disabled — so the
|
|
93
|
+
* write path can check one nullable field instead of calling into a no-op.
|
|
94
|
+
*/
|
|
95
|
+
declare function createWebhookDispatcher(config: WebhookConfig | undefined): WebhookDispatcher | null;
|
|
96
|
+
|
|
1
97
|
/** Similarity metric used for vector search. */
|
|
2
98
|
type VectorMetric = 'cosine' | 'dot' | 'euclidean';
|
|
3
99
|
interface VectorIndexOptions {
|
|
@@ -104,16 +200,6 @@ type Document = {
|
|
|
104
200
|
_id?: string;
|
|
105
201
|
[key: string]: Value | undefined;
|
|
106
202
|
};
|
|
107
|
-
/**
|
|
108
|
-
* Who authored a write, and therefore whether it replicates outward.
|
|
109
|
-
*
|
|
110
|
-
* - `'local'` *(default)* — an ordinary user write. Replicates to peers as usual.
|
|
111
|
-
* - `'remote'` — a row replicated **in** from an authoritative origin. The origin
|
|
112
|
-
* already has it, so it must never go back out: rows written this way fire no
|
|
113
|
-
* sync events and never appear in `exportChanges()`, and deletes made this way
|
|
114
|
-
* leave no tombstone. Enforced in the engine, not by convention.
|
|
115
|
-
*/
|
|
116
|
-
type WriteOrigin = 'local' | 'remote';
|
|
117
203
|
/**
|
|
118
204
|
* The operators available on a single field.
|
|
119
205
|
*
|
|
@@ -184,18 +270,22 @@ interface Schema<T> {
|
|
|
184
270
|
* without constraining the value's type. */
|
|
185
271
|
type SyncFieldType = 'bool' | 'int' | 'float' | 'str' | 'bytes' | 'array' | 'object' | 'any';
|
|
186
272
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
273
|
+
* Declares this collection's document *shape version*.
|
|
274
|
+
*
|
|
275
|
+
* ::: warning Only `version` is live in 0.11.0
|
|
276
|
+
* This schema was built for the tolerant import path used by `db.sync()`.
|
|
277
|
+
* **Sync was removed in 0.11.0**, and with it the engine-side validator, so
|
|
278
|
+
* `required`, `types`, `renames` and `defaults` are currently inert — nothing
|
|
279
|
+
* reads them. `version` still does real work: it stamps `_v` on locally
|
|
280
|
+
* inserted documents and is the target `migrateDocument` migrates *to*.
|
|
281
|
+
* :::
|
|
282
|
+
*
|
|
283
|
+
* Distinct from {@link Schema} (Zod/Valibot), which is strict and runs on the
|
|
284
|
+
* local `insert` path.
|
|
191
285
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* - `_v` **above** `version` → accepted untouched (the peer is ahead).
|
|
196
|
-
* - a missing/`null` `required` field or a `types` mismatch → **quarantined**
|
|
197
|
-
* (set aside, recoverable via {@link TalaDB.quarantined}), never dropped and
|
|
198
|
-
* never aborting the batch.
|
|
286
|
+
* The import behaviour these fields described — upgrade below `version`, accept
|
|
287
|
+
* above it, quarantine a bad shape rather than drop it — went with the sync
|
|
288
|
+
* path. `TalaDB.quarantined` no longer exists.
|
|
199
289
|
*
|
|
200
290
|
* @example
|
|
201
291
|
* const users = db.collection<User>('users', {
|
|
@@ -216,7 +306,7 @@ interface SyncSchema {
|
|
|
216
306
|
* than silently quarantining the documents they were meant to upgrade.
|
|
217
307
|
*/
|
|
218
308
|
version?: number;
|
|
219
|
-
/**
|
|
309
|
+
/** Inert since 0.11.0 — see {@link SyncSchema}. */
|
|
220
310
|
required?: string[];
|
|
221
311
|
/** Expected primitive type per field. Fields absent here accept any type. */
|
|
222
312
|
types?: Record<string, SyncFieldType>;
|
|
@@ -249,11 +339,11 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
249
339
|
*/
|
|
250
340
|
validateOnRead?: boolean;
|
|
251
341
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
342
|
+
* Document shape version for this collection. See {@link SyncSchema}.
|
|
343
|
+
*
|
|
344
|
+
* Its `required`/`types`/`renames`/`defaults` fields fed the sync import path
|
|
345
|
+
* and are inert since sync was removed in 0.11.0; `version` remains the
|
|
346
|
+
* migration target and the `_v` stamped on local inserts.
|
|
257
347
|
*
|
|
258
348
|
* Declaring a `version` also makes locally-inserted documents carry that `_v`,
|
|
259
349
|
* so they are never mistaken for legacy documents on read.
|
|
@@ -382,42 +472,19 @@ type AggregateStage<T extends Document = Document> = {
|
|
|
382
472
|
};
|
|
383
473
|
/** An ordered aggregation pipeline. */
|
|
384
474
|
type AggregatePipeline<T extends Document = Document> = AggregateStage<T>[];
|
|
475
|
+
/**
|
|
476
|
+
* A document on its way in: `_id` is optional, not forbidden.
|
|
477
|
+
*
|
|
478
|
+
* The engine mints a ULID when none is given, and honours one when it is — that
|
|
479
|
+
* is what makes `deriveDocId` hydration idempotent. Typing these parameters as
|
|
480
|
+
* `Omit<T, '_id'>` rejected the very pattern the documentation recommends.
|
|
481
|
+
*/
|
|
482
|
+
type InsertDoc<T extends Document> = Omit<T, '_id'> & {
|
|
483
|
+
_id?: string;
|
|
484
|
+
};
|
|
385
485
|
interface Collection<T extends Document = Document> {
|
|
386
|
-
insert(doc:
|
|
387
|
-
insertMany(docs:
|
|
388
|
-
/**
|
|
389
|
-
* Upsert many documents **by `_id`**, in a single commit. Existing rows are
|
|
390
|
-
* replaced in place, absent rows are created, and rows not named in `docs` are
|
|
391
|
-
* left alone — so writing page 2 never disturbs page 1.
|
|
392
|
-
*
|
|
393
|
-
* Unlike {@link insertMany}, which discards `_id` and mints a fresh ULID, this
|
|
394
|
-
* *honours* the id you supply. That is the whole point: for a row replicated
|
|
395
|
-
* from a remote origin, pass `_id: deriveDocId(collection, remoteKey)` and every
|
|
396
|
-
* later fetch of that row converges on the same document instead of duplicating
|
|
397
|
-
* it. Idempotent, and safe to run concurrently from a background hydration walk
|
|
398
|
-
* and an on-demand fetch.
|
|
399
|
-
*
|
|
400
|
-
* `origin: 'remote'` marks the rows as replicated in from an authoritative
|
|
401
|
-
* origin, which means they are **never replicated back out** — they will not
|
|
402
|
-
* fire sync events and will not appear in `exportChanges()`. Use it for anything
|
|
403
|
-
* the origin already knows about. Defaults to `'local'`.
|
|
404
|
-
*
|
|
405
|
-
* @example
|
|
406
|
-
* await products.replaceManyWithIds(
|
|
407
|
-
* rows.map((r) => ({ ...r, _id: deriveDocId('products', r.id) })),
|
|
408
|
-
* 'remote',
|
|
409
|
-
* );
|
|
410
|
-
*/
|
|
411
|
-
replaceManyWithIds(docs: T[], origin?: WriteOrigin): Promise<string[]>;
|
|
412
|
-
/**
|
|
413
|
-
* Delete many documents by `_id`, in a single commit. Returns how many were
|
|
414
|
-
* present and removed; unknown ids are skipped.
|
|
415
|
-
*
|
|
416
|
-
* `origin: 'remote'` deletes **without a tombstone**, so the deletion is not
|
|
417
|
-
* replicated outward — correct when the origin is the one that told you the row
|
|
418
|
-
* was deleted. Defaults to `'local'`, which tombstones as usual.
|
|
419
|
-
*/
|
|
420
|
-
deleteManyWithIds(ids: string[], origin?: WriteOrigin): Promise<number>;
|
|
486
|
+
insert(doc: InsertDoc<T>): Promise<string>;
|
|
487
|
+
insertMany(docs: InsertDoc<T>[]): Promise<string[]>;
|
|
421
488
|
find(filter?: Filter<T>): Promise<T[]>;
|
|
422
489
|
findOne(filter: Filter<T>): Promise<T | null>;
|
|
423
490
|
updateOne(filter: Filter<T>, update: Update<T>): Promise<boolean>;
|
|
@@ -592,171 +659,20 @@ interface Collection<T extends Document = Document> {
|
|
|
592
659
|
*/
|
|
593
660
|
subscribeAggregate<R extends Document = Document>(pipeline: AggregatePipeline<T>, callback: (docs: R[]) => void, onError?: (error: unknown) => void): () => void;
|
|
594
661
|
}
|
|
595
|
-
|
|
596
|
-
* A JSON-encoded changeset — the opaque payload exchanged between peers. Produced
|
|
597
|
-
* by {@link TalaDB.exportChanges}, transported by a {@link SyncAdapter}, and
|
|
598
|
-
* consumed by {@link TalaDB.importChanges}. Treat it as an opaque string.
|
|
599
|
-
*/
|
|
600
|
-
type SerializedChangeset = string;
|
|
601
|
-
/** Direction of a sync pass. `'both'` (default) is fully bidirectional. */
|
|
602
|
-
type SyncDirection = 'push' | 'pull' | 'both';
|
|
603
|
-
/**
|
|
604
|
-
* A transport for {@link TalaDB.sync}. Implement `push` to send local changes to
|
|
605
|
-
* a remote, `pull` to fetch remote changes — or both for bidirectional sync.
|
|
606
|
-
* The changeset is an opaque JSON string; move it over any wire you like.
|
|
607
|
-
*/
|
|
608
|
-
interface SyncAdapter {
|
|
609
|
-
/** Send a local changeset to the remote. Required for `'push'` / `'both'`. */
|
|
610
|
-
push?(changeset: SerializedChangeset): Promise<void>;
|
|
611
|
-
/**
|
|
612
|
-
* Fetch remote changes with `changed_at` after `sinceMs` (ms epoch), as a
|
|
613
|
-
* serialized changeset. Return `'[]'` when there is nothing new. Required for
|
|
614
|
-
* `'pull'` / `'both'`.
|
|
615
|
-
*
|
|
616
|
-
* @deprecated in spirit, not in support — wall-clock timestamps are not safe
|
|
617
|
-
* cursors (see {@link CursorSyncAdapter}), which is why every pass built on this
|
|
618
|
-
* method replays the whole collection from zero. Implement
|
|
619
|
-
* {@link CursorSyncAdapter.pullWithCursor} instead when your origin can issue a
|
|
620
|
-
* cursor. Adapters that only implement `pull` keep working unchanged.
|
|
621
|
-
*/
|
|
622
|
-
pull?(sinceMs: number): Promise<SerializedChangeset>;
|
|
623
|
-
}
|
|
624
|
-
/** One page of remote changes, plus where to resume from. */
|
|
625
|
-
interface PullResult {
|
|
626
|
-
/** The changes themselves. `'[]'` when there is nothing new. */
|
|
627
|
-
changeset: SerializedChangeset;
|
|
628
|
-
/**
|
|
629
|
-
* Opaque resume token, issued by the origin. **Never parse this.** It may be a
|
|
630
|
-
* timestamp, a sequence number, an LSN, a snapshot id — that is the origin's
|
|
631
|
-
* business, and treating it as a number is how clients reintroduce the
|
|
632
|
-
* clock-skew bug this type exists to kill.
|
|
633
|
-
*/
|
|
634
|
-
cursor: string;
|
|
635
|
-
/** `true` when more pages remain; call again with the returned `cursor`. */
|
|
636
|
-
hasMore: boolean;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* A {@link SyncAdapter} whose origin can issue a resume cursor.
|
|
640
|
-
*
|
|
641
|
-
* ## Why this exists
|
|
642
|
-
*
|
|
643
|
-
* The original contract is `pull(sinceMs)`, and it cannot be made correct. Author
|
|
644
|
-
* wall-clock timestamps are not safe cursors: a write can commit *after* an export
|
|
645
|
-
* yet carry an *earlier* timestamp, so resuming from "the newest timestamp I saw"
|
|
646
|
-
* silently drops rows. TalaDB's answer was to give up on cursors entirely and
|
|
647
|
-
* replay from zero on every pass — correct, but it re-downloads the whole
|
|
648
|
-
* collection forever, which makes a full local replica of a real catalog
|
|
649
|
-
* unaffordable.
|
|
650
|
-
*
|
|
651
|
-
* The fix is to stop inventing the cursor on the client. The origin issues an
|
|
652
|
-
* opaque token; we store it and hand it back. Whatever ordering guarantee the
|
|
653
|
-
* origin has (a sequence, an LSN, a snapshot) travels with the token, and the
|
|
654
|
-
* client never has to reason about clocks at all.
|
|
655
|
-
*
|
|
656
|
-
* `runSync` feature-detects `pullWithCursor` and prefers it. Adapters that only
|
|
657
|
-
* implement `pull(sinceMs)` are untouched and keep their replay-from-zero
|
|
658
|
-
* behavior.
|
|
659
|
-
*/
|
|
660
|
-
interface CursorSyncAdapter extends SyncAdapter {
|
|
661
|
-
/**
|
|
662
|
-
* Fetch changes after `cursor`, or from the beginning when it is `null`.
|
|
663
|
-
* Returns the changes plus the token to resume from next time.
|
|
664
|
-
*/
|
|
665
|
-
pullWithCursor(cursor: string | null): Promise<PullResult>;
|
|
666
|
-
}
|
|
667
|
-
interface SyncOptions {
|
|
668
|
-
/**
|
|
669
|
-
* Collections to sync. Omit to sync **all** user collections (reserved
|
|
670
|
-
* `_`-prefixed collections are always skipped). Provide an array to sync only
|
|
671
|
-
* those.
|
|
672
|
-
*/
|
|
673
|
-
collections?: string[];
|
|
674
|
-
/**
|
|
675
|
-
* Collections to skip. Applied after `collections` (or after the
|
|
676
|
-
* all-collections default), so `{ exclude: ['logs'] }` means "sync everything
|
|
677
|
-
* except logs".
|
|
678
|
-
*/
|
|
679
|
-
exclude?: string[];
|
|
680
|
-
/** Direction of the pass. Default `'both'` (bidirectional). */
|
|
681
|
-
direction?: SyncDirection;
|
|
682
|
-
/**
|
|
683
|
-
* Names this sync target. Reserved cursor state remains isolated per target
|
|
684
|
-
* for forward compatibility with monotonic server cursors. Default
|
|
685
|
-
* `'default'`.
|
|
686
|
-
*/
|
|
687
|
-
target?: string;
|
|
688
|
-
}
|
|
689
|
-
interface SyncResult {
|
|
690
|
-
/** Number of local changes pushed to the remote. */
|
|
691
|
-
pushed: number;
|
|
692
|
-
/** Number of documents changed locally by the pulled remote changeset. */
|
|
693
|
-
pulled: number;
|
|
694
|
-
/**
|
|
695
|
-
* Documents in the pulled changeset skipped by an import validator (a
|
|
696
|
-
* collection this client does not model). Always `0` when no `syncSchema`
|
|
697
|
-
* applied to the pass.
|
|
698
|
-
*/
|
|
699
|
-
skipped?: number;
|
|
700
|
-
/**
|
|
701
|
-
* Documents in the pulled changeset set aside by an import validator because
|
|
702
|
-
* they failed structural validation. Recoverable via {@link TalaDB.quarantined}.
|
|
703
|
-
* Always `0` when no `syncSchema` applied to the pass.
|
|
704
|
-
*/
|
|
705
|
-
quarantined?: number;
|
|
706
|
-
/** Active sync cursor. Currently `0` because timestamp adapters replay safely. */
|
|
707
|
-
cursor: number;
|
|
708
|
-
}
|
|
709
|
-
/** A document set aside during a validated sync import, with its rejection reason. */
|
|
710
|
-
interface QuarantinedDocument<T extends Document = Document> {
|
|
711
|
-
/** The rejected document, retained verbatim. */
|
|
712
|
-
document: T;
|
|
713
|
-
/** Human-readable reason the document was quarantined. */
|
|
714
|
-
reason: string;
|
|
715
|
-
/** The `changed_at` (ms epoch) the rejected change carried. */
|
|
716
|
-
changedAt: number;
|
|
717
|
-
}
|
|
662
|
+
|
|
718
663
|
interface TalaDB {
|
|
719
664
|
collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
|
|
720
|
-
/**
|
|
721
|
-
* Run one bidirectional sync pass against `adapter`: pull remote changes and
|
|
722
|
-
* merge them (Last-Write-Wins), then push local changes since the last cursor.
|
|
723
|
-
* The cursor is persisted per `target`, so successive calls sync incrementally.
|
|
724
|
-
* Set `direction` to `'push'` or `'pull'` to make it one-way.
|
|
725
|
-
*
|
|
726
|
-
* @example
|
|
727
|
-
* await db.sync(httpAdapter, { collections: ['notes'] }); // bidirectional
|
|
728
|
-
* await db.sync(httpAdapter, { collections: ['logs'], direction: 'push' });
|
|
729
|
-
*/
|
|
730
|
-
sync(adapter: SyncAdapter, options: SyncOptions): Promise<SyncResult>;
|
|
731
|
-
/**
|
|
732
|
-
* Low-level: export changes to `collections` with `changed_at` after `sinceMs`
|
|
733
|
-
* (exclusive) as a serialized changeset. Most apps use {@link TalaDB.sync}.
|
|
734
|
-
*/
|
|
735
|
-
exportChanges(collections: string[], sinceMs: number): Promise<SerializedChangeset>;
|
|
736
|
-
/**
|
|
737
|
-
* Low-level: merge a serialized changeset into the local database via
|
|
738
|
-
* Last-Write-Wins. Returns the number of documents changed. Idempotent —
|
|
739
|
-
* re-importing the same changeset is a no-op.
|
|
740
|
-
*/
|
|
741
|
-
importChanges(changeset: SerializedChangeset): Promise<number>;
|
|
742
665
|
/**
|
|
743
666
|
* Compact the underlying storage file, reclaiming space freed by deletes
|
|
744
667
|
* and updates.
|
|
745
668
|
*
|
|
746
|
-
* Call during idle periods — e.g. once on startup
|
|
747
|
-
*
|
|
669
|
+
* Call during idle periods — e.g. once on startup. No-op on in-memory
|
|
670
|
+
* (IndexedDB-fallback) databases.
|
|
748
671
|
*
|
|
749
672
|
* @example
|
|
750
673
|
* await db.compact();
|
|
751
674
|
*/
|
|
752
675
|
compact(): Promise<void>;
|
|
753
|
-
/**
|
|
754
|
-
* Return the documents set aside in `collection`'s quarantine table by a
|
|
755
|
-
* validated sync import (see {@link SyncSchema}). Empty when nothing was
|
|
756
|
-
* quarantined. Wired on browser and Node.js; resolves to `[]` on runtimes
|
|
757
|
-
* without support.
|
|
758
|
-
*/
|
|
759
|
-
quarantined?<T extends Document = Document>(collection: string): Promise<QuarantinedDocument<T>[]>;
|
|
760
676
|
/**
|
|
761
677
|
* Force any batched (eventual-durability) writes to durable storage, and on
|
|
762
678
|
* the browser also write the IndexedDB fallback snapshot immediately. A
|
|
@@ -764,49 +680,48 @@ interface TalaDB {
|
|
|
764
680
|
* "save now" moments (before checkout, on `visibilitychange`).
|
|
765
681
|
*/
|
|
766
682
|
flush?(): Promise<void>;
|
|
767
|
-
/** Browser HTTP-push queue health, when supported by the active binding. */
|
|
768
|
-
syncStatus?(): Promise<{
|
|
769
|
-
pending: number;
|
|
770
|
-
dropped: number;
|
|
771
|
-
failed: number;
|
|
772
|
-
}>;
|
|
773
|
-
/** Wait for accepted browser HTTP-push events, returning false on timeout. */
|
|
774
|
-
flushSync?(timeoutMs?: number): Promise<boolean>;
|
|
775
|
-
close(): Promise<void>;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/** HTTP push sync settings. */
|
|
779
|
-
interface SyncConfig {
|
|
780
683
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
684
|
+
* Whether this tab's writes land authoritatively, without being forwarded to
|
|
685
|
+
* another tab.
|
|
686
|
+
*
|
|
687
|
+
* **Browser only.** The first tab to open a database becomes the primary and
|
|
688
|
+
* owns the storage; later tabs run an in-memory copy and forward their writes
|
|
689
|
+
* to it over BroadcastChannel. Both the OPFS owner and — where OPFS is
|
|
690
|
+
* unavailable — the tab that publishes the IndexedDB snapshot report `true`.
|
|
691
|
+
*
|
|
692
|
+
* Use it for work that must not run in more than one tab at a time, or that
|
|
693
|
+
* depends on reading its own writes back immediately: a background queue
|
|
694
|
+
* drainer, a scheduled cleanup pass, an outbound sync loop. A secondary tab
|
|
695
|
+
* sees other tabs' writes up to ~500 ms late, and its own writes only once
|
|
696
|
+
* the primary has applied them.
|
|
697
|
+
*
|
|
698
|
+
* Primary status changes during a session — closing the owning tab promotes
|
|
699
|
+
* another — so re-check it rather than caching the answer.
|
|
700
|
+
*
|
|
701
|
+
* Always `true` on Node.js and React Native, where a single process owns the
|
|
702
|
+
* database. May be absent on older `@taladb/web` builds — treat absence as
|
|
703
|
+
* `true`.
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* if (await db.isPrimary?.() ?? true) {
|
|
707
|
+
* await drainOutbox();
|
|
708
|
+
* }
|
|
784
709
|
*/
|
|
785
|
-
|
|
710
|
+
isPrimary?(): Promise<boolean>;
|
|
786
711
|
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
712
|
+
* Change-webhook delivery counters, when the webhook is enabled. All zero
|
|
713
|
+
* (and `pending: 0`) when it is not.
|
|
789
714
|
*/
|
|
790
|
-
|
|
791
|
-
/** HTTP headers sent with every outgoing request (e.g. `Authorization`). */
|
|
792
|
-
headers?: Record<string, string>;
|
|
793
|
-
/** Override the endpoint for `insert` events only. */
|
|
794
|
-
insert_endpoint?: string;
|
|
795
|
-
/** Override the endpoint for `update` events only. */
|
|
796
|
-
update_endpoint?: string;
|
|
797
|
-
/** Override the endpoint for `delete` events only. */
|
|
798
|
-
delete_endpoint?: string;
|
|
715
|
+
webhookStats?(): WebhookStats;
|
|
799
716
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* that the remote endpoint doesn't need.
|
|
804
|
-
*
|
|
805
|
-
* @example
|
|
806
|
-
* exclude_fields: ['embedding', 'clip_vector']
|
|
717
|
+
* Wait for queued change-webhook events to drain. Resolves `true` when the
|
|
718
|
+
* queue emptied, `false` on timeout. Resolves `true` immediately when the
|
|
719
|
+
* webhook is disabled.
|
|
807
720
|
*/
|
|
808
|
-
|
|
721
|
+
flushWebhook?(timeoutMs?: number): Promise<boolean>;
|
|
722
|
+
close(): Promise<void>;
|
|
809
723
|
}
|
|
724
|
+
|
|
810
725
|
/** Storage durability settings. */
|
|
811
726
|
interface DurabilityConfig {
|
|
812
727
|
/**
|
|
@@ -825,65 +740,32 @@ interface DurabilityConfig {
|
|
|
825
740
|
}
|
|
826
741
|
/** Top-level TalaDB configuration. */
|
|
827
742
|
interface TalaDbConfig {
|
|
828
|
-
/**
|
|
829
|
-
|
|
743
|
+
/** Outbound change-webhook configuration. Disabled by default. */
|
|
744
|
+
webhook?: WebhookConfig;
|
|
830
745
|
/** Storage durability configuration. */
|
|
831
746
|
durability?: DurabilityConfig;
|
|
832
747
|
}
|
|
833
748
|
|
|
834
|
-
interface HttpSyncAdapterOptions {
|
|
835
|
-
/** Base URL, e.g. `https://api.example.com/sync`. `/push` and `/pull` are appended. */
|
|
836
|
-
endpoint: string;
|
|
837
|
-
/** Extra headers on every request — typically `Authorization`. */
|
|
838
|
-
headers?: Record<string, string>;
|
|
839
|
-
/**
|
|
840
|
-
* `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
|
|
841
|
-
* React Native). Inject a custom one for tests or non-standard environments.
|
|
842
|
-
*/
|
|
843
|
-
fetch?: typeof fetch;
|
|
844
|
-
/** Paths appended to `endpoint`. Override to match an existing API. */
|
|
845
|
-
paths?: {
|
|
846
|
-
push?: string;
|
|
847
|
-
pull?: string;
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
749
|
/**
|
|
851
|
-
*
|
|
852
|
-
* {@link TalaDB.sync}:
|
|
750
|
+
* Deterministic document ids for rows that already have an identity elsewhere.
|
|
853
751
|
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* });
|
|
859
|
-
* await db.sync(adapter, { collections: ['notes'] });
|
|
860
|
-
* ```
|
|
861
|
-
*/
|
|
862
|
-
declare class HttpSyncAdapter implements SyncAdapter {
|
|
863
|
-
private readonly endpoint;
|
|
864
|
-
private readonly headers;
|
|
865
|
-
private readonly fetchFn;
|
|
866
|
-
private readonly pushPath;
|
|
867
|
-
private readonly pullPath;
|
|
868
|
-
constructor(options: HttpSyncAdapterOptions);
|
|
869
|
-
push(changeset: SerializedChangeset): Promise<void>;
|
|
870
|
-
pull(sinceMs: number): Promise<SerializedChangeset>;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Deterministic document ids for replicated rows.
|
|
752
|
+
* `insert` accepts an `_id`, but it must be a ULID — ids are 16 raw bytes on
|
|
753
|
+
* disk and every index key ends with one, so `'sku-1'` cannot be stored as an
|
|
754
|
+
* id and is rejected. Hashing the origin's primary key into a ULID bridges the
|
|
755
|
+
* two: the same `(collection, key)` always maps to the same document.
|
|
875
756
|
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
757
|
+
* That is what makes hydrating from a server **idempotent** — fetch a page,
|
|
758
|
+
* `insertMany` it, and re-running the same fetch cannot produce a second copy of
|
|
759
|
+
* a row, because the second insert is refused as a duplicate id rather than
|
|
760
|
+
* minting a new document. It is also **resumable** (a bootstrap walk can restart
|
|
761
|
+
* mid-way) and **safe to run concurrently** (an on-demand fetch and a background
|
|
762
|
+
* walk touching the same row converge on one document rather than two).
|
|
880
763
|
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* (
|
|
885
|
-
*
|
|
886
|
-
* one document rather than two).
|
|
764
|
+
* @example
|
|
765
|
+
* const rows = await fetch('/api/products').then((r) => r.json())
|
|
766
|
+
* await products.insertMany(
|
|
767
|
+
* rows.map((row) => ({ ...row, _id: deriveDocId('products', row.sku) })),
|
|
768
|
+
* )
|
|
887
769
|
*
|
|
888
770
|
* ## This must stay byte-identical to the Rust `derive_doc_id`
|
|
889
771
|
*
|
|
@@ -917,457 +799,6 @@ declare class HttpSyncAdapter implements SyncAdapter {
|
|
|
917
799
|
*/
|
|
918
800
|
declare function deriveDocId(collection: string, key: string): string;
|
|
919
801
|
|
|
920
|
-
/**
|
|
921
|
-
* Coverage — "is this collection complete enough, locally, to answer a query
|
|
922
|
-
* without the network?"
|
|
923
|
-
*
|
|
924
|
-
* This is the question the whole coverage-first design turns on, and it is *not*
|
|
925
|
-
* "have I fetched this page?". A replica assembled from whichever pages a user
|
|
926
|
-
* happened to visit is an arbitrary partial subset: it cannot answer a query
|
|
927
|
-
* nobody has asked yet ("products under ₱500" may live on page 43), so every new
|
|
928
|
-
* filter or sort still goes to the network and the local database buys you almost
|
|
929
|
-
* nothing. Coverage is what licenses a purely local read.
|
|
930
|
-
*
|
|
931
|
-
* Two things make it trustworthy:
|
|
932
|
-
*
|
|
933
|
-
* 1. **It is scoped, not per-collection.** `complete` for a bare collection name
|
|
934
|
-
* would leak across users: log in as someone else and you inherit the previous
|
|
935
|
-
* user's "complete" flag *and* their rows. The key is a tuple.
|
|
936
|
-
* 2. **It is a state machine, not a boolean.** Only `complete` authorizes a
|
|
937
|
-
* local-only read. `best-effort` exists precisely so an origin that *cannot*
|
|
938
|
-
* give us a consistent snapshot degrades honestly instead of claiming a
|
|
939
|
-
* completeness it never established.
|
|
940
|
-
*/
|
|
941
|
-
|
|
942
|
-
/** Reserved collection holding one coverage document per replicated scope. */
|
|
943
|
-
declare const COVERAGE_COLLECTION = "__taladb_replica";
|
|
944
|
-
/**
|
|
945
|
-
* What identifies a replicated scope. Every component must be part of the key,
|
|
946
|
-
* because each one changes what "complete" means:
|
|
947
|
-
*
|
|
948
|
-
* - `origin` — two origins are two different datasets.
|
|
949
|
-
* - `collection` — the local collection being filled.
|
|
950
|
-
* - `scope` — the *authorization* slice (a user, a tenant, a store). This is the
|
|
951
|
-
* one that bites: without it, user B logging in inherits user A's completeness.
|
|
952
|
-
* - `projectionVersion` — a replica hydrated with a slimmer projection is not
|
|
953
|
-
* complete for a query that needs the dropped fields.
|
|
954
|
-
* - `schemaVersion` — rows hydrated under an older shape may not satisfy today's.
|
|
955
|
-
*/
|
|
956
|
-
interface CoverageKey {
|
|
957
|
-
origin: string;
|
|
958
|
-
collection: string;
|
|
959
|
-
scope: string;
|
|
960
|
-
projectionVersion: number;
|
|
961
|
-
schemaVersion: number;
|
|
962
|
-
}
|
|
963
|
-
type CoverageState =
|
|
964
|
-
/** Nothing local. */
|
|
965
|
-
{
|
|
966
|
-
status: 'empty';
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* A bootstrap walk is in progress. `snapshot` pins every page to one logical
|
|
970
|
-
* view of the origin; `nextPage` is the durable resume point.
|
|
971
|
-
*/
|
|
972
|
-
| {
|
|
973
|
-
status: 'hydrating';
|
|
974
|
-
snapshot: string;
|
|
975
|
-
nextPage: string | number;
|
|
976
|
-
rowsApplied: number;
|
|
977
|
-
deltaCursor?: string;
|
|
978
|
-
total?: number;
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* The scope is fully local as of `cursor`. **The only state that permits a
|
|
982
|
-
* local-only read.**
|
|
983
|
-
*/
|
|
984
|
-
| {
|
|
985
|
-
status: 'complete';
|
|
986
|
-
cursor: string;
|
|
987
|
-
completedAt: number;
|
|
988
|
-
rowsApplied: number;
|
|
989
|
-
total?: number;
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Every row the origin offered was applied, but the origin could not pin a
|
|
993
|
-
* snapshot, so we cannot *prove* we saw a consistent view — a row that shifted
|
|
994
|
-
* between pages mid-walk may have been missed. Reads must not treat this as
|
|
995
|
-
* authoritative.
|
|
996
|
-
*/
|
|
997
|
-
| {
|
|
998
|
-
status: 'best-effort';
|
|
999
|
-
cursor: string;
|
|
1000
|
-
reason: string;
|
|
1001
|
-
rowsApplied: number;
|
|
1002
|
-
total?: number;
|
|
1003
|
-
}
|
|
1004
|
-
/** Complete once, but known to have fallen behind (e.g. a projection change). */
|
|
1005
|
-
| {
|
|
1006
|
-
status: 'stale';
|
|
1007
|
-
cursor: string;
|
|
1008
|
-
reason: string;
|
|
1009
|
-
}
|
|
1010
|
-
/** The walk failed. `resumeFrom` is where to pick it up. */
|
|
1011
|
-
| {
|
|
1012
|
-
status: 'error';
|
|
1013
|
-
resumeFrom: string | number;
|
|
1014
|
-
snapshot?: string;
|
|
1015
|
-
deltaCursor?: string;
|
|
1016
|
-
rowsApplied?: number;
|
|
1017
|
-
total?: number;
|
|
1018
|
-
error: string;
|
|
1019
|
-
};
|
|
1020
|
-
/**
|
|
1021
|
-
* Serialize a {@link CoverageKey} into a stable string.
|
|
1022
|
-
*
|
|
1023
|
-
* Field order is fixed rather than derived from `Object.keys`, so the key cannot
|
|
1024
|
-
* change meaning if someone reorders the interface — a silent coverage reset,
|
|
1025
|
-
* which would look like "the app re-downloads everything for no reason".
|
|
1026
|
-
*/
|
|
1027
|
-
declare function coverageKey(key: CoverageKey): string;
|
|
1028
|
-
/**
|
|
1029
|
-
* Persistent coverage state, one document per scope.
|
|
1030
|
-
*
|
|
1031
|
-
* The state is stored as a JSON string rather than as structured fields: it is a
|
|
1032
|
-
* discriminated union whose shape varies per variant, and TalaDB documents are
|
|
1033
|
-
* flat. Writing it whole also makes each transition a single atomic write, which
|
|
1034
|
-
* is what lets `markComplete` be the durable commit point of a bootstrap.
|
|
1035
|
-
*/
|
|
1036
|
-
declare class CoverageStore {
|
|
1037
|
-
private readonly col;
|
|
1038
|
-
constructor(db: TalaDB);
|
|
1039
|
-
read(key: CoverageKey): Promise<CoverageState>;
|
|
1040
|
-
write(key: CoverageKey, state: CoverageState): Promise<void>;
|
|
1041
|
-
/** Drop a scope's coverage, forcing a fresh bootstrap on next use. */
|
|
1042
|
-
clear(key: CoverageKey): Promise<void>;
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Whether a local-only read is authorized for this state.
|
|
1046
|
-
*
|
|
1047
|
-
* Deliberately strict: **only `complete`**. `best-effort` is the interesting
|
|
1048
|
-
* exclusion — it means we applied everything the origin gave us, but the origin
|
|
1049
|
-
* could not pin a snapshot, so a row that moved between pages during the walk may
|
|
1050
|
-
* never have been seen. Serving that as authoritative would silently return
|
|
1051
|
-
* incomplete results, which is worse than going to the network.
|
|
1052
|
-
*/
|
|
1053
|
-
declare function isAuthoritative(state: CoverageState): boolean;
|
|
1054
|
-
/** Rows applied so far, for progress reporting. */
|
|
1055
|
-
declare function rowsApplied(state: CoverageState): number;
|
|
1056
|
-
/** Fractional hydration progress, when the origin told us the total. */
|
|
1057
|
-
declare function progress(state: CoverageState): number | undefined;
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* The replication *source* — wire translation, and nothing else.
|
|
1061
|
-
*
|
|
1062
|
-
* A source knows how to talk to one origin: how to ask for a page, how to ask for
|
|
1063
|
-
* changes since a cursor, how to find a row's primary key, and how to shape a row
|
|
1064
|
-
* into a document. It owns **no orchestration**: no batching, no yielding, no
|
|
1065
|
-
* cursor persistence, no coverage transitions, no retry, no dedup. All of that
|
|
1066
|
-
* belongs to the coordinator, which is generic over sources.
|
|
1067
|
-
*
|
|
1068
|
-
* That split is deliberate. The obvious alternative — make the REST origin a
|
|
1069
|
-
* `SyncAdapter` and let `db.sync()` drive it — does not work: a bootstrap of 100k
|
|
1070
|
-
* rows would sit inside a single `pull()` call with no way to report progress,
|
|
1071
|
-
* pause, resume, or yield to the UI between pages. Orchestration has to live one
|
|
1072
|
-
* level up, or it cannot be orchestrated at all.
|
|
1073
|
-
*/
|
|
1074
|
-
|
|
1075
|
-
/** The origin's primary key for a row. Stringified before hashing into an id. */
|
|
1076
|
-
type RemoteKey = string;
|
|
1077
|
-
/** A request for one page of the initial bootstrap walk. */
|
|
1078
|
-
interface BootstrapRequest {
|
|
1079
|
-
/**
|
|
1080
|
-
* Where to resume. `null` on the first call — which is also when the origin is
|
|
1081
|
-
* expected to *issue* the snapshot and delta cursor.
|
|
1082
|
-
*/
|
|
1083
|
-
page: string | number | null;
|
|
1084
|
-
/**
|
|
1085
|
-
* The snapshot token from the first page, echoed back on every subsequent one.
|
|
1086
|
-
* `null` on the first call, and on origins that don't support snapshots.
|
|
1087
|
-
*/
|
|
1088
|
-
snapshot: string | null;
|
|
1089
|
-
/** Rows per page. */
|
|
1090
|
-
limit: number;
|
|
1091
|
-
}
|
|
1092
|
-
/** One page of the bootstrap walk. */
|
|
1093
|
-
interface BootstrapPage<RemoteRow> {
|
|
1094
|
-
rows: RemoteRow[];
|
|
1095
|
-
/** Resume token for the next page; `null` when the walk is done. */
|
|
1096
|
-
nextPage: string | number | null;
|
|
1097
|
-
/**
|
|
1098
|
-
* An opaque token pinning every page of this walk to one logical view of the
|
|
1099
|
-
* origin.
|
|
1100
|
-
*
|
|
1101
|
-
* **Omit it and you get `best-effort` coverage, not `complete`.** Without a
|
|
1102
|
-
* snapshot, a page walk over live data is not a consistent read: fetch page 1,
|
|
1103
|
-
* a row is inserted, everything shifts, and the row that was going to be on
|
|
1104
|
-
* page 20 is now on page 19 — which you already passed. It is never seen. The
|
|
1105
|
-
* walk still "succeeds", and the replica silently has a hole in it. Since
|
|
1106
|
-
* nothing detects that, the honest response is to refuse to call the result
|
|
1107
|
-
* complete, and to keep serving reads from the network.
|
|
1108
|
-
*/
|
|
1109
|
-
snapshot?: string;
|
|
1110
|
-
/**
|
|
1111
|
-
* The cursor to begin the *delta* stream from once the walk finishes. Issued on
|
|
1112
|
-
* the first page — i.e. as of the snapshot — so no change made during the walk
|
|
1113
|
-
* can slip between "bootstrap ended" and "delta began".
|
|
1114
|
-
*/
|
|
1115
|
-
deltaCursor?: string;
|
|
1116
|
-
/** Total rows in scope, when the origin knows it. Drives progress reporting. */
|
|
1117
|
-
total?: number;
|
|
1118
|
-
}
|
|
1119
|
-
/** One batch of incremental changes since a cursor. */
|
|
1120
|
-
interface DeltaPage<RemoteRow> {
|
|
1121
|
-
changed: RemoteRow[];
|
|
1122
|
-
/**
|
|
1123
|
-
* Primary keys the origin has deleted.
|
|
1124
|
-
*
|
|
1125
|
-
* This is the only way a REST replica learns about deletions. A plain paged GET
|
|
1126
|
-
* returns survivors, and a row's *absence* from a response is ambiguous — it may
|
|
1127
|
-
* have been deleted, or it may merely have shifted to another page. Guessing
|
|
1128
|
-
* would eventually delete live data, so we never infer; the origin must say so.
|
|
1129
|
-
*/
|
|
1130
|
-
deleted: RemoteKey[];
|
|
1131
|
-
cursor: string;
|
|
1132
|
-
hasMore: boolean;
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Everything the coordinator needs to replicate one collection from one origin.
|
|
1136
|
-
*
|
|
1137
|
-
* @typeParam RemoteRow - the row shape the origin returns, before mapping.
|
|
1138
|
-
* @typeParam T - the local document shape.
|
|
1139
|
-
*/
|
|
1140
|
-
interface ReplicationSource<RemoteRow = unknown, T extends Document = Document> {
|
|
1141
|
-
/** Bump when a custom source's behavior changes without changing its metadata. */
|
|
1142
|
-
readonly configVersion?: string | number;
|
|
1143
|
-
/** Stable identity for this origin. Part of the coverage key. */
|
|
1144
|
-
readonly origin: string;
|
|
1145
|
-
/** The local collection this source fills. */
|
|
1146
|
-
readonly collection: string;
|
|
1147
|
-
/**
|
|
1148
|
-
* The authorization slice these rows belong to — a user, tenant, or store.
|
|
1149
|
-
* Part of the coverage key, so one user's completeness never licenses another's
|
|
1150
|
-
* reads. Use a constant for genuinely global data.
|
|
1151
|
-
*/
|
|
1152
|
-
readonly scope: string;
|
|
1153
|
-
/** Bump when {@link mapRow} starts producing a different shape. */
|
|
1154
|
-
readonly projectionVersion: number;
|
|
1155
|
-
/** Bump when the local schema changes in a way hydrated rows must match. */
|
|
1156
|
-
readonly schemaVersion: number;
|
|
1157
|
-
/** Fetch one page of the initial walk. */
|
|
1158
|
-
bootstrap(request: BootstrapRequest): Promise<BootstrapPage<RemoteRow>>;
|
|
1159
|
-
/** Fetch changes since `cursor`. Absent when the origin has no delta feed. */
|
|
1160
|
-
delta?(cursor: string): Promise<DeltaPage<RemoteRow>>;
|
|
1161
|
-
/**
|
|
1162
|
-
* Fetch exactly the rows a specific query needs, for the cold-start bridge.
|
|
1163
|
-
*
|
|
1164
|
-
* Optional. When absent, a query against an un-hydrated scope simply waits for
|
|
1165
|
-
* coverage rather than short-circuiting to the network.
|
|
1166
|
-
*/
|
|
1167
|
-
fetchQuery?(query: BridgeQuery): Promise<RemoteRow[]>;
|
|
1168
|
-
/** The origin's primary key for a row. Must be stable across fetches. */
|
|
1169
|
-
keyOf(row: RemoteRow): RemoteKey;
|
|
1170
|
-
/**
|
|
1171
|
-
* Monotonic authoritative revision for stale-response protection. Strongly
|
|
1172
|
-
* recommended whenever bridge/bootstrap/delta requests may overlap.
|
|
1173
|
-
*/
|
|
1174
|
-
revisionOf(row: RemoteRow): number;
|
|
1175
|
-
/** Shape a remote row into a local document (minus `_id`, which is derived). */
|
|
1176
|
-
mapRow(row: RemoteRow): Omit<T, '_id'>;
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* A local query, handed to the bridge so it can ask the origin for the same rows.
|
|
1180
|
-
*
|
|
1181
|
-
* Deliberately loose: every REST API spells pagination and filtering differently,
|
|
1182
|
-
* so translating this into a query string is the source's job, not ours.
|
|
1183
|
-
*/
|
|
1184
|
-
interface BridgeQuery {
|
|
1185
|
-
filter?: Record<string, unknown>;
|
|
1186
|
-
sort?: Record<string, 1 | -1>;
|
|
1187
|
-
page?: number;
|
|
1188
|
-
limit?: number;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/**
|
|
1192
|
-
* The replication coordinator — all orchestration, no wire format.
|
|
1193
|
-
*
|
|
1194
|
-
* Owns: the bootstrap walk, resume-after-crash, delta refresh, the cold-start
|
|
1195
|
-
* bridge, batching, yielding, coverage transitions, and in-flight dedup. The
|
|
1196
|
-
* {@link ReplicationSource} it drives owns only wire translation.
|
|
1197
|
-
*
|
|
1198
|
-
* ## The two mechanisms are one mechanism
|
|
1199
|
-
*
|
|
1200
|
-
* "Fetch the page the user is looking at" and "import the whole catalog in the
|
|
1201
|
-
* background" look like separate features. They are the same primitive with two
|
|
1202
|
-
* schedulers: *fetch rows → upsert them by derived id*. Because both write the
|
|
1203
|
-
* **same rows under the same ids**, they compose for free — a bridged fetch is not
|
|
1204
|
-
* a throwaway cache entry, it is a down payment on the replica, and when the walk
|
|
1205
|
-
* later reaches those rows it overwrites them in place instead of duplicating
|
|
1206
|
-
* them. Nothing has to reconcile the two.
|
|
1207
|
-
*
|
|
1208
|
-
* The one thing they do *not* share is coverage. A bridge fetch must never advance
|
|
1209
|
-
* the bootstrap cursor, because it did not come from the walk's snapshot and
|
|
1210
|
-
* proves nothing about completeness. Trading a little duplicate network for a
|
|
1211
|
-
* trustworthy completeness proof is the right side of that bargain.
|
|
1212
|
-
*/
|
|
1213
|
-
|
|
1214
|
-
interface CoordinatorOptions<T extends Document = Document> {
|
|
1215
|
-
/** Rows per bootstrap page. Larger = fewer commits, longer stalls. */
|
|
1216
|
-
pageSize?: number;
|
|
1217
|
-
/**
|
|
1218
|
-
* Called between pages so the walk yields. Defaults to a macrotask.
|
|
1219
|
-
*
|
|
1220
|
-
* This matters more than it looks. Live queries re-run on a 300 ms poll, and on
|
|
1221
|
-
* React Native every write is *synchronous on the JS thread* — a tight bootstrap
|
|
1222
|
-
* loop starves both, and the UI freezes for the duration of the import.
|
|
1223
|
-
*/
|
|
1224
|
-
yieldFn?: () => Promise<void>;
|
|
1225
|
-
/** Fired after each committed page, for progress UI. */
|
|
1226
|
-
onProgress?: (state: CoverageState) => void;
|
|
1227
|
-
/** Collection schema/migration options registered by the host application. */
|
|
1228
|
-
collectionOptions?: CollectionOptions<T>;
|
|
1229
|
-
}
|
|
1230
|
-
declare const REPLICA_SCOPE_FIELD = "_replica_scope";
|
|
1231
|
-
declare const REPLICA_REVISION_FIELD = "_remote_rev";
|
|
1232
|
-
interface BridgeResult {
|
|
1233
|
-
count: number;
|
|
1234
|
-
ids: string[];
|
|
1235
|
-
}
|
|
1236
|
-
declare class ReplicationCoordinator<RemoteRow, T extends Document> {
|
|
1237
|
-
private readonly db;
|
|
1238
|
-
private readonly source;
|
|
1239
|
-
private readonly coverage;
|
|
1240
|
-
private readonly key;
|
|
1241
|
-
private readonly pageSize;
|
|
1242
|
-
private readonly yieldFn;
|
|
1243
|
-
private readonly onProgress?;
|
|
1244
|
-
private readonly collectionOptions?;
|
|
1245
|
-
/**
|
|
1246
|
-
* In-flight passes, keyed by intent. Two components mounting the same query must
|
|
1247
|
-
* fire one request, and the background walk must not race the bridge for the
|
|
1248
|
-
* same rows — both join the existing promise instead.
|
|
1249
|
-
*/
|
|
1250
|
-
private readonly inflight;
|
|
1251
|
-
constructor(db: TalaDB, source: ReplicationSource<RemoteRow, T>, options?: CoordinatorOptions<T>);
|
|
1252
|
-
get replicaScope(): string;
|
|
1253
|
-
private get identityNamespace();
|
|
1254
|
-
getCoverage(): Promise<CoverageState>;
|
|
1255
|
-
/** Whether a purely local read is authorized right now. */
|
|
1256
|
-
isReady(): Promise<boolean>;
|
|
1257
|
-
/** Dedup by intent: identical concurrent work joins rather than duplicating. */
|
|
1258
|
-
private dedup;
|
|
1259
|
-
/**
|
|
1260
|
-
* Write a batch of remote rows into the local collection.
|
|
1261
|
-
*
|
|
1262
|
-
* One commit for the whole batch, ids derived from the origin's primary key, and
|
|
1263
|
-
* `origin: 'remote'` so the rows can never replicate back out at the origin they
|
|
1264
|
-
* came from. This is the *only* write path in the coordinator — bootstrap, delta
|
|
1265
|
-
* and bridge all funnel through it, which is precisely why they converge instead
|
|
1266
|
-
* of conflicting.
|
|
1267
|
-
*/
|
|
1268
|
-
private applyRows;
|
|
1269
|
-
/**
|
|
1270
|
-
* Hydrate the scope: walk the origin page by page until the whole collection is
|
|
1271
|
-
* local, then mark it complete.
|
|
1272
|
-
*
|
|
1273
|
-
* Resumable and idempotent. If the walk is interrupted — a reload, a crash, a
|
|
1274
|
-
* dead network — the next call picks up from the last committed page, and
|
|
1275
|
-
* re-applying a page it already wrote is a no-op because the ids are derived.
|
|
1276
|
-
*/
|
|
1277
|
-
hydrate(): Promise<CoverageState>;
|
|
1278
|
-
private runHydrate;
|
|
1279
|
-
/**
|
|
1280
|
-
* Apply incremental changes since the stored cursor.
|
|
1281
|
-
*
|
|
1282
|
-
* Deletions are applied by mapping the origin's primary keys through the same
|
|
1283
|
-
* `deriveDocId`, and are written with `origin: 'remote'` so they leave no
|
|
1284
|
-
* tombstone — the origin already knows it deleted these, and a tombstone would
|
|
1285
|
-
* push its own deletion back at it.
|
|
1286
|
-
*/
|
|
1287
|
-
refresh(): Promise<CoverageState>;
|
|
1288
|
-
private runRefresh;
|
|
1289
|
-
/**
|
|
1290
|
-
* Cold-start bridge: fetch exactly the rows one query needs, right now.
|
|
1291
|
-
*
|
|
1292
|
-
* Needed because a SPA or React Native app has no server render to paint behind
|
|
1293
|
-
* while the replica fills. The rows land in the same collection under the same
|
|
1294
|
-
* derived ids as the walk's, so this is not a cache — it is the replica, arriving
|
|
1295
|
-
* early.
|
|
1296
|
-
*
|
|
1297
|
-
* **Does not advance coverage.** These rows did not come from the bootstrap
|
|
1298
|
-
* snapshot and prove nothing about completeness; treating them as progress would
|
|
1299
|
-
* let a page-1 fetch masquerade as a hydrated catalog.
|
|
1300
|
-
*/
|
|
1301
|
-
bridge(query: BridgeQuery): Promise<BridgeResult>;
|
|
1302
|
-
/** Drop coverage and force a fresh bootstrap. Local rows are left alone. */
|
|
1303
|
-
reset(): Promise<void>;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
/**
|
|
1307
|
-
* A {@link ReplicationSource} for an ordinary paged JSON API.
|
|
1308
|
-
*
|
|
1309
|
-
* This is the adoption path: point it at `GET /api/products?page=1&limit=500` and
|
|
1310
|
-
* a team on Express + Postgres gets a local replica without rewriting their API to
|
|
1311
|
-
* speak TalaDB's sync contract. Everything here is wire translation — the
|
|
1312
|
-
* coordinator owns the walk, the coverage, and the retries.
|
|
1313
|
-
*
|
|
1314
|
-
* ## What the origin has to provide, and what happens when it doesn't
|
|
1315
|
-
*
|
|
1316
|
-
* | Feature | Endpoint | Without it |
|
|
1317
|
-
* |---|---|---|
|
|
1318
|
-
* | Paged list | `?page=&limit=` | Nothing works. Required. |
|
|
1319
|
-
* | Snapshot token | `snapshot` in the response | Coverage caps at `best-effort`; reads keep hitting the network |
|
|
1320
|
-
* | Delta feed | `?since=<cursor>` | No incremental refresh, and **deletions never propagate** |
|
|
1321
|
-
*
|
|
1322
|
-
* The snapshot and the delta feed are each about twenty minutes of Express work
|
|
1323
|
-
* (a monotonic `updated_at`/revision column, a soft-delete table, and a
|
|
1324
|
-
* `rev <= snapshotRev` predicate). They are worth it: without a snapshot the
|
|
1325
|
-
* replica can never be trusted for a local-only read, which is the entire point.
|
|
1326
|
-
*/
|
|
1327
|
-
|
|
1328
|
-
interface RestSourceOptions<RemoteRow, T extends Document> {
|
|
1329
|
-
/** Base URL, e.g. `/api/products`. */
|
|
1330
|
-
endpoint: string;
|
|
1331
|
-
/** The local collection to fill. */
|
|
1332
|
-
collection: string;
|
|
1333
|
-
/** Stable identity for the origin. Defaults to `endpoint`. */
|
|
1334
|
-
origin?: string;
|
|
1335
|
-
/**
|
|
1336
|
-
* The authorization slice these rows belong to — a user id, tenant, or store.
|
|
1337
|
-
* Part of the coverage key, so one user's completeness never licenses another
|
|
1338
|
-
* user's reads. Defaults to `'global'`; **set it for anything user-scoped.**
|
|
1339
|
-
*/
|
|
1340
|
-
scope?: string;
|
|
1341
|
-
/** Bump when {@link mapRow} starts producing a different shape. Default 1. */
|
|
1342
|
-
projectionVersion?: number;
|
|
1343
|
-
/** Bump when the local schema changes. Default 1. */
|
|
1344
|
-
schemaVersion?: number;
|
|
1345
|
-
/** Field on the remote row holding its primary key. Default `'id'`. */
|
|
1346
|
-
key?: string;
|
|
1347
|
-
/** Field/callback yielding a monotonic numeric row revision. Default `'rev'`. */
|
|
1348
|
-
revision?: string | ((row: RemoteRow) => number | undefined);
|
|
1349
|
-
/** Shape a remote row into a local document. Default: identity, minus `_id`. */
|
|
1350
|
-
mapRow?: (row: RemoteRow) => Omit<T, '_id'>;
|
|
1351
|
-
/** Per-request headers, resolved **at send time** so a refreshed token is used. */
|
|
1352
|
-
getAuth?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
1353
|
-
/** `fetch` implementation. Defaults to the global. */
|
|
1354
|
-
fetch?: typeof fetch;
|
|
1355
|
-
/** Sub-paths appended to `endpoint`. */
|
|
1356
|
-
paths?: {
|
|
1357
|
-
bootstrap?: string;
|
|
1358
|
-
delta?: string;
|
|
1359
|
-
};
|
|
1360
|
-
/** Enable delta polling. Defaults to true only when `paths.delta` is set. */
|
|
1361
|
-
delta?: boolean;
|
|
1362
|
-
/** Meaning of the fallback `page` parameter when no next token is returned. */
|
|
1363
|
-
pagination?: 'page' | 'offset';
|
|
1364
|
-
/** Translate a local query into this API's query-string conventions. */
|
|
1365
|
-
toParams?: (query: BridgeQuery) => Record<string, string>;
|
|
1366
|
-
/** Pull the row array out of a response whose envelope we don't recognize. */
|
|
1367
|
-
parse?: (body: unknown) => unknown[];
|
|
1368
|
-
}
|
|
1369
|
-
declare function createRestSource<RemoteRow = Record<string, unknown>, T extends Document = Document>(options: RestSourceOptions<RemoteRow, T>): ReplicationSource<RemoteRow, T>;
|
|
1370
|
-
|
|
1371
802
|
/**
|
|
1372
803
|
* Thrown when a document fails schema validation on `insert` or `insertMany`.
|
|
1373
804
|
* The `cause` property holds the original error thrown by the schema library.
|
|
@@ -1391,6 +822,25 @@ declare class TalaDbValidationError extends Error {
|
|
|
1391
822
|
* @internal Exported for unit testing; not part of the public API surface.
|
|
1392
823
|
*/
|
|
1393
824
|
declare function applySchema<T extends Document>(col: Collection<T>, options: CollectionOptions<T>): Collection<T>;
|
|
825
|
+
/**
|
|
826
|
+
* Assemble the decorators that sit between an application and a platform
|
|
827
|
+
* adapter's raw collection. **Order is load-bearing:** webhook reporting goes
|
|
828
|
+
* innermost, schema handling outermost.
|
|
829
|
+
*
|
|
830
|
+
* Schema-outermost means the webhook wrapper sees writes exactly as the engine
|
|
831
|
+
* will run them — filters already narrowed by `writableFilter`, documents
|
|
832
|
+
* already `_v`-stamped — so what gets reported is what actually happened. It
|
|
833
|
+
* also keeps webhook id-resolution reads off the schema read path, where
|
|
834
|
+
* `validateOnRead` could otherwise fail a delete and `persistMigrations` could
|
|
835
|
+
* turn one into a write.
|
|
836
|
+
*
|
|
837
|
+
* Every adapter routes through here so that ordering cannot drift between
|
|
838
|
+
* runtimes, which is the same reason the dispatcher itself is one shared
|
|
839
|
+
* implementation rather than one per binding.
|
|
840
|
+
*
|
|
841
|
+
* @internal Exported for unit testing; not part of the public API surface.
|
|
842
|
+
*/
|
|
843
|
+
declare function decorateCollection<T extends Document>(raw: Collection<T>, name: string, opts: CollectionOptions<T> | undefined, webhook: WebhookDispatcher | null): Collection<T>;
|
|
1394
844
|
|
|
1395
845
|
/**
|
|
1396
846
|
* A single application schema migration, run once at `openDB` when its
|
|
@@ -1453,6 +903,26 @@ interface OpenDBOptions {
|
|
|
1453
903
|
* Useful for passing config programmatically without a config file on disk.
|
|
1454
904
|
*/
|
|
1455
905
|
config?: TalaDbConfig;
|
|
906
|
+
/**
|
|
907
|
+
* Outbound change webhook. Every committed mutation fires one HTTP request:
|
|
908
|
+
* `POST` on insert, `PUT` on update, `DELETE` on delete.
|
|
909
|
+
*
|
|
910
|
+
* Takes precedence over `config.webhook`. This is the only way to enable the
|
|
911
|
+
* webhook on the browser and React Native, where there is no config file to
|
|
912
|
+
* discover. Delivery is best effort with stable idempotency keys across
|
|
913
|
+
* retries — see `webhook.ts`.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* const db = await openDB('app.db', {
|
|
917
|
+
* webhook: {
|
|
918
|
+
* enabled: true,
|
|
919
|
+
* endpoint: 'https://api.example.com/taladb',
|
|
920
|
+
* headers: { Authorization: `Bearer ${token}` },
|
|
921
|
+
* exclude_fields: ['embedding'],
|
|
922
|
+
* },
|
|
923
|
+
* });
|
|
924
|
+
*/
|
|
925
|
+
webhook?: WebhookConfig;
|
|
1456
926
|
/**
|
|
1457
927
|
* Storage durability, e.g. `{ flush_every_write: false }` to batch commits
|
|
1458
928
|
* for write throughput (call `db.flush()` to force a sync), or `{ flush_ms }`
|
|
@@ -1466,17 +936,17 @@ interface OpenDBOptions {
|
|
|
1466
936
|
* Open a TalaDB database.
|
|
1467
937
|
*
|
|
1468
938
|
* @param dbName Name of the database file (used for OPFS and native file paths).
|
|
1469
|
-
* @param options Optional config. Pass `{ config }`
|
|
1470
|
-
* `{ configPath }` to load from a specific file.
|
|
939
|
+
* @param options Optional config. Pass `{ config }` to supply settings inline
|
|
940
|
+
* or `{ configPath }` to load them from a specific file.
|
|
1471
941
|
*
|
|
1472
942
|
* @example
|
|
1473
943
|
* const db = await openDB('myapp.db');
|
|
1474
944
|
*
|
|
1475
|
-
* @example with inline
|
|
945
|
+
* @example with an inline change webhook
|
|
1476
946
|
* const db = await openDB('myapp.db', {
|
|
1477
|
-
*
|
|
947
|
+
* webhook: { enabled: true, endpoint: 'https://api.example.com/taladb' },
|
|
1478
948
|
* });
|
|
1479
949
|
*/
|
|
1480
950
|
declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
|
|
1481
951
|
|
|
1482
|
-
export { type AggregatePipeline, type AggregateStage, type
|
|
952
|
+
export { type AggregatePipeline, type AggregateStage, type Collection, type CollectionIndexInfo, type CollectionOptions, type Document, type DurabilityConfig, type Filter, type HybridSearchOptions, type HybridSearchResult, type InsertDoc, type Migration, type OpenDBOptions, type Schema, type TalaDB, type TalaDbConfig, TalaDbValidationError, type TextSearchOptions, type TextSearchResult, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, type WebhookConfig, type WebhookDispatcher, type WebhookEvent, type WebhookOp, type WebhookStats, applySchema, createWebhookDispatcher, decorateCollection, deriveDocId, openDB, runMigrations, validateWebhookConfig };
|