taladb 0.10.2 → 0.11.1
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 +308 -688
- package/dist/index.d.mts +237 -771
- package/dist/index.d.ts +237 -771
- package/dist/index.js +319 -734
- package/dist/index.mjs +314 -722
- package/dist/index.react-native.mjs +308 -688
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -30,57 +30,298 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
COVERAGE_COLLECTION: () => COVERAGE_COLLECTION,
|
|
34
|
-
CoverageStore: () => CoverageStore,
|
|
35
|
-
HttpSyncAdapter: () => HttpSyncAdapter,
|
|
36
|
-
REPLICA_REVISION_FIELD: () => REPLICA_REVISION_FIELD,
|
|
37
|
-
REPLICA_SCOPE_FIELD: () => REPLICA_SCOPE_FIELD,
|
|
38
|
-
ReplicationCoordinator: () => ReplicationCoordinator,
|
|
39
33
|
TalaDbValidationError: () => TalaDbValidationError,
|
|
40
34
|
applySchema: () => applySchema,
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
createWebhookDispatcher: () => createWebhookDispatcher,
|
|
36
|
+
decorateCollection: () => decorateCollection,
|
|
43
37
|
deriveDocId: () => deriveDocId,
|
|
44
|
-
|
|
38
|
+
isDocId: () => isDocId,
|
|
45
39
|
openDB: () => openDB,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
runMigrations: () => runMigrations
|
|
40
|
+
runMigrations: () => runMigrations,
|
|
41
|
+
validateWebhookConfig: () => validateWebhookConfig
|
|
49
42
|
});
|
|
50
43
|
module.exports = __toCommonJS(index_exports);
|
|
51
44
|
|
|
52
|
-
// src/
|
|
53
|
-
var
|
|
45
|
+
// src/webhook.ts
|
|
46
|
+
var METHOD = {
|
|
47
|
+
insert: "POST",
|
|
48
|
+
update: "PUT",
|
|
49
|
+
delete: "DELETE"
|
|
50
|
+
};
|
|
51
|
+
var ENDPOINT_KEYS = [
|
|
54
52
|
"endpoint",
|
|
55
53
|
"insert_endpoint",
|
|
56
54
|
"update_endpoint",
|
|
57
55
|
"delete_endpoint"
|
|
58
56
|
];
|
|
59
|
-
var
|
|
60
|
-
function
|
|
57
|
+
var LOCALHOST = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
58
|
+
function isLocalhost(url) {
|
|
61
59
|
try {
|
|
62
|
-
return
|
|
60
|
+
return LOCALHOST.has(new URL(url).hostname);
|
|
63
61
|
} catch {
|
|
64
62
|
return false;
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (url !== void 0 && !url.startsWith("http://") && !url.startsWith("https://")) {
|
|
65
|
+
function validateWebhookConfig(config) {
|
|
66
|
+
for (const key of ENDPOINT_KEYS) {
|
|
67
|
+
const url = config[key];
|
|
68
|
+
if (url === void 0) continue;
|
|
69
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
73
70
|
throw new Error(
|
|
74
|
-
`TalaDB
|
|
71
|
+
`TalaDB webhook: invalid ${key} "${url}" \u2014 must start with http:// or https://`
|
|
75
72
|
);
|
|
76
73
|
}
|
|
77
|
-
if (url
|
|
74
|
+
if (url.startsWith("http://") && !isLocalhost(url)) {
|
|
78
75
|
console.warn(
|
|
79
|
-
`[TalaDB]
|
|
76
|
+
`[TalaDB] webhook ${key} "${url}" uses plaintext HTTP \u2014 document bodies will cross the network unencrypted. Use HTTPS in production.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (config.enabled && !config.endpoint) {
|
|
81
|
+
const perOp = ENDPOINT_KEYS.slice(1).every((k) => config[k] !== void 0);
|
|
82
|
+
if (!perOp) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"TalaDB webhook: `enabled: true` requires `endpoint` (or all three per-op endpoints)"
|
|
80
85
|
);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
}
|
|
89
|
+
var DEFAULT_MAX_QUEUE = 512;
|
|
90
|
+
var DEFAULT_RETRIES = 3;
|
|
91
|
+
var BACKOFF_BASE_MS = 200;
|
|
92
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
function stripFields(doc, exclude) {
|
|
94
|
+
if (exclude.size === 0) return doc;
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [k, v] of Object.entries(doc)) {
|
|
97
|
+
if (!exclude.has(k)) out[k] = v;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function createWebhookDispatcher(config) {
|
|
102
|
+
if (!config?.enabled) return null;
|
|
103
|
+
validateWebhookConfig(config);
|
|
104
|
+
const fetchFn = config.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
105
|
+
if (!fetchFn) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"TalaDB webhook: no global fetch available. Pass `fetch` in the webhook config."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const headers = { "content-type": "application/json", ...config.headers ?? {} };
|
|
111
|
+
const exclude = new Set(config.exclude_fields ?? []);
|
|
112
|
+
const only = config.collections ? new Set(config.collections) : null;
|
|
113
|
+
const maxQueue = config.max_queue ?? DEFAULT_MAX_QUEUE;
|
|
114
|
+
const retries = config.retries ?? DEFAULT_RETRIES;
|
|
115
|
+
const stats = { pending: 0, delivered: 0, failed: 0, dropped: 0 };
|
|
116
|
+
const chains = /* @__PURE__ */ new Map();
|
|
117
|
+
let eventSequence = 0;
|
|
118
|
+
function nextEventId() {
|
|
119
|
+
eventSequence++;
|
|
120
|
+
return `${Date.now().toString(36)}-${eventSequence.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
121
|
+
}
|
|
122
|
+
function endpointFor(op) {
|
|
123
|
+
return config[`${op}_endpoint`] ?? config.endpoint;
|
|
124
|
+
}
|
|
125
|
+
function bodyFor(event, at, eventId) {
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
event_id: eventId,
|
|
128
|
+
collection: event.collection,
|
|
129
|
+
id: event.id,
|
|
130
|
+
timestamp: at,
|
|
131
|
+
document: event.document ? stripFields(event.document, exclude) : null
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function deliver(event, at, eventId) {
|
|
135
|
+
const url = endpointFor(event.op);
|
|
136
|
+
const init = {
|
|
137
|
+
method: METHOD[event.op],
|
|
138
|
+
headers: { ...headers, "Idempotency-Key": eventId },
|
|
139
|
+
body: bodyFor(event, at, eventId)
|
|
140
|
+
};
|
|
141
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetchFn(url, init);
|
|
144
|
+
if (res.ok) {
|
|
145
|
+
stats.delivered++;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (res.status >= 400 && res.status < 500) {
|
|
149
|
+
stats.failed++;
|
|
150
|
+
console.warn(
|
|
151
|
+
`[TalaDB] webhook ${event.op} ${event.collection}/${event.id} \u2192 ${res.status} ${res.statusText} (not retried)`
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
if (attempt < retries) await sleep(BACKOFF_BASE_MS * 2 ** attempt);
|
|
158
|
+
}
|
|
159
|
+
stats.failed++;
|
|
160
|
+
console.warn(
|
|
161
|
+
`[TalaDB] webhook ${event.op} ${event.collection}/${event.id} failed after ${retries + 1} attempts`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
reports(collection) {
|
|
166
|
+
if (collection.startsWith("_")) return false;
|
|
167
|
+
return only === null || only.has(collection);
|
|
168
|
+
},
|
|
169
|
+
emit(event) {
|
|
170
|
+
if (!this.reports(event.collection)) return;
|
|
171
|
+
if (stats.pending >= maxQueue) {
|
|
172
|
+
stats.dropped++;
|
|
173
|
+
console.warn(
|
|
174
|
+
`[TalaDB] webhook queue full (${maxQueue}) \u2014 dropped ${event.op} ${event.collection}/${event.id}. The endpoint is not keeping up.`
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
stats.pending++;
|
|
179
|
+
const at = event.committedAt ?? Date.now();
|
|
180
|
+
const eventId = nextEventId();
|
|
181
|
+
const key = `${event.collection}\0${event.id}`;
|
|
182
|
+
const prior = chains.get(key) ?? Promise.resolve();
|
|
183
|
+
const next = prior.then(() => deliver(event, at, eventId)).finally(() => {
|
|
184
|
+
stats.pending--;
|
|
185
|
+
if (chains.get(key) === next) chains.delete(key);
|
|
186
|
+
});
|
|
187
|
+
chains.set(key, next);
|
|
188
|
+
},
|
|
189
|
+
async flush(timeoutMs = 5e3) {
|
|
190
|
+
const deadline = Date.now() + timeoutMs;
|
|
191
|
+
while (stats.pending > 0) {
|
|
192
|
+
const remaining = deadline - Date.now();
|
|
193
|
+
if (remaining <= 0) break;
|
|
194
|
+
let timer;
|
|
195
|
+
const expired = new Promise((resolve) => {
|
|
196
|
+
timer = setTimeout(resolve, remaining);
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
await Promise.race([Promise.allSettled([...chains.values()]), expired]);
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return stats.pending === 0;
|
|
205
|
+
},
|
|
206
|
+
stats() {
|
|
207
|
+
return { ...stats };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function byIds(ids) {
|
|
212
|
+
return ids.length === 1 ? { _id: ids[0] } : { _id: { $in: ids } };
|
|
213
|
+
}
|
|
214
|
+
function idFromFilter(filter) {
|
|
215
|
+
if (filter === null || typeof filter !== "object") return null;
|
|
216
|
+
const keys = Object.keys(filter);
|
|
217
|
+
if (keys.length !== 1 || keys[0] !== "_id") return null;
|
|
218
|
+
const id = filter._id;
|
|
219
|
+
return typeof id === "string" ? id : null;
|
|
220
|
+
}
|
|
221
|
+
function wrapCollectionWithWebhook(col, collection, webhook) {
|
|
222
|
+
if (!webhook.reports(collection)) return col;
|
|
223
|
+
async function idsFor(filter, limitOne) {
|
|
224
|
+
const fast = idFromFilter(filter);
|
|
225
|
+
if (fast !== null) return [fast];
|
|
226
|
+
if (limitOne) {
|
|
227
|
+
const doc = await col.findOne(filter);
|
|
228
|
+
return doc && typeof doc._id === "string" ? [doc._id] : [];
|
|
229
|
+
}
|
|
230
|
+
const docs = col.aggregate ? await col.aggregate([{ $match: filter }, { $project: { _id: 1 } }]) : await col.find(filter);
|
|
231
|
+
return docs.map((d) => d._id).filter((id) => typeof id === "string");
|
|
232
|
+
}
|
|
233
|
+
async function docsFor(filter, limitOne) {
|
|
234
|
+
if (limitOne) {
|
|
235
|
+
const doc = await col.findOne(filter);
|
|
236
|
+
return doc === null ? [] : [doc];
|
|
237
|
+
}
|
|
238
|
+
return col.find(filter);
|
|
239
|
+
}
|
|
240
|
+
async function emitPostImages(ids, op, committedAt) {
|
|
241
|
+
if (ids.length === 0) return;
|
|
242
|
+
const docs = await col.find(byIds(ids));
|
|
243
|
+
const byId = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const doc of docs) {
|
|
245
|
+
if (typeof doc._id === "string") byId.set(doc._id, doc);
|
|
246
|
+
}
|
|
247
|
+
for (const id of ids) {
|
|
248
|
+
const doc = byId.get(id);
|
|
249
|
+
if (doc === void 0) {
|
|
250
|
+
webhook.emit({ op: "delete", collection, id, document: null, committedAt });
|
|
251
|
+
} else {
|
|
252
|
+
webhook.emit({ op, collection, id, document: doc, committedAt });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...col,
|
|
258
|
+
async insert(doc) {
|
|
259
|
+
const id = await col.insert(doc);
|
|
260
|
+
const committedAt = Date.now();
|
|
261
|
+
await emitPostImages([id], "insert", committedAt);
|
|
262
|
+
return id;
|
|
263
|
+
},
|
|
264
|
+
async insertMany(docs) {
|
|
265
|
+
const ids = await col.insertMany(docs);
|
|
266
|
+
const committedAt = Date.now();
|
|
267
|
+
await emitPostImages(ids, "insert", committedAt);
|
|
268
|
+
return ids;
|
|
269
|
+
},
|
|
270
|
+
async updateOne(filter, update) {
|
|
271
|
+
const ids = await idsFor(filter, true);
|
|
272
|
+
const changed = await col.updateOne(filter, update);
|
|
273
|
+
const committedAt = Date.now();
|
|
274
|
+
if (changed) await emitPostImages(ids, "update", committedAt);
|
|
275
|
+
return changed;
|
|
276
|
+
},
|
|
277
|
+
async updateMany(filter, update) {
|
|
278
|
+
const ids = await idsFor(filter, false);
|
|
279
|
+
const n = await col.updateMany(filter, update);
|
|
280
|
+
const committedAt = Date.now();
|
|
281
|
+
if (n > 0) await emitPostImages(ids, "update", committedAt);
|
|
282
|
+
return n;
|
|
283
|
+
},
|
|
284
|
+
async deleteOne(filter) {
|
|
285
|
+
const docs = await docsFor(filter, true);
|
|
286
|
+
const deleted = await col.deleteOne(filter);
|
|
287
|
+
const committedAt = Date.now();
|
|
288
|
+
if (deleted) {
|
|
289
|
+
for (const document of docs) {
|
|
290
|
+
webhook.emit({
|
|
291
|
+
op: "delete",
|
|
292
|
+
collection,
|
|
293
|
+
id: document._id,
|
|
294
|
+
document,
|
|
295
|
+
committedAt
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return deleted;
|
|
300
|
+
},
|
|
301
|
+
async deleteMany(filter) {
|
|
302
|
+
const docs = await docsFor(filter, false);
|
|
303
|
+
const n = await col.deleteMany(filter);
|
|
304
|
+
const committedAt = Date.now();
|
|
305
|
+
if (n > 0) {
|
|
306
|
+
for (const document of docs) {
|
|
307
|
+
webhook.emit({
|
|
308
|
+
op: "delete",
|
|
309
|
+
collection,
|
|
310
|
+
id: document._id,
|
|
311
|
+
document,
|
|
312
|
+
committedAt
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return n;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/config.ts
|
|
322
|
+
function validateConfig(config) {
|
|
323
|
+
if (config.webhook) validateWebhookConfig(config.webhook);
|
|
324
|
+
}
|
|
84
325
|
async function loadConfig(configPath) {
|
|
85
326
|
if (typeof process === "undefined" || typeof process.cwd !== "function") {
|
|
86
327
|
return {};
|
|
@@ -129,146 +370,6 @@ async function loadConfig(configPath) {
|
|
|
129
370
|
return {};
|
|
130
371
|
}
|
|
131
372
|
|
|
132
|
-
// src/sync.ts
|
|
133
|
-
var CURSOR_COLLECTION = "__taladb_sync";
|
|
134
|
-
async function resolveCollections(handle, options) {
|
|
135
|
-
const base = options.collections ?? await handle.listCollectionNames();
|
|
136
|
-
const excluded = new Set(options.exclude ?? []);
|
|
137
|
-
return base.filter((c) => !excluded.has(c) && !c.startsWith("_"));
|
|
138
|
-
}
|
|
139
|
-
function unsupportedSync(runtime) {
|
|
140
|
-
const err = () => new Error(
|
|
141
|
-
`TalaDB sync is not yet available on the ${runtime} runtime (Node.js is supported today; browser and React Native are in progress). Track it on the roadmap.`
|
|
142
|
-
);
|
|
143
|
-
return {
|
|
144
|
-
sync: () => Promise.reject(err()),
|
|
145
|
-
exportChanges: () => Promise.reject(err()),
|
|
146
|
-
importChanges: () => Promise.reject(err())
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
async function readCursor(cursorCol, target) {
|
|
150
|
-
const doc = await cursorCol.findOne({ target });
|
|
151
|
-
return {
|
|
152
|
-
pushMs: doc?.pushMs ?? 0,
|
|
153
|
-
pullMs: doc?.pullMs ?? 0,
|
|
154
|
-
pullCursor: doc?.pullCursor
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
async function writeCursor(cursorCol, target, cursor) {
|
|
158
|
-
const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
|
|
159
|
-
if (!updated) {
|
|
160
|
-
await cursorCol.insert({ target, ...cursor });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function isCursorAdapter(adapter) {
|
|
164
|
-
return typeof adapter.pullWithCursor === "function";
|
|
165
|
-
}
|
|
166
|
-
var MAX_PULL_PAGES = 1e4;
|
|
167
|
-
async function runSync(handle, adapter, options, syncSchemas = {}) {
|
|
168
|
-
const direction = options.direction ?? "both";
|
|
169
|
-
const target = options.target ?? "default";
|
|
170
|
-
const doPush = direction === "push" || direction === "both";
|
|
171
|
-
const doPull = direction === "pull" || direction === "both";
|
|
172
|
-
if (doPull && !adapter.pull && !isCursorAdapter(adapter)) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`sync direction '${direction}' requires adapter.pull() or adapter.pullWithCursor()`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
if (doPush && !adapter.push) {
|
|
178
|
-
throw new Error(`sync direction '${direction}' requires adapter.push()`);
|
|
179
|
-
}
|
|
180
|
-
const collections = await resolveCollections(handle, options);
|
|
181
|
-
const cursorCol = handle.collection(CURSOR_COLLECTION);
|
|
182
|
-
const cursor = await readCursor(cursorCol, target);
|
|
183
|
-
const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
|
|
184
|
-
const scopedSchemas = {};
|
|
185
|
-
for (const c of collections) {
|
|
186
|
-
if (syncSchemas[c]) scopedSchemas[c] = syncSchemas[c];
|
|
187
|
-
}
|
|
188
|
-
const useValidated = handle.importChangesValidated && Object.keys(scopedSchemas).length > 0;
|
|
189
|
-
let pulled = 0;
|
|
190
|
-
let skipped = 0;
|
|
191
|
-
let quarantined = 0;
|
|
192
|
-
let pullCursor = cursor.pullCursor;
|
|
193
|
-
async function importOne(changeset) {
|
|
194
|
-
if (!changeset || changeset === "[]") return;
|
|
195
|
-
if (useValidated) {
|
|
196
|
-
const report = await handle.importChangesValidated(changeset, JSON.stringify(scopedSchemas));
|
|
197
|
-
pulled += report.applied;
|
|
198
|
-
skipped += report.skipped;
|
|
199
|
-
quarantined += report.quarantined;
|
|
200
|
-
} else {
|
|
201
|
-
pulled += await handle.importChanges(changeset);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (doPull) {
|
|
205
|
-
if (isCursorAdapter(adapter)) {
|
|
206
|
-
let pages = 0;
|
|
207
|
-
for (; ; ) {
|
|
208
|
-
const result = await adapter.pullWithCursor(pullCursor ?? null);
|
|
209
|
-
await importOne(result.changeset);
|
|
210
|
-
pullCursor = result.cursor;
|
|
211
|
-
await writeCursor(cursorCol, target, { ...cursor, pullCursor });
|
|
212
|
-
if (!result.hasMore) break;
|
|
213
|
-
if (++pages >= MAX_PULL_PAGES) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
`sync: origin returned hasMore after ${MAX_PULL_PAGES} pages for target '${target}' \u2014 it is probably not advancing its cursor.`
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
await importOne(await adapter.pull(0));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
let pushed = 0;
|
|
224
|
-
if (doPush && local !== "[]") {
|
|
225
|
-
pushed = JSON.parse(local).length;
|
|
226
|
-
await adapter.push(local);
|
|
227
|
-
}
|
|
228
|
-
await writeCursor(cursorCol, target, {
|
|
229
|
-
pushMs: cursor.pushMs,
|
|
230
|
-
pullMs: cursor.pullMs,
|
|
231
|
-
...pullCursor !== void 0 ? { pullCursor } : {}
|
|
232
|
-
});
|
|
233
|
-
return { pushed, pulled, skipped, quarantined, cursor: 0 };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// src/http-adapter.ts
|
|
237
|
-
var HttpSyncAdapter = class {
|
|
238
|
-
constructor(options) {
|
|
239
|
-
this.endpoint = options.endpoint.replace(/\/$/, "");
|
|
240
|
-
this.headers = options.headers ?? {};
|
|
241
|
-
const f = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
242
|
-
if (!f) {
|
|
243
|
-
throw new Error(
|
|
244
|
-
"HttpSyncAdapter: no fetch available. Pass options.fetch on runtimes without a global fetch."
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
this.fetchFn = f;
|
|
248
|
-
this.pushPath = options.paths?.push ?? "/push";
|
|
249
|
-
this.pullPath = options.paths?.pull ?? "/pull";
|
|
250
|
-
}
|
|
251
|
-
async push(changeset) {
|
|
252
|
-
const res = await this.fetchFn(`${this.endpoint}${this.pushPath}`, {
|
|
253
|
-
method: "POST",
|
|
254
|
-
headers: { "content-type": "application/json", ...this.headers },
|
|
255
|
-
body: changeset
|
|
256
|
-
});
|
|
257
|
-
if (!res.ok) {
|
|
258
|
-
throw new Error(`HttpSyncAdapter push failed: ${res.status} ${res.statusText}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async pull(sinceMs) {
|
|
262
|
-
const url = `${this.endpoint}${this.pullPath}?since=${encodeURIComponent(String(sinceMs))}`;
|
|
263
|
-
const res = await this.fetchFn(url, { method: "GET", headers: this.headers });
|
|
264
|
-
if (!res.ok) {
|
|
265
|
-
throw new Error(`HttpSyncAdapter pull failed: ${res.status} ${res.statusText}`);
|
|
266
|
-
}
|
|
267
|
-
const body = (await res.text()).trim();
|
|
268
|
-
return body.length === 0 ? "[]" : body;
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
373
|
// src/derive-id.ts
|
|
273
374
|
var FNV1A128_OFFSET_BASIS = 0x6c62272e07bb014262b821756295c58dn;
|
|
274
375
|
var FNV1A128_PRIME = 0x0000000001000000000000000000013bn;
|
|
@@ -282,6 +383,10 @@ function encodeUlid(value) {
|
|
|
282
383
|
}
|
|
283
384
|
return out;
|
|
284
385
|
}
|
|
386
|
+
var DOC_ID_PATTERN = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;
|
|
387
|
+
function isDocId(value) {
|
|
388
|
+
return typeof value === "string" && DOC_ID_PATTERN.test(value);
|
|
389
|
+
}
|
|
285
390
|
function deriveDocId(collection, key) {
|
|
286
391
|
const bytes = [...UTF8.encode(collection), 0, ...UTF8.encode(key)];
|
|
287
392
|
let hash = FNV1A128_OFFSET_BASIS;
|
|
@@ -292,455 +397,6 @@ function deriveDocId(collection, key) {
|
|
|
292
397
|
return encodeUlid(hash);
|
|
293
398
|
}
|
|
294
399
|
|
|
295
|
-
// src/replication/coverage.ts
|
|
296
|
-
var COVERAGE_COLLECTION = "__taladb_replica";
|
|
297
|
-
function coverageKey(key) {
|
|
298
|
-
return [
|
|
299
|
-
key.origin,
|
|
300
|
-
key.collection,
|
|
301
|
-
key.scope,
|
|
302
|
-
`p${key.projectionVersion}`,
|
|
303
|
-
`s${key.schemaVersion}`
|
|
304
|
-
].map(encodeURIComponent).join("|");
|
|
305
|
-
}
|
|
306
|
-
var CoverageStore = class {
|
|
307
|
-
constructor(db) {
|
|
308
|
-
this.col = db.collection(COVERAGE_COLLECTION);
|
|
309
|
-
}
|
|
310
|
-
async read(key) {
|
|
311
|
-
const doc = await this.col.findOne({ key: coverageKey(key) });
|
|
312
|
-
if (!doc?.state) return { status: "empty" };
|
|
313
|
-
try {
|
|
314
|
-
return JSON.parse(doc.state);
|
|
315
|
-
} catch {
|
|
316
|
-
return { status: "empty" };
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async write(key, state) {
|
|
320
|
-
const k = coverageKey(key);
|
|
321
|
-
const state_json = JSON.stringify(state);
|
|
322
|
-
await this.col.replaceManyWithIds(
|
|
323
|
-
[{ _id: deriveDocId(COVERAGE_COLLECTION, k), key: k, state: state_json }],
|
|
324
|
-
"local"
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
/** Drop a scope's coverage, forcing a fresh bootstrap on next use. */
|
|
328
|
-
async clear(key) {
|
|
329
|
-
const k = coverageKey(key);
|
|
330
|
-
await this.col.deleteManyWithIds([deriveDocId(COVERAGE_COLLECTION, k)], "local");
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
function isAuthoritative(state) {
|
|
334
|
-
return state.status === "complete";
|
|
335
|
-
}
|
|
336
|
-
function rowsApplied(state) {
|
|
337
|
-
switch (state.status) {
|
|
338
|
-
case "hydrating":
|
|
339
|
-
case "complete":
|
|
340
|
-
case "best-effort":
|
|
341
|
-
return state.rowsApplied;
|
|
342
|
-
default:
|
|
343
|
-
return 0;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function progress(state) {
|
|
347
|
-
if (state.status === "complete") return 1;
|
|
348
|
-
if (state.status !== "hydrating" || !state.total) return void 0;
|
|
349
|
-
return Math.min(1, state.rowsApplied / state.total);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// src/replication/coordinator.ts
|
|
353
|
-
var DEFAULT_PAGE_SIZE = 500;
|
|
354
|
-
var defaultYield = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
355
|
-
var MAX_BOOTSTRAP_PAGES = 1e5;
|
|
356
|
-
var inflightByDatabase = /* @__PURE__ */ new WeakMap();
|
|
357
|
-
var REPLICA_SCOPE_FIELD = "_replica_scope";
|
|
358
|
-
var REPLICA_REVISION_FIELD = "_remote_rev";
|
|
359
|
-
var ReplicationCoordinator = class {
|
|
360
|
-
constructor(db, source, options = {}) {
|
|
361
|
-
this.db = db;
|
|
362
|
-
this.source = source;
|
|
363
|
-
this.coverage = new CoverageStore(db);
|
|
364
|
-
this.key = {
|
|
365
|
-
origin: source.origin,
|
|
366
|
-
collection: source.collection,
|
|
367
|
-
scope: source.scope,
|
|
368
|
-
projectionVersion: source.projectionVersion,
|
|
369
|
-
schemaVersion: source.schemaVersion
|
|
370
|
-
};
|
|
371
|
-
this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
372
|
-
this.yieldFn = options.yieldFn ?? defaultYield;
|
|
373
|
-
this.onProgress = options.onProgress;
|
|
374
|
-
this.collectionOptions = options.collectionOptions;
|
|
375
|
-
let shared = inflightByDatabase.get(db);
|
|
376
|
-
if (!shared) {
|
|
377
|
-
shared = /* @__PURE__ */ new Map();
|
|
378
|
-
inflightByDatabase.set(db, shared);
|
|
379
|
-
}
|
|
380
|
-
this.inflight = shared;
|
|
381
|
-
}
|
|
382
|
-
get replicaScope() {
|
|
383
|
-
return coverageKey(this.key);
|
|
384
|
-
}
|
|
385
|
-
get identityNamespace() {
|
|
386
|
-
return `${this.source.origin}\0${this.source.scope}\0${this.source.collection}`;
|
|
387
|
-
}
|
|
388
|
-
getCoverage() {
|
|
389
|
-
return this.coverage.read(this.key);
|
|
390
|
-
}
|
|
391
|
-
/** Whether a purely local read is authorized right now. */
|
|
392
|
-
async isReady() {
|
|
393
|
-
return isAuthoritative(await this.getCoverage());
|
|
394
|
-
}
|
|
395
|
-
/** Dedup by intent: identical concurrent work joins rather than duplicating. */
|
|
396
|
-
dedup(key, run) {
|
|
397
|
-
const existing = this.inflight.get(key);
|
|
398
|
-
if (existing) return existing;
|
|
399
|
-
const pass = run().finally(() => this.inflight.delete(key));
|
|
400
|
-
this.inflight.set(key, pass);
|
|
401
|
-
return pass;
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Write a batch of remote rows into the local collection.
|
|
405
|
-
*
|
|
406
|
-
* One commit for the whole batch, ids derived from the origin's primary key, and
|
|
407
|
-
* `origin: 'remote'` so the rows can never replicate back out at the origin they
|
|
408
|
-
* came from. This is the *only* write path in the coordinator — bootstrap, delta
|
|
409
|
-
* and bridge all funnel through it, which is precisely why they converge instead
|
|
410
|
-
* of conflicting.
|
|
411
|
-
*/
|
|
412
|
-
async applyRows(rows) {
|
|
413
|
-
if (rows.length === 0) return [];
|
|
414
|
-
const col = this.db.collection(this.source.collection, this.collectionOptions);
|
|
415
|
-
const docs = rows.map(
|
|
416
|
-
(row) => {
|
|
417
|
-
const revision = this.source.revisionOf(row);
|
|
418
|
-
return {
|
|
419
|
-
...this.source.mapRow(row),
|
|
420
|
-
_id: deriveDocId(this.identityNamespace, String(this.source.keyOf(row))),
|
|
421
|
-
[REPLICA_SCOPE_FIELD]: this.replicaScope,
|
|
422
|
-
[REPLICA_REVISION_FIELD]: revision
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
);
|
|
426
|
-
await col.replaceManyWithIds(docs, "remote");
|
|
427
|
-
return docs.map((doc) => doc._id);
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Hydrate the scope: walk the origin page by page until the whole collection is
|
|
431
|
-
* local, then mark it complete.
|
|
432
|
-
*
|
|
433
|
-
* Resumable and idempotent. If the walk is interrupted — a reload, a crash, a
|
|
434
|
-
* dead network — the next call picks up from the last committed page, and
|
|
435
|
-
* re-applying a page it already wrote is a no-op because the ids are derived.
|
|
436
|
-
*/
|
|
437
|
-
hydrate() {
|
|
438
|
-
return this.dedup(`${this.replicaScope}:hydrate`, () => this.runHydrate());
|
|
439
|
-
}
|
|
440
|
-
async runHydrate() {
|
|
441
|
-
let state = await this.coverage.read(this.key);
|
|
442
|
-
if (state.status === "complete") return state;
|
|
443
|
-
let page = null;
|
|
444
|
-
let snapshot = null;
|
|
445
|
-
let rowsApplied2 = 0;
|
|
446
|
-
let total;
|
|
447
|
-
let deltaCursor;
|
|
448
|
-
if (state.status === "hydrating") {
|
|
449
|
-
page = state.nextPage;
|
|
450
|
-
snapshot = state.snapshot;
|
|
451
|
-
rowsApplied2 = state.rowsApplied;
|
|
452
|
-
total = state.total;
|
|
453
|
-
deltaCursor = state.deltaCursor;
|
|
454
|
-
} else if (state.status === "error" && state.snapshot) {
|
|
455
|
-
page = state.resumeFrom;
|
|
456
|
-
snapshot = state.snapshot;
|
|
457
|
-
rowsApplied2 = state.rowsApplied ?? 0;
|
|
458
|
-
total = state.total;
|
|
459
|
-
deltaCursor = state.deltaCursor;
|
|
460
|
-
}
|
|
461
|
-
let snapshotSupported = true;
|
|
462
|
-
let pages = 0;
|
|
463
|
-
try {
|
|
464
|
-
for (; ; ) {
|
|
465
|
-
const result = await this.source.bootstrap({ page, snapshot, limit: this.pageSize });
|
|
466
|
-
if (snapshot !== null && result.snapshot !== void 0 && result.snapshot !== snapshot) {
|
|
467
|
-
throw new Error(
|
|
468
|
-
`replication: origin '${this.source.origin}' changed snapshot token mid-walk`
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
if (snapshot === null && result.snapshot) snapshot = result.snapshot;
|
|
472
|
-
if (result.snapshot === void 0 && page === null) snapshotSupported = false;
|
|
473
|
-
if (result.deltaCursor && !deltaCursor) deltaCursor = result.deltaCursor;
|
|
474
|
-
if (result.total !== void 0) total = result.total;
|
|
475
|
-
rowsApplied2 += (await this.applyRows(result.rows)).length;
|
|
476
|
-
page = result.nextPage;
|
|
477
|
-
if (page !== null) {
|
|
478
|
-
const next = {
|
|
479
|
-
status: "hydrating",
|
|
480
|
-
snapshot: snapshot ?? "",
|
|
481
|
-
nextPage: page,
|
|
482
|
-
rowsApplied: rowsApplied2,
|
|
483
|
-
...deltaCursor !== void 0 ? { deltaCursor } : {},
|
|
484
|
-
...total !== void 0 ? { total } : {}
|
|
485
|
-
};
|
|
486
|
-
await this.coverage.write(this.key, next);
|
|
487
|
-
this.onProgress?.(next);
|
|
488
|
-
if (++pages >= MAX_BOOTSTRAP_PAGES) {
|
|
489
|
-
throw new Error(
|
|
490
|
-
`replication: origin '${this.source.origin}' offered more than ${MAX_BOOTSTRAP_PAGES} bootstrap pages for '${this.source.collection}' \u2014 it is probably not advancing nextPage.`
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
await this.yieldFn();
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
if (snapshotSupported && this.source.delta && deltaCursor === void 0) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
`replication: origin '${this.source.origin}' supports delta refresh but did not issue deltaCursor on the first bootstrap page`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
state = snapshotSupported ? {
|
|
502
|
-
status: "complete",
|
|
503
|
-
cursor: deltaCursor ?? "",
|
|
504
|
-
completedAt: Date.now(),
|
|
505
|
-
rowsApplied: rowsApplied2,
|
|
506
|
-
...total !== void 0 ? { total } : {}
|
|
507
|
-
} : {
|
|
508
|
-
// Every row the origin offered was applied — but without a snapshot
|
|
509
|
-
// we cannot prove we saw a consistent view of it, so we must not
|
|
510
|
-
// claim completeness. Reads keep going to the network.
|
|
511
|
-
status: "best-effort",
|
|
512
|
-
cursor: deltaCursor ?? "",
|
|
513
|
-
reason: "the origin did not return a snapshot token, so a row that moved between pages during the walk may have been missed",
|
|
514
|
-
rowsApplied: rowsApplied2,
|
|
515
|
-
...total !== void 0 ? { total } : {}
|
|
516
|
-
};
|
|
517
|
-
await this.coverage.write(this.key, state);
|
|
518
|
-
this.onProgress?.(state);
|
|
519
|
-
return state;
|
|
520
|
-
}
|
|
521
|
-
} catch (error) {
|
|
522
|
-
const failed = {
|
|
523
|
-
status: "error",
|
|
524
|
-
resumeFrom: page ?? 0,
|
|
525
|
-
...snapshot ? { snapshot } : {},
|
|
526
|
-
...deltaCursor !== void 0 ? { deltaCursor } : {},
|
|
527
|
-
rowsApplied: rowsApplied2,
|
|
528
|
-
...total !== void 0 ? { total } : {},
|
|
529
|
-
error: error instanceof Error ? error.message : String(error)
|
|
530
|
-
};
|
|
531
|
-
await this.coverage.write(this.key, failed);
|
|
532
|
-
this.onProgress?.(failed);
|
|
533
|
-
throw error;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Apply incremental changes since the stored cursor.
|
|
538
|
-
*
|
|
539
|
-
* Deletions are applied by mapping the origin's primary keys through the same
|
|
540
|
-
* `deriveDocId`, and are written with `origin: 'remote'` so they leave no
|
|
541
|
-
* tombstone — the origin already knows it deleted these, and a tombstone would
|
|
542
|
-
* push its own deletion back at it.
|
|
543
|
-
*/
|
|
544
|
-
refresh() {
|
|
545
|
-
return this.dedup(`${this.replicaScope}:refresh`, () => this.runRefresh());
|
|
546
|
-
}
|
|
547
|
-
async runRefresh() {
|
|
548
|
-
const state = await this.coverage.read(this.key);
|
|
549
|
-
if (state.status !== "complete") return state;
|
|
550
|
-
if (!this.source.delta) return state;
|
|
551
|
-
const col = this.db.collection(this.source.collection);
|
|
552
|
-
let cursor = state.cursor;
|
|
553
|
-
let rowsApplied2 = state.rowsApplied;
|
|
554
|
-
for (; ; ) {
|
|
555
|
-
const page = await this.source.delta(cursor);
|
|
556
|
-
rowsApplied2 += (await this.applyRows(page.changed)).length;
|
|
557
|
-
if (page.deleted.length > 0) {
|
|
558
|
-
const ids = page.deleted.map(
|
|
559
|
-
(k) => deriveDocId(this.identityNamespace, String(k))
|
|
560
|
-
);
|
|
561
|
-
await col.deleteManyWithIds(ids, "remote");
|
|
562
|
-
}
|
|
563
|
-
cursor = page.cursor;
|
|
564
|
-
const next = { ...state, cursor, rowsApplied: rowsApplied2 };
|
|
565
|
-
await this.coverage.write(this.key, next);
|
|
566
|
-
if (!page.hasMore) {
|
|
567
|
-
this.onProgress?.(next);
|
|
568
|
-
return next;
|
|
569
|
-
}
|
|
570
|
-
await this.yieldFn();
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Cold-start bridge: fetch exactly the rows one query needs, right now.
|
|
575
|
-
*
|
|
576
|
-
* Needed because a SPA or React Native app has no server render to paint behind
|
|
577
|
-
* while the replica fills. The rows land in the same collection under the same
|
|
578
|
-
* derived ids as the walk's, so this is not a cache — it is the replica, arriving
|
|
579
|
-
* early.
|
|
580
|
-
*
|
|
581
|
-
* **Does not advance coverage.** These rows did not come from the bootstrap
|
|
582
|
-
* snapshot and prove nothing about completeness; treating them as progress would
|
|
583
|
-
* let a page-1 fetch masquerade as a hydrated catalog.
|
|
584
|
-
*/
|
|
585
|
-
bridge(query) {
|
|
586
|
-
if (!this.source.fetchQuery) return Promise.resolve({ count: 0, ids: [] });
|
|
587
|
-
const key = `bridge:${coverageKey(this.key)}:${JSON.stringify(query)}`;
|
|
588
|
-
return this.dedup(key, async () => {
|
|
589
|
-
const rows = await this.source.fetchQuery(query);
|
|
590
|
-
const ids = await this.applyRows(rows);
|
|
591
|
-
return { count: ids.length, ids };
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
/** Drop coverage and force a fresh bootstrap. Local rows are left alone. */
|
|
595
|
-
async reset() {
|
|
596
|
-
await this.coverage.clear(this.key);
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
// src/replication/rest.ts
|
|
601
|
-
function parseRows(body, endpoint) {
|
|
602
|
-
if (Array.isArray(body)) return body;
|
|
603
|
-
if (body && typeof body === "object") {
|
|
604
|
-
const env = body;
|
|
605
|
-
for (const field of ["data", "items", "rows"]) {
|
|
606
|
-
const value = env[field];
|
|
607
|
-
if (Array.isArray(value)) return value;
|
|
608
|
-
}
|
|
609
|
-
throw new Error(
|
|
610
|
-
`taladb: could not find a row array in the response from ${endpoint}. Expected a bare array or a { data | items | rows } envelope, but got an object with keys: ${Object.keys(env).join(", ") || "(none)"}. Pass { parse } to extract them yourself.`
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
throw new Error(
|
|
614
|
-
`taladb: expected an array or object from ${endpoint}, got ${typeof body}.`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
function pick(body, names) {
|
|
618
|
-
if (!body || typeof body !== "object") return void 0;
|
|
619
|
-
const rec = body;
|
|
620
|
-
for (const n of names) {
|
|
621
|
-
if (rec[n] !== void 0) return rec[n];
|
|
622
|
-
const meta = rec.meta;
|
|
623
|
-
if (meta && meta[n] !== void 0) return meta[n];
|
|
624
|
-
}
|
|
625
|
-
return void 0;
|
|
626
|
-
}
|
|
627
|
-
function createRestSource(options) {
|
|
628
|
-
const {
|
|
629
|
-
endpoint,
|
|
630
|
-
collection,
|
|
631
|
-
origin = endpoint,
|
|
632
|
-
scope = "global",
|
|
633
|
-
projectionVersion = 1,
|
|
634
|
-
schemaVersion = 1,
|
|
635
|
-
key = "id",
|
|
636
|
-
revision = "rev",
|
|
637
|
-
mapRow,
|
|
638
|
-
getAuth,
|
|
639
|
-
paths,
|
|
640
|
-
toParams,
|
|
641
|
-
parse,
|
|
642
|
-
pagination = "page"
|
|
643
|
-
} = options;
|
|
644
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
645
|
-
async function get(path, params) {
|
|
646
|
-
const url = new URL(path, globalThis.location?.origin ?? "http://localhost");
|
|
647
|
-
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
648
|
-
const headers = getAuth ? await getAuth() : void 0;
|
|
649
|
-
const response = await doFetch(url.href, { headers });
|
|
650
|
-
if (!response.ok) {
|
|
651
|
-
throw new Error(
|
|
652
|
-
`taladb: ${path} responded ${response.status} ${response.statusText}`
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
return response.json();
|
|
656
|
-
}
|
|
657
|
-
const rowsFrom = (body) => parse ? parse(body) : parseRows(body, endpoint);
|
|
658
|
-
return {
|
|
659
|
-
origin,
|
|
660
|
-
collection,
|
|
661
|
-
scope,
|
|
662
|
-
projectionVersion,
|
|
663
|
-
schemaVersion,
|
|
664
|
-
keyOf: (row) => {
|
|
665
|
-
const value = row[key];
|
|
666
|
-
if (value === void 0 || value === null) {
|
|
667
|
-
throw new Error(
|
|
668
|
-
`taladb: row from ${endpoint} has no '${key}' field to use as its primary key. Pass { key } to name the right one. Without a stable key, repeated fetches of the same row cannot be recognized as the same row.`
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
return String(value);
|
|
672
|
-
},
|
|
673
|
-
revisionOf: (row) => {
|
|
674
|
-
const value = typeof revision === "function" ? revision(row) : row[revision];
|
|
675
|
-
if (value === void 0 || value === null) {
|
|
676
|
-
throw new Error(
|
|
677
|
-
`taladb: row from ${endpoint} has no authoritative revision. Pass { revision } to name the monotonic revision field.`
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
const n = Number(value);
|
|
681
|
-
if (!Number.isSafeInteger(n)) {
|
|
682
|
-
throw new Error(`taladb: authoritative revision must be a safe integer, got ${String(value)}`);
|
|
683
|
-
}
|
|
684
|
-
return n;
|
|
685
|
-
},
|
|
686
|
-
mapRow: (row) => {
|
|
687
|
-
if (mapRow) return mapRow(row);
|
|
688
|
-
const { _id, ...rest } = row;
|
|
689
|
-
void _id;
|
|
690
|
-
return rest;
|
|
691
|
-
},
|
|
692
|
-
bootstrap: async (request) => {
|
|
693
|
-
const params = { limit: String(request.limit) };
|
|
694
|
-
if (request.page !== null) params.page = String(request.page);
|
|
695
|
-
if (request.snapshot !== null) params.snapshot = request.snapshot;
|
|
696
|
-
const body = await get(endpoint + (paths?.bootstrap ?? ""), params);
|
|
697
|
-
const rows = rowsFrom(body);
|
|
698
|
-
const nextPage = pick(body, ["nextPage", "next_page", "next"]);
|
|
699
|
-
const snapshot = pick(body, ["snapshot"]);
|
|
700
|
-
const deltaCursor = pick(body, ["deltaCursor", "delta_cursor", "cursor"]);
|
|
701
|
-
const total = pick(body, ["total", "totalCount", "count"]);
|
|
702
|
-
return {
|
|
703
|
-
rows,
|
|
704
|
-
// An origin that reports no explicit `nextPage` is treated as exhausted
|
|
705
|
-
// once it returns a short page — the conventional REST behavior.
|
|
706
|
-
nextPage: nextPage !== void 0 ? nextPage : rows.length < request.limit ? null : pagination === "offset" ? Number(request.page ?? 0) + request.limit : Number(request.page ?? 1) + 1,
|
|
707
|
-
...snapshot !== void 0 ? { snapshot } : {},
|
|
708
|
-
...deltaCursor !== void 0 ? { deltaCursor } : {},
|
|
709
|
-
...total !== void 0 ? { total } : {}
|
|
710
|
-
};
|
|
711
|
-
},
|
|
712
|
-
...options.delta === true || paths?.delta ? { delta: async (cursor) => {
|
|
713
|
-
const body = await get(endpoint + (paths?.delta ?? ""), { since: cursor });
|
|
714
|
-
const changed = rowsFrom(body);
|
|
715
|
-
const deleted = pick(body, ["deleted", "removed"]) ?? [];
|
|
716
|
-
const next = pick(body, ["cursor", "now", "nextCursor"]);
|
|
717
|
-
return {
|
|
718
|
-
changed,
|
|
719
|
-
deleted: deleted.map(String),
|
|
720
|
-
cursor: next ?? cursor,
|
|
721
|
-
hasMore: Boolean(pick(body, ["hasMore", "has_more"]))
|
|
722
|
-
};
|
|
723
|
-
} } : {},
|
|
724
|
-
fetchQuery: async (query) => {
|
|
725
|
-
if (!toParams && Object.values(query.filter ?? {}).some((v) => typeof v === "object" && v !== null)) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
"taladb: bridge filters with operators require RestSourceOptions.toParams; the default translator only supports scalar equality fields."
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
const sortEntry = Object.entries(query.sort ?? {})[0];
|
|
731
|
-
const params = toParams ? toParams(query) : {
|
|
732
|
-
...query.page !== void 0 ? { page: String(query.page) } : {},
|
|
733
|
-
...query.limit !== void 0 ? { limit: String(query.limit) } : {},
|
|
734
|
-
...Object.fromEntries(
|
|
735
|
-
Object.entries(query.filter ?? {}).map(([k, v]) => [k, String(v)])
|
|
736
|
-
),
|
|
737
|
-
...sortEntry ? { sort: sortEntry[0], order: sortEntry[1] === -1 ? "desc" : "asc" } : {}
|
|
738
|
-
};
|
|
739
|
-
return rowsFrom(await get(endpoint, params));
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
400
|
// src/index.ts
|
|
745
401
|
var import_meta = {};
|
|
746
402
|
var TalaDbValidationError = class extends Error {
|
|
@@ -796,10 +452,7 @@ function applySchema(col, options) {
|
|
|
796
452
|
const engineOwned = /* @__PURE__ */ new Set([
|
|
797
453
|
"_id",
|
|
798
454
|
"_v",
|
|
799
|
-
"_changed_at"
|
|
800
|
-
"_remote",
|
|
801
|
-
"_remote_rev",
|
|
802
|
-
"_replica_scope"
|
|
455
|
+
"_changed_at"
|
|
803
456
|
]);
|
|
804
457
|
const downcastViews = /* @__PURE__ */ new WeakSet();
|
|
805
458
|
function preserveFields(original, next, preserveUnknown, preserveVersion = true) {
|
|
@@ -862,10 +515,6 @@ function applySchema(col, options) {
|
|
|
862
515
|
if (!stampVersion || doc._v !== void 0) return doc;
|
|
863
516
|
return { ...doc, _v: targetVersion };
|
|
864
517
|
}
|
|
865
|
-
function stampDoc(doc) {
|
|
866
|
-
if (!stampVersion || doc._v !== void 0) return doc;
|
|
867
|
-
return { ...doc, _v: targetVersion };
|
|
868
|
-
}
|
|
869
518
|
function diffUpdate(original, migrated) {
|
|
870
519
|
const $set = {};
|
|
871
520
|
const $unset = {};
|
|
@@ -956,35 +605,6 @@ function applySchema(col, options) {
|
|
|
956
605
|
if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
957
606
|
return col.insertMany(docs.map(stamp));
|
|
958
607
|
} : col.insertMany.bind(col),
|
|
959
|
-
// Rows arriving from a remote origin are validated like any other write. This
|
|
960
|
-
// is the "parse, don't assert" boundary: the compile-time generic and the
|
|
961
|
-
// runtime schema check have to be the same seam, or a malformed server
|
|
962
|
-
// response walks straight into a typed collection.
|
|
963
|
-
replaceManyWithIds: wrapWrites ? async (docs, origin) => {
|
|
964
|
-
if (origin !== "remote") {
|
|
965
|
-
docs.forEach((doc, i) => assertWritableDocument(doc, `replaceManyWithIds[${i}]`));
|
|
966
|
-
if (targetVersion > 0) {
|
|
967
|
-
throw new Error(
|
|
968
|
-
"local replaceManyWithIds is disabled on versioned collections; use updateOne/updateMany so schema-version guards are atomic"
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
if (schema) docs.forEach((doc, i) => {
|
|
973
|
-
const { _replica_scope, _remote_rev, ...schemaDoc } = doc;
|
|
974
|
-
void _replica_scope;
|
|
975
|
-
void _remote_rev;
|
|
976
|
-
parseWrite(schemaDoc, `replaceManyWithIds[${i}]`);
|
|
977
|
-
});
|
|
978
|
-
return col.replaceManyWithIds(docs.map((d) => stampDoc(d)), origin);
|
|
979
|
-
} : col.replaceManyWithIds.bind(col),
|
|
980
|
-
deleteManyWithIds: stampVersion ? async (ids, origin) => {
|
|
981
|
-
if (origin !== "remote") {
|
|
982
|
-
throw new Error(
|
|
983
|
-
"local deleteManyWithIds is disabled on versioned collections; use deleteOne/deleteMany so schema-version guards are atomic"
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
return col.deleteManyWithIds(ids, origin);
|
|
987
|
-
} : col.deleteManyWithIds.bind(col),
|
|
988
608
|
updateOne: wrapWrites ? async (filter, update) => {
|
|
989
609
|
assertSafeUpdate(update);
|
|
990
610
|
return col.updateOne(writableFilter(filter), update);
|
|
@@ -1048,6 +668,10 @@ function applySchema(col, options) {
|
|
|
1048
668
|
) : col.subscribeAggregate.bind(col)
|
|
1049
669
|
};
|
|
1050
670
|
}
|
|
671
|
+
function decorateCollection(raw, name, opts, webhook) {
|
|
672
|
+
const reported = webhook ? wrapCollectionWithWebhook(raw, name, webhook) : raw;
|
|
673
|
+
return opts ? applySchema(reported, opts) : reported;
|
|
674
|
+
}
|
|
1051
675
|
function detectPlatform() {
|
|
1052
676
|
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
|
|
1053
677
|
return "react-native";
|
|
@@ -1133,7 +757,7 @@ function makePoller(findFn, callback, onError) {
|
|
|
1133
757
|
active = false;
|
|
1134
758
|
};
|
|
1135
759
|
}
|
|
1136
|
-
async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
760
|
+
async function createBrowserDB(dbName, webhook, config, passphrase, migrations) {
|
|
1137
761
|
const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import_meta.url);
|
|
1138
762
|
const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
|
|
1139
763
|
const proxy = new WorkerProxy(worker);
|
|
@@ -1158,9 +782,7 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1158
782
|
}
|
|
1159
783
|
};
|
|
1160
784
|
}
|
|
1161
|
-
const syncSchemas = {};
|
|
1162
785
|
function wrapCollection(name, opts) {
|
|
1163
|
-
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
1164
786
|
const s = JSON.stringify;
|
|
1165
787
|
const wrapped = {
|
|
1166
788
|
insert: (doc) => proxy.send("insert", { collection: name, docJson: s(doc) }),
|
|
@@ -1171,19 +793,6 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1171
793
|
});
|
|
1172
794
|
return JSON.parse(json);
|
|
1173
795
|
},
|
|
1174
|
-
replaceManyWithIds: async (docs, origin = "local") => {
|
|
1175
|
-
const json = await proxy.send("replaceManyWithIds", {
|
|
1176
|
-
collection: name,
|
|
1177
|
-
docsJson: s(docs),
|
|
1178
|
-
origin
|
|
1179
|
-
});
|
|
1180
|
-
return JSON.parse(json);
|
|
1181
|
-
},
|
|
1182
|
-
deleteManyWithIds: (ids, origin = "local") => proxy.send("deleteManyWithIds", {
|
|
1183
|
-
collection: name,
|
|
1184
|
-
idsJson: s(ids),
|
|
1185
|
-
origin
|
|
1186
|
-
}),
|
|
1187
796
|
find: async (filter) => {
|
|
1188
797
|
const json = await proxy.send("find", {
|
|
1189
798
|
collection: name,
|
|
@@ -1298,7 +907,7 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1298
907
|
onError
|
|
1299
908
|
)
|
|
1300
909
|
};
|
|
1301
|
-
return
|
|
910
|
+
return decorateCollection(wrapped, name, opts, webhook);
|
|
1302
911
|
}
|
|
1303
912
|
function nudgedPoller(collection, fetchJson, callback, onError) {
|
|
1304
913
|
let active = true;
|
|
@@ -1367,8 +976,7 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1367
976
|
flush: async () => {
|
|
1368
977
|
await proxy.send("flush");
|
|
1369
978
|
},
|
|
1370
|
-
|
|
1371
|
-
flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
|
|
979
|
+
isPrimary: () => proxy.send("isPrimary"),
|
|
1372
980
|
close: async () => {
|
|
1373
981
|
channel?.close();
|
|
1374
982
|
try {
|
|
@@ -1378,14 +986,7 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1378
986
|
proxy.abort(new Error("taladb worker closed"));
|
|
1379
987
|
}
|
|
1380
988
|
},
|
|
1381
|
-
|
|
1382
|
-
// main thread — a sync pass never blocks rendering, whatever its size.
|
|
1383
|
-
exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
|
|
1384
|
-
importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
|
|
1385
|
-
importChangesValidated: async (changeset, schemasJson) => JSON.parse(await proxy.send("importChangesetValidated", { changesetJson: changeset, schemasJson })),
|
|
1386
|
-
listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
|
|
1387
|
-
quarantined: async (collection) => JSON.parse(await proxy.send("quarantined", { collection })),
|
|
1388
|
-
sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
|
|
989
|
+
listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
|
|
1389
990
|
};
|
|
1390
991
|
if (migrations?.length) {
|
|
1391
992
|
await runMigrations(
|
|
@@ -1399,21 +1000,21 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1399
1000
|
}
|
|
1400
1001
|
return handle;
|
|
1401
1002
|
}
|
|
1402
|
-
async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
1403
|
-
const native = await import(
|
|
1003
|
+
async function createNodeDB(dbName, webhook, config, passphrase, migrations) {
|
|
1004
|
+
const native = await import(
|
|
1005
|
+
/* webpackIgnore: true */
|
|
1006
|
+
/* turbopackIgnore: true */
|
|
1007
|
+
"@taladb/node"
|
|
1008
|
+
);
|
|
1404
1009
|
const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
|
|
1405
1010
|
if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
|
|
1406
1011
|
const configJson = config !== void 0 ? JSON.stringify(config) : null;
|
|
1407
1012
|
const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
|
|
1408
|
-
const syncSchemas = {};
|
|
1409
1013
|
function wrapCollection(name, opts) {
|
|
1410
|
-
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
1411
1014
|
const col = db.collection(name);
|
|
1412
1015
|
const wrapped = {
|
|
1413
1016
|
insert: async (doc) => col.insertAsync ? col.insertAsync(doc) : col.insert(doc),
|
|
1414
1017
|
insertMany: async (docs) => col.insertManyAsync ? col.insertManyAsync(docs) : col.insertMany(docs),
|
|
1415
|
-
replaceManyWithIds: async (docs, origin = "local") => col.replaceManyWithIdsAsync ? col.replaceManyWithIdsAsync(docs, origin) : col.replaceManyWithIds(docs, origin),
|
|
1416
|
-
deleteManyWithIds: async (ids, origin = "local") => col.deleteManyWithIdsAsync ? col.deleteManyWithIdsAsync(ids, origin) : col.deleteManyWithIds(ids, origin),
|
|
1417
1018
|
find: async (filter) => col.findAsync ? col.findAsync(filter ?? null) : col.find(filter ?? null),
|
|
1418
1019
|
findOne: async (filter) => col.findOne(filter) ?? null,
|
|
1419
1020
|
updateOne: async (filter, update) => col.updateOneAsync ? col.updateOneAsync(filter, update) : col.updateOne(filter, update),
|
|
@@ -1448,7 +1049,7 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1448
1049
|
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError),
|
|
1449
1050
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => wrapped.aggregate(pipeline), callback, onError)
|
|
1450
1051
|
};
|
|
1451
|
-
return
|
|
1052
|
+
return decorateCollection(wrapped, name, opts, webhook);
|
|
1452
1053
|
}
|
|
1453
1054
|
const handle = {
|
|
1454
1055
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
@@ -1458,14 +1059,9 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1458
1059
|
flush: db.flush ? async () => {
|
|
1459
1060
|
db.flush();
|
|
1460
1061
|
} : void 0,
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
// so older prebuilt binaries fall back to plain importChanges.
|
|
1465
|
-
importChangesValidated: db.importChangesValidated ? async (changeset, schemasJson) => db.importChangesValidated(changeset, schemasJson) : void 0,
|
|
1466
|
-
listCollectionNames: async () => db.listCollectionNames(),
|
|
1467
|
-
quarantined: async (collection) => db.quarantined ? db.quarantined(collection) : [],
|
|
1468
|
-
sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
|
|
1062
|
+
// One process owns the file — there is no other tab to defer to.
|
|
1063
|
+
isPrimary: async () => true,
|
|
1064
|
+
listCollectionNames: async () => db.listCollectionNames()
|
|
1469
1065
|
};
|
|
1470
1066
|
if (migrations?.length) {
|
|
1471
1067
|
if (typeof db.userVersion !== "function" || typeof db.setUserVersion !== "function") {
|
|
@@ -1480,7 +1076,7 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1480
1076
|
}
|
|
1481
1077
|
return handle;
|
|
1482
1078
|
}
|
|
1483
|
-
async function createNativeDB(_dbName, migrations) {
|
|
1079
|
+
async function createNativeDB(_dbName, webhook, migrations) {
|
|
1484
1080
|
const maybeNative = globalThis.__TalaDB__;
|
|
1485
1081
|
if (!maybeNative) {
|
|
1486
1082
|
throw new Error(
|
|
@@ -1488,14 +1084,10 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1488
1084
|
);
|
|
1489
1085
|
}
|
|
1490
1086
|
const native = maybeNative;
|
|
1491
|
-
const syncSchemas = {};
|
|
1492
1087
|
function wrapCollection(name, opts) {
|
|
1493
|
-
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
1494
1088
|
const wrapped = {
|
|
1495
1089
|
insert: async (doc) => native.insert(name, doc),
|
|
1496
1090
|
insertMany: async (docs) => native.insertMany(name, docs),
|
|
1497
|
-
replaceManyWithIds: async (docs, origin = "local") => native.replaceManyWithIds(name, docs, origin),
|
|
1498
|
-
deleteManyWithIds: async (ids, origin = "local") => native.deleteManyWithIds(name, ids, origin),
|
|
1499
1091
|
find: async (filter) => native.find(name, filter ?? {}),
|
|
1500
1092
|
findOne: async (filter) => native.findOne(name, filter ?? {}),
|
|
1501
1093
|
updateOne: async (filter, update) => native.updateOne(name, filter, update),
|
|
@@ -1542,25 +1134,8 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1542
1134
|
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
|
|
1543
1135
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
|
|
1544
1136
|
};
|
|
1545
|
-
return
|
|
1137
|
+
return decorateCollection(wrapped, name, opts, webhook);
|
|
1546
1138
|
}
|
|
1547
|
-
const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
|
|
1548
|
-
const handle2 = {
|
|
1549
|
-
collection: (name, opts) => wrapCollection(name, opts),
|
|
1550
|
-
exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
|
|
1551
|
-
importChanges: async (changeset) => native.importChanges(changeset),
|
|
1552
|
-
// Feature-detected: present on 0.9.2+ JSI HostObjects; when absent,
|
|
1553
|
-
// runSync falls back to unvalidated importChanges.
|
|
1554
|
-
importChangesValidated: native.importChangesValidated ? async (changeset, schemasJson) => native.importChangesValidated(changeset, schemasJson) : void 0,
|
|
1555
|
-
listCollectionNames: async () => native.listCollectionNames(),
|
|
1556
|
-
sync: (adapter, options) => runSync(handle2, adapter, options, syncSchemas)
|
|
1557
|
-
};
|
|
1558
|
-
return {
|
|
1559
|
-
exportChanges: handle2.exportChanges,
|
|
1560
|
-
importChanges: handle2.importChanges,
|
|
1561
|
-
sync: handle2.sync
|
|
1562
|
-
};
|
|
1563
|
-
})() : unsupportedSync("react-native");
|
|
1564
1139
|
const handle = {
|
|
1565
1140
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
1566
1141
|
compact: async () => native.compact(),
|
|
@@ -1568,8 +1143,8 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1568
1143
|
flush: native.flush ? async () => {
|
|
1569
1144
|
native.flush();
|
|
1570
1145
|
} : void 0,
|
|
1571
|
-
|
|
1572
|
-
|
|
1146
|
+
// One process owns the file — there is no other tab to defer to.
|
|
1147
|
+
isPrimary: async () => true
|
|
1573
1148
|
};
|
|
1574
1149
|
if (migrations?.length) {
|
|
1575
1150
|
if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
|
|
@@ -1621,36 +1196,46 @@ async function openDB(dbName = "taladb.db", options) {
|
|
|
1621
1196
|
durability: { ...resolvedConfig?.durability, ...options.durability }
|
|
1622
1197
|
};
|
|
1623
1198
|
}
|
|
1199
|
+
const webhook = createWebhookDispatcher(options?.webhook ?? resolvedConfig?.webhook);
|
|
1624
1200
|
const platform = detectPlatform();
|
|
1625
1201
|
const migrations = options?.migrations;
|
|
1202
|
+
let db;
|
|
1626
1203
|
switch (platform) {
|
|
1627
1204
|
case "browser":
|
|
1628
|
-
|
|
1205
|
+
db = await createBrowserDB(dbName, webhook, resolvedConfig, options?.passphrase, migrations);
|
|
1206
|
+
break;
|
|
1629
1207
|
case "react-native":
|
|
1630
1208
|
if (options?.passphrase !== void 0) {
|
|
1631
1209
|
throw new Error("On React Native, pass the passphrase in the config JSON to TalaDBModule.initialize(); refusing to assume the already-open native database is encrypted");
|
|
1632
1210
|
}
|
|
1633
|
-
|
|
1211
|
+
db = await createNativeDB(dbName, webhook, migrations);
|
|
1212
|
+
break;
|
|
1634
1213
|
case "node":
|
|
1635
|
-
|
|
1214
|
+
db = await createNodeDB(dbName, webhook, resolvedConfig, options?.passphrase, migrations);
|
|
1215
|
+
break;
|
|
1636
1216
|
}
|
|
1217
|
+
return webhook ? attachWebhook(db, webhook) : db;
|
|
1218
|
+
}
|
|
1219
|
+
function attachWebhook(db, webhook) {
|
|
1220
|
+
return {
|
|
1221
|
+
...db,
|
|
1222
|
+
webhookStats: () => webhook.stats(),
|
|
1223
|
+
flushWebhook: (timeoutMs) => webhook.flush(timeoutMs),
|
|
1224
|
+
async close() {
|
|
1225
|
+
await webhook.flush();
|
|
1226
|
+
await db.close();
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1637
1229
|
}
|
|
1638
1230
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1639
1231
|
0 && (module.exports = {
|
|
1640
|
-
COVERAGE_COLLECTION,
|
|
1641
|
-
CoverageStore,
|
|
1642
|
-
HttpSyncAdapter,
|
|
1643
|
-
REPLICA_REVISION_FIELD,
|
|
1644
|
-
REPLICA_SCOPE_FIELD,
|
|
1645
|
-
ReplicationCoordinator,
|
|
1646
1232
|
TalaDbValidationError,
|
|
1647
1233
|
applySchema,
|
|
1648
|
-
|
|
1649
|
-
|
|
1234
|
+
createWebhookDispatcher,
|
|
1235
|
+
decorateCollection,
|
|
1650
1236
|
deriveDocId,
|
|
1651
|
-
|
|
1237
|
+
isDocId,
|
|
1652
1238
|
openDB,
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
runMigrations
|
|
1239
|
+
runMigrations,
|
|
1240
|
+
validateWebhookConfig
|
|
1656
1241
|
});
|