@powerhousedao/reactor 6.1.0-dev.3 → 6.1.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-worker-executor-DDVXB921.js +83 -0
- package/dist/build-worker-executor-DDVXB921.js.map +1 -0
- package/dist/document-indexer-B2iLRB0o.js +917 -0
- package/dist/document-indexer-B2iLRB0o.js.map +1 -0
- package/dist/drive-container-types-BNpMlgT_.js +2964 -0
- package/dist/drive-container-types-BNpMlgT_.js.map +1 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +313 -0
- package/dist/entry.js.map +1 -0
- package/dist/error-info-Cpu4OY3o.js +62 -0
- package/dist/error-info-Cpu4OY3o.js.map +1 -0
- package/dist/errors-D3S6Eysd.js +56 -0
- package/dist/errors-D3S6Eysd.js.map +1 -0
- package/dist/forwarding-logger-BBkMSxuJ.js +85 -0
- package/dist/forwarding-logger-BBkMSxuJ.js.map +1 -0
- package/dist/index.d.ts +990 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +889 -3890
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +1 -0
- package/dist/projection-entry.js +406 -0
- package/dist/projection-entry.js.map +1 -0
- package/dist/projection-shard-manager-_c7orNo5.js +313 -0
- package/dist/projection-shard-manager-_c7orNo5.js.map +1 -0
- package/dist/projection-worker-wI4PwcV2.js +13 -0
- package/dist/projection-worker-wI4PwcV2.js.map +1 -0
- package/dist/transport-ByGviWdZ.js +33 -0
- package/dist/transport-ByGviWdZ.js.map +1 -0
- package/dist/transport-CuogVKN_.js +23 -0
- package/dist/transport-CuogVKN_.js.map +1 -0
- package/dist/types-CxSpmNGK.js +32 -0
- package/dist/types-CxSpmNGK.js.map +1 -0
- package/dist/worker-SUoDhurA.js +22 -0
- package/dist/worker-SUoDhurA.js.map +1 -0
- package/dist/worker-handle-B1w03nRA.js +383 -0
- package/dist/worker-handle-B1w03nRA.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,2964 @@
|
|
|
1
|
+
import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-CxSpmNGK.js";
|
|
2
|
+
import { createPresignedHeader, defaultBaseState, deriveOperationId, isUndoRedo } from "@powerhousedao/shared/document-model";
|
|
3
|
+
import { v4 } from "uuid";
|
|
4
|
+
import { Migrator, sql } from "kysely";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/shared/utils.ts
|
|
18
|
+
function matchesScope(view = {}, scope) {
|
|
19
|
+
if (view.scopes) return view.scopes.includes(scope);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function yieldToMain() {
|
|
23
|
+
const s = globalThis.scheduler;
|
|
24
|
+
if (s?.yield) return s.yield();
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
26
|
+
}
|
|
27
|
+
const defaultAbortError = () => /* @__PURE__ */ new Error("Operation aborted");
|
|
28
|
+
function throwIfAborted(signal, makeError = defaultAbortError) {
|
|
29
|
+
if (signal?.aborted) throw makeError();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates PagingOptions and returns a normalized offset and limit.
|
|
33
|
+
* Throws if the cursor is not empty and not a non-negative integer, or if
|
|
34
|
+
* limit is less than 1. When `paging` is undefined, returns offset 0 and
|
|
35
|
+
* the caller-supplied `defaultLimit`.
|
|
36
|
+
*/
|
|
37
|
+
function parsePagingOptions(paging, defaultLimit) {
|
|
38
|
+
if (paging === void 0) return {
|
|
39
|
+
offset: 0,
|
|
40
|
+
limit: defaultLimit
|
|
41
|
+
};
|
|
42
|
+
if (!Number.isInteger(paging.limit) || paging.limit < 1) throw new Error(`Invalid paging limit: ${String(paging.limit)} (must be an integer >= 1)`);
|
|
43
|
+
if (paging.cursor === "") return {
|
|
44
|
+
offset: 0,
|
|
45
|
+
limit: paging.limit
|
|
46
|
+
};
|
|
47
|
+
const parsed = Number(paging.cursor);
|
|
48
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`Invalid paging cursor: ${JSON.stringify(paging.cursor)} (must be empty or a non-negative integer)`);
|
|
49
|
+
return {
|
|
50
|
+
offset: parsed,
|
|
51
|
+
limit: paging.limit
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/shared/errors.ts
|
|
56
|
+
/**
|
|
57
|
+
* Error thrown when attempting to access a deleted document.
|
|
58
|
+
*/
|
|
59
|
+
var DocumentDeletedError = class DocumentDeletedError extends Error {
|
|
60
|
+
documentId;
|
|
61
|
+
deletedAtUtcIso;
|
|
62
|
+
constructor(documentId, deletedAtUtcIso = null) {
|
|
63
|
+
const message = deletedAtUtcIso ? `Document ${documentId} was deleted at ${deletedAtUtcIso}` : `Document ${documentId} has been deleted`;
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "DocumentDeletedError";
|
|
66
|
+
this.documentId = documentId;
|
|
67
|
+
this.deletedAtUtcIso = deletedAtUtcIso;
|
|
68
|
+
Error.captureStackTrace(this, DocumentDeletedError);
|
|
69
|
+
}
|
|
70
|
+
static isError(error) {
|
|
71
|
+
return Error.isError(error) && error.name === "DocumentDeletedError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Error thrown when an operation has an invalid signature.
|
|
76
|
+
*/
|
|
77
|
+
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
78
|
+
documentId;
|
|
79
|
+
reason;
|
|
80
|
+
constructor(documentId, reason) {
|
|
81
|
+
super(`Invalid signature in document ${documentId}: ${reason}`);
|
|
82
|
+
this.name = "InvalidSignatureError";
|
|
83
|
+
this.documentId = documentId;
|
|
84
|
+
this.reason = reason;
|
|
85
|
+
Error.captureStackTrace(this, InvalidSignatureError);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Error thrown when attempting to downgrade a document version.
|
|
90
|
+
*/
|
|
91
|
+
var DowngradeNotSupportedError$1 = class DowngradeNotSupportedError$1 extends Error {
|
|
92
|
+
documentType;
|
|
93
|
+
fromVersion;
|
|
94
|
+
toVersion;
|
|
95
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
96
|
+
super(`Downgrade not supported for ${documentType}: cannot upgrade from version ${fromVersion} to ${toVersion}`);
|
|
97
|
+
this.name = "DowngradeNotSupportedError";
|
|
98
|
+
this.documentType = documentType;
|
|
99
|
+
this.fromVersion = fromVersion;
|
|
100
|
+
this.toVersion = toVersion;
|
|
101
|
+
Error.captureStackTrace(this, DowngradeNotSupportedError$1);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Error thrown when a document is not found (no operations exist for the document ID).
|
|
106
|
+
*/
|
|
107
|
+
var DocumentNotFoundError = class DocumentNotFoundError extends Error {
|
|
108
|
+
documentId;
|
|
109
|
+
constructor(documentId) {
|
|
110
|
+
super(`Document ${documentId} not found`);
|
|
111
|
+
this.name = "DocumentNotFoundError";
|
|
112
|
+
this.documentId = documentId;
|
|
113
|
+
Error.captureStackTrace(this, DocumentNotFoundError);
|
|
114
|
+
}
|
|
115
|
+
static isError(error) {
|
|
116
|
+
return Error.isError(error) && error.name === "DocumentNotFoundError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/registry/errors.ts
|
|
121
|
+
/**
|
|
122
|
+
* Error thrown when a document model module is not found in the registry.
|
|
123
|
+
*/
|
|
124
|
+
var ModuleNotFoundError = class extends Error {
|
|
125
|
+
documentType;
|
|
126
|
+
requestedVersion;
|
|
127
|
+
constructor(documentType, version) {
|
|
128
|
+
const versionSuffix = version !== void 0 ? ` version ${version}` : "";
|
|
129
|
+
super(`Document model module not found for type: ${documentType}${versionSuffix}`);
|
|
130
|
+
this.name = "ModuleNotFoundError";
|
|
131
|
+
this.documentType = documentType;
|
|
132
|
+
this.requestedVersion = version;
|
|
133
|
+
}
|
|
134
|
+
static isError(error) {
|
|
135
|
+
return Error.isError(error) && error.name === "ModuleNotFoundError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Error thrown when attempting to register a module that already exists.
|
|
140
|
+
*/
|
|
141
|
+
var DuplicateModuleError = class extends Error {
|
|
142
|
+
constructor(documentType, version) {
|
|
143
|
+
const versionSuffix = version !== void 0 ? ` (version ${version})` : "";
|
|
144
|
+
super(`Document model module already registered for type: ${documentType}${versionSuffix}`);
|
|
145
|
+
this.name = "DuplicateModuleError";
|
|
146
|
+
}
|
|
147
|
+
static isError(error) {
|
|
148
|
+
return Error.isError(error) && error.name === "DuplicateModuleError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Error thrown when a module is invalid or malformed.
|
|
153
|
+
*/
|
|
154
|
+
var InvalidModuleError = class extends Error {
|
|
155
|
+
constructor(message) {
|
|
156
|
+
super(`Invalid document model module: ${message}`);
|
|
157
|
+
this.name = "InvalidModuleError";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Error thrown when attempting to register an upgrade manifest that already exists.
|
|
162
|
+
*/
|
|
163
|
+
var DuplicateManifestError = class extends Error {
|
|
164
|
+
constructor(documentType) {
|
|
165
|
+
super(`Upgrade manifest already registered for type: ${documentType}`);
|
|
166
|
+
this.name = "DuplicateManifestError";
|
|
167
|
+
}
|
|
168
|
+
static isError(error) {
|
|
169
|
+
return Error.isError(error) && error.name === "DuplicateManifestError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Error thrown when an upgrade manifest is not found.
|
|
174
|
+
*/
|
|
175
|
+
var ManifestNotFoundError = class extends Error {
|
|
176
|
+
constructor(documentType) {
|
|
177
|
+
super(`Upgrade manifest not found for type: ${documentType}`);
|
|
178
|
+
this.name = "ManifestNotFoundError";
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Error thrown when attempting a downgrade operation.
|
|
183
|
+
*/
|
|
184
|
+
var DowngradeNotSupportedError = class extends Error {
|
|
185
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
186
|
+
super(`Downgrade not supported for ${documentType}: cannot go from version ${fromVersion} to ${toVersion}`);
|
|
187
|
+
this.name = "DowngradeNotSupportedError";
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Error thrown when a required upgrade transition is missing from the manifest.
|
|
192
|
+
*/
|
|
193
|
+
var MissingUpgradeTransitionError = class extends Error {
|
|
194
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
195
|
+
super(`Missing upgrade transition for ${documentType}: v${fromVersion} to v${toVersion}`);
|
|
196
|
+
this.name = "MissingUpgradeTransitionError";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Error thrown when getUpgradeReducer is called with a non-single-step version increment.
|
|
201
|
+
*/
|
|
202
|
+
var InvalidUpgradeStepError = class extends Error {
|
|
203
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
204
|
+
super(`Invalid upgrade step for ${documentType}: must be single version increment, got v${fromVersion} to v${toVersion}`);
|
|
205
|
+
this.name = "InvalidUpgradeStepError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/cache/collection-membership-cache.ts
|
|
210
|
+
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
211
|
+
cache = /* @__PURE__ */ new Map();
|
|
212
|
+
constructor(operationIndex) {
|
|
213
|
+
this.operationIndex = operationIndex;
|
|
214
|
+
}
|
|
215
|
+
withScopedIndex(operationIndex) {
|
|
216
|
+
const scoped = new CollectionMembershipCache(operationIndex);
|
|
217
|
+
scoped.cache = this.cache;
|
|
218
|
+
return scoped;
|
|
219
|
+
}
|
|
220
|
+
async getCollectionsForDocuments(documentIds) {
|
|
221
|
+
const result = {};
|
|
222
|
+
const missing = [];
|
|
223
|
+
for (const docId of documentIds) {
|
|
224
|
+
const cached = this.cache.get(docId);
|
|
225
|
+
if (cached !== void 0) result[docId] = cached;
|
|
226
|
+
else missing.push(docId);
|
|
227
|
+
}
|
|
228
|
+
if (missing.length > 0) {
|
|
229
|
+
const fromDb = await this.operationIndex.getCollectionsForDocuments(missing);
|
|
230
|
+
for (const docId of missing) {
|
|
231
|
+
const collections = fromDb[docId] ?? [];
|
|
232
|
+
result[docId] = collections;
|
|
233
|
+
this.cache.set(docId, collections);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
invalidate(documentId) {
|
|
239
|
+
this.cache.delete(documentId);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/executor/util.ts
|
|
244
|
+
/**
|
|
245
|
+
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
246
|
+
* Reconstructs the document header and initializes the base state.
|
|
247
|
+
*
|
|
248
|
+
* @param action - The CREATE_DOCUMENT action containing the document parameters
|
|
249
|
+
* @returns A newly constructed PHDocument with initialized header and base state
|
|
250
|
+
*/
|
|
251
|
+
function createDocumentFromAction(action) {
|
|
252
|
+
const input = action.input;
|
|
253
|
+
const header = createPresignedHeader();
|
|
254
|
+
header.id = input.documentId;
|
|
255
|
+
header.documentType = input.model;
|
|
256
|
+
if (input.signing) {
|
|
257
|
+
header.createdAtUtcIso = input.signing.createdAtUtcIso;
|
|
258
|
+
header.lastModifiedAtUtcIso = input.signing.createdAtUtcIso;
|
|
259
|
+
header.sig = {
|
|
260
|
+
publicKey: input.signing.publicKey,
|
|
261
|
+
nonce: input.signing.nonce
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (input.slug !== void 0) header.slug = input.slug;
|
|
265
|
+
if (!header.slug) header.slug = input.documentId;
|
|
266
|
+
if (input.name !== void 0) header.name = input.name;
|
|
267
|
+
if (input.branch !== void 0) header.branch = input.branch;
|
|
268
|
+
if (input.meta !== void 0) header.meta = input.meta;
|
|
269
|
+
if (input.protocolVersions !== void 0) header.protocolVersions = input.protocolVersions;
|
|
270
|
+
const baseState = defaultBaseState();
|
|
271
|
+
return {
|
|
272
|
+
header,
|
|
273
|
+
operations: {},
|
|
274
|
+
state: baseState,
|
|
275
|
+
initialState: baseState,
|
|
276
|
+
clipboard: []
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Applies an UPGRADE_DOCUMENT action to a document.
|
|
281
|
+
* Handles all upgrade scenarios including initial upgrades, no-ops, and multi-step upgrades.
|
|
282
|
+
*
|
|
283
|
+
* Behavior based on fromVersion/toVersion:
|
|
284
|
+
* - fromVersion === toVersion (and fromVersion > 0): No-op - return unchanged document
|
|
285
|
+
* - fromVersion > toVersion: Throw DowngradeNotSupportedError
|
|
286
|
+
* - All other cases: Apply upgradePath transitions (if provided), then apply initialState, set version
|
|
287
|
+
*
|
|
288
|
+
* The initialState from the action is always applied (if provided) to maintain backward
|
|
289
|
+
* compatibility with the original implementation.
|
|
290
|
+
*
|
|
291
|
+
* @param document - The document to upgrade
|
|
292
|
+
* @param action - The UPGRADE_DOCUMENT action
|
|
293
|
+
* @param upgradePath - Optional pre-computed upgrade path for multi-step upgrades
|
|
294
|
+
* @returns The upgraded document (unchanged if no-op)
|
|
295
|
+
* @throws DowngradeNotSupportedError if attempting to downgrade
|
|
296
|
+
*/
|
|
297
|
+
function applyUpgradeDocumentAction(document, action, upgradePath) {
|
|
298
|
+
const fromVersion = action.input.fromVersion;
|
|
299
|
+
const toVersion = action.input.toVersion;
|
|
300
|
+
if (fromVersion === toVersion && fromVersion > 0) return document;
|
|
301
|
+
if (fromVersion > toVersion) throw new DowngradeNotSupportedError$1(document.header.documentType, fromVersion, toVersion);
|
|
302
|
+
if (upgradePath) for (const transition of upgradePath) document = transition.upgradeReducer(document, action);
|
|
303
|
+
applyInitialState(document, action);
|
|
304
|
+
document.state.document = {
|
|
305
|
+
...document.state.document,
|
|
306
|
+
version: toVersion
|
|
307
|
+
};
|
|
308
|
+
return document;
|
|
309
|
+
}
|
|
310
|
+
function applyInitialState(document, action) {
|
|
311
|
+
const input = action.input;
|
|
312
|
+
const newState = input.initialState || input.state;
|
|
313
|
+
if (newState) {
|
|
314
|
+
document.state = {
|
|
315
|
+
...document.state,
|
|
316
|
+
...newState
|
|
317
|
+
};
|
|
318
|
+
document.initialState = document.state;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Applies a DELETE_DOCUMENT action to a document.
|
|
323
|
+
* Marks the document as deleted in the document scope state.
|
|
324
|
+
*
|
|
325
|
+
* @param document - The document to mark as deleted
|
|
326
|
+
* @param action - The DELETE_DOCUMENT action
|
|
327
|
+
* @returns The updated document (mutates in place and returns for convenience)
|
|
328
|
+
*/
|
|
329
|
+
function applyDeleteDocumentAction(document, action) {
|
|
330
|
+
const deletedAt = action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString();
|
|
331
|
+
document.state = {
|
|
332
|
+
...document.state,
|
|
333
|
+
document: {
|
|
334
|
+
...document.state.document,
|
|
335
|
+
isDeleted: true,
|
|
336
|
+
deletedAtUtcIso: deletedAt
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
return document;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Calculate the next operation index for a specific scope.
|
|
343
|
+
* Each scope maintains its own independent index sequence.
|
|
344
|
+
*
|
|
345
|
+
* Per-scope indexing means:
|
|
346
|
+
* - Each scope (document, global, local, etc.) has independent indexes
|
|
347
|
+
* - Indexes start at 0 for each scope
|
|
348
|
+
* - Different scopes can have operations with the same index value
|
|
349
|
+
*
|
|
350
|
+
* This function uses header.revision which is populated by the cache/storage layer
|
|
351
|
+
* and contains the next available index for each scope. This design avoids requiring
|
|
352
|
+
* the full operation history to be loaded, which is crucial for snapshot-based caching.
|
|
353
|
+
*
|
|
354
|
+
* @param document - The document whose header.revision to inspect
|
|
355
|
+
* @param scope - The scope to calculate the next index for
|
|
356
|
+
* @returns The next available index in the specified scope
|
|
357
|
+
*/
|
|
358
|
+
const getNextIndexForScope = (document, scope) => {
|
|
359
|
+
return document.header.revision[scope] || 0;
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Creates an empty consistency token with no coordinates.
|
|
363
|
+
* Used when a job is registered or fails without writing operations.
|
|
364
|
+
*
|
|
365
|
+
* @returns A consistency token with an empty coordinates array
|
|
366
|
+
*/
|
|
367
|
+
function createEmptyConsistencyToken() {
|
|
368
|
+
return {
|
|
369
|
+
version: 1,
|
|
370
|
+
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
371
|
+
coordinates: []
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Creates a consistency token from operations written during job execution.
|
|
376
|
+
* Maps each operation to a consistency coordinate tracking (documentId, scope, branch, operationIndex).
|
|
377
|
+
* If no operations are provided, returns an empty token.
|
|
378
|
+
*
|
|
379
|
+
* @param operationsWithContext - Array of operations with their execution context
|
|
380
|
+
* @returns A consistency token representing all operations written
|
|
381
|
+
*/
|
|
382
|
+
function createConsistencyToken(operationsWithContext) {
|
|
383
|
+
if (operationsWithContext.length === 0) return createEmptyConsistencyToken();
|
|
384
|
+
const coordinates = [];
|
|
385
|
+
for (let i = 0; i < operationsWithContext.length; i++) {
|
|
386
|
+
const opWithContext = operationsWithContext[i];
|
|
387
|
+
coordinates.push({
|
|
388
|
+
documentId: opWithContext.context.documentId,
|
|
389
|
+
scope: opWithContext.context.scope,
|
|
390
|
+
branch: opWithContext.context.branch,
|
|
391
|
+
operationIndex: opWithContext.operation.index
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
version: 1,
|
|
396
|
+
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
397
|
+
coordinates
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function createOperation(action, index, skip, context) {
|
|
401
|
+
return {
|
|
402
|
+
id: deriveOperationId(context.documentId, context.scope, context.branch, action.id),
|
|
403
|
+
index,
|
|
404
|
+
timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
|
|
405
|
+
hash: "",
|
|
406
|
+
skip,
|
|
407
|
+
action
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function updateDocumentRevision(document, scope, operationIndex) {
|
|
411
|
+
document.header.revision = {
|
|
412
|
+
...document.header.revision,
|
|
413
|
+
[scope]: operationIndex + 1
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
|
|
417
|
+
return {
|
|
418
|
+
job,
|
|
419
|
+
success: true,
|
|
420
|
+
operations: [operation],
|
|
421
|
+
operationsWithContext: [{
|
|
422
|
+
operation,
|
|
423
|
+
context: {
|
|
424
|
+
documentId,
|
|
425
|
+
scope: job.scope,
|
|
426
|
+
branch: job.branch,
|
|
427
|
+
documentType,
|
|
428
|
+
resultingState,
|
|
429
|
+
ordinal: 0
|
|
430
|
+
}
|
|
431
|
+
}],
|
|
432
|
+
duration: Date.now() - startTime
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function buildErrorResult(job, error, startTime) {
|
|
436
|
+
return {
|
|
437
|
+
job,
|
|
438
|
+
success: false,
|
|
439
|
+
error,
|
|
440
|
+
duration: Date.now() - startTime
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/cache/lru/lru-tracker.ts
|
|
445
|
+
var LRUNode = class {
|
|
446
|
+
key;
|
|
447
|
+
prev;
|
|
448
|
+
next;
|
|
449
|
+
constructor(key) {
|
|
450
|
+
this.key = key;
|
|
451
|
+
this.prev = void 0;
|
|
452
|
+
this.next = void 0;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var LRUTracker = class {
|
|
456
|
+
map;
|
|
457
|
+
head;
|
|
458
|
+
tail;
|
|
459
|
+
constructor() {
|
|
460
|
+
this.map = /* @__PURE__ */ new Map();
|
|
461
|
+
this.head = void 0;
|
|
462
|
+
this.tail = void 0;
|
|
463
|
+
}
|
|
464
|
+
get size() {
|
|
465
|
+
return this.map.size;
|
|
466
|
+
}
|
|
467
|
+
touch(key) {
|
|
468
|
+
const node = this.map.get(key);
|
|
469
|
+
if (node) this.moveToFront(node);
|
|
470
|
+
else this.addToFront(key);
|
|
471
|
+
}
|
|
472
|
+
evict() {
|
|
473
|
+
if (!this.tail) return;
|
|
474
|
+
const key = this.tail.key;
|
|
475
|
+
this.remove(key);
|
|
476
|
+
return key;
|
|
477
|
+
}
|
|
478
|
+
remove(key) {
|
|
479
|
+
const node = this.map.get(key);
|
|
480
|
+
if (!node) return;
|
|
481
|
+
this.removeNode(node);
|
|
482
|
+
this.map.delete(key);
|
|
483
|
+
}
|
|
484
|
+
clear() {
|
|
485
|
+
this.map.clear();
|
|
486
|
+
this.head = void 0;
|
|
487
|
+
this.tail = void 0;
|
|
488
|
+
}
|
|
489
|
+
addToFront(key) {
|
|
490
|
+
const node = new LRUNode(key);
|
|
491
|
+
this.map.set(key, node);
|
|
492
|
+
if (!this.head) {
|
|
493
|
+
this.head = node;
|
|
494
|
+
this.tail = node;
|
|
495
|
+
} else {
|
|
496
|
+
node.next = this.head;
|
|
497
|
+
this.head.prev = node;
|
|
498
|
+
this.head = node;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
moveToFront(node) {
|
|
502
|
+
if (node === this.head) return;
|
|
503
|
+
this.removeNode(node);
|
|
504
|
+
node.prev = void 0;
|
|
505
|
+
node.next = this.head;
|
|
506
|
+
if (this.head) this.head.prev = node;
|
|
507
|
+
this.head = node;
|
|
508
|
+
if (!this.tail) this.tail = node;
|
|
509
|
+
}
|
|
510
|
+
removeNode(node) {
|
|
511
|
+
if (node.prev) node.prev.next = node.next;
|
|
512
|
+
else this.head = node.next;
|
|
513
|
+
if (node.next) node.next.prev = node.prev;
|
|
514
|
+
else this.tail = node.prev;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/cache/document-meta-cache.ts
|
|
519
|
+
/**
|
|
520
|
+
* In-memory document metadata cache with LRU eviction.
|
|
521
|
+
*
|
|
522
|
+
* Caches PHDocumentState per (documentId, branch) key. On cache miss,
|
|
523
|
+
* rebuilds from document scope operations. Provides an explicit cross-scope
|
|
524
|
+
* contract for accessing document scope metadata.
|
|
525
|
+
*
|
|
526
|
+
* **Thread Safety:**
|
|
527
|
+
* Not thread-safe. Designed for single-threaded job executor environment.
|
|
528
|
+
*/
|
|
529
|
+
var DocumentMetaCache = class DocumentMetaCache {
|
|
530
|
+
cache;
|
|
531
|
+
lruTracker;
|
|
532
|
+
operationStore;
|
|
533
|
+
config;
|
|
534
|
+
constructor(operationStore, config) {
|
|
535
|
+
this.operationStore = operationStore;
|
|
536
|
+
this.config = { maxDocuments: config.maxDocuments };
|
|
537
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
538
|
+
this.lruTracker = new LRUTracker();
|
|
539
|
+
}
|
|
540
|
+
withScopedStore(operationStore) {
|
|
541
|
+
const scoped = new DocumentMetaCache(operationStore, this.config);
|
|
542
|
+
scoped.cache = this.cache;
|
|
543
|
+
scoped.lruTracker = this.lruTracker;
|
|
544
|
+
return scoped;
|
|
545
|
+
}
|
|
546
|
+
async startup() {
|
|
547
|
+
return Promise.resolve();
|
|
548
|
+
}
|
|
549
|
+
async shutdown() {
|
|
550
|
+
return Promise.resolve();
|
|
551
|
+
}
|
|
552
|
+
async getDocumentMeta(documentId, branch, signal) {
|
|
553
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
554
|
+
const key = this.makeKey(documentId, branch);
|
|
555
|
+
const cached = this.cache.get(key);
|
|
556
|
+
if (cached) {
|
|
557
|
+
this.lruTracker.touch(key);
|
|
558
|
+
return cached;
|
|
559
|
+
}
|
|
560
|
+
const meta = await this.rebuildLatest(documentId, branch, signal);
|
|
561
|
+
this.putDocumentMeta(documentId, branch, meta);
|
|
562
|
+
return meta;
|
|
563
|
+
}
|
|
564
|
+
async rebuildAtRevision(documentId, branch, targetRevision, signal) {
|
|
565
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
566
|
+
return this.rebuildFromOperations(documentId, branch, targetRevision, signal);
|
|
567
|
+
}
|
|
568
|
+
putDocumentMeta(documentId, branch, meta) {
|
|
569
|
+
const key = this.makeKey(documentId, branch);
|
|
570
|
+
if (!this.cache.has(key) && this.cache.size >= this.config.maxDocuments) {
|
|
571
|
+
const evictKey = this.lruTracker.evict();
|
|
572
|
+
if (evictKey) this.cache.delete(evictKey);
|
|
573
|
+
}
|
|
574
|
+
this.cache.set(key, structuredClone(meta));
|
|
575
|
+
this.lruTracker.touch(key);
|
|
576
|
+
}
|
|
577
|
+
invalidate(documentId, branch) {
|
|
578
|
+
let evicted = 0;
|
|
579
|
+
if (branch === void 0) {
|
|
580
|
+
for (const key of this.cache.keys()) if (key.startsWith(`${documentId}:`)) {
|
|
581
|
+
this.cache.delete(key);
|
|
582
|
+
this.lruTracker.remove(key);
|
|
583
|
+
evicted++;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
const key = this.makeKey(documentId, branch);
|
|
587
|
+
if (this.cache.has(key)) {
|
|
588
|
+
this.cache.delete(key);
|
|
589
|
+
this.lruTracker.remove(key);
|
|
590
|
+
evicted = 1;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return evicted;
|
|
594
|
+
}
|
|
595
|
+
clear() {
|
|
596
|
+
this.cache.clear();
|
|
597
|
+
this.lruTracker.clear();
|
|
598
|
+
}
|
|
599
|
+
makeKey(documentId, branch) {
|
|
600
|
+
return `${documentId}:${branch}`;
|
|
601
|
+
}
|
|
602
|
+
async rebuildLatest(documentId, branch, signal) {
|
|
603
|
+
return this.rebuildFromOperations(documentId, branch, void 0, signal);
|
|
604
|
+
}
|
|
605
|
+
async rebuildFromOperations(documentId, branch, targetRevision, signal) {
|
|
606
|
+
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
|
|
607
|
+
if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
608
|
+
const createOp = docScopeOps.results[0];
|
|
609
|
+
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
610
|
+
const createAction = createOp.action;
|
|
611
|
+
const documentType = createAction.input.model;
|
|
612
|
+
let document = createDocumentFromAction(createAction);
|
|
613
|
+
let documentScopeRevision = 0;
|
|
614
|
+
for (const op of docScopeOps.results) {
|
|
615
|
+
if (targetRevision !== void 0 && op.index > targetRevision) break;
|
|
616
|
+
documentScopeRevision = op.index;
|
|
617
|
+
if (op.action.type === "UPGRADE_DOCUMENT") {
|
|
618
|
+
const upgradeAction = op.action;
|
|
619
|
+
document = applyUpgradeDocumentAction(document, upgradeAction);
|
|
620
|
+
} else if (op.action.type === "DELETE_DOCUMENT") document = applyDeleteDocumentAction(document, op.action);
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
state: document.state.document,
|
|
624
|
+
documentType,
|
|
625
|
+
documentScopeRevision: documentScopeRevision + 1
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/cache/kysely-operation-index.ts
|
|
631
|
+
var KyselyOperationIndexTxn = class {
|
|
632
|
+
collections = [];
|
|
633
|
+
collectionMemberships = [];
|
|
634
|
+
collectionRemovals = [];
|
|
635
|
+
operations = [];
|
|
636
|
+
createCollection(collectionId) {
|
|
637
|
+
this.collections.push(collectionId);
|
|
638
|
+
}
|
|
639
|
+
addToCollection(collectionId, documentId) {
|
|
640
|
+
const lastOpIndex = this.operations.length - 1;
|
|
641
|
+
if (lastOpIndex < 0) throw new Error("addToCollection must be called after write() - no operations in transaction");
|
|
642
|
+
this.collectionMemberships.push({
|
|
643
|
+
collectionId,
|
|
644
|
+
documentId,
|
|
645
|
+
operationIndex: lastOpIndex
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
removeFromCollection(collectionId, documentId) {
|
|
649
|
+
const lastOpIndex = this.operations.length - 1;
|
|
650
|
+
if (lastOpIndex < 0) throw new Error("removeFromCollection must be called after write() - no operations in transaction");
|
|
651
|
+
this.collectionRemovals.push({
|
|
652
|
+
collectionId,
|
|
653
|
+
documentId,
|
|
654
|
+
operationIndex: lastOpIndex
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
write(operations) {
|
|
658
|
+
this.operations.push(...operations);
|
|
659
|
+
}
|
|
660
|
+
getCollections() {
|
|
661
|
+
return this.collections;
|
|
662
|
+
}
|
|
663
|
+
getCollectionMembershipRecords() {
|
|
664
|
+
return this.collectionMemberships;
|
|
665
|
+
}
|
|
666
|
+
getCollectionRemovals() {
|
|
667
|
+
return this.collectionRemovals;
|
|
668
|
+
}
|
|
669
|
+
getOperations() {
|
|
670
|
+
return this.operations;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
674
|
+
trx;
|
|
675
|
+
constructor(db) {
|
|
676
|
+
this.db = db;
|
|
677
|
+
}
|
|
678
|
+
get queryExecutor() {
|
|
679
|
+
return this.trx ?? this.db;
|
|
680
|
+
}
|
|
681
|
+
withTransaction(trx) {
|
|
682
|
+
const instance = new KyselyOperationIndex(this.db);
|
|
683
|
+
instance.trx = trx;
|
|
684
|
+
return instance;
|
|
685
|
+
}
|
|
686
|
+
start() {
|
|
687
|
+
return new KyselyOperationIndexTxn();
|
|
688
|
+
}
|
|
689
|
+
async commit(txn, signal) {
|
|
690
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
691
|
+
const kyselyTxn = txn;
|
|
692
|
+
if (this.trx) return this.executeCommit(this.trx, kyselyTxn);
|
|
693
|
+
let resultOrdinals = [];
|
|
694
|
+
await this.db.transaction().execute(async (trx) => {
|
|
695
|
+
resultOrdinals = await this.executeCommit(trx, kyselyTxn);
|
|
696
|
+
});
|
|
697
|
+
return resultOrdinals;
|
|
698
|
+
}
|
|
699
|
+
async executeCommit(trx, kyselyTxn) {
|
|
700
|
+
const collections = kyselyTxn.getCollections();
|
|
701
|
+
const memberships = kyselyTxn.getCollectionMembershipRecords();
|
|
702
|
+
const removals = kyselyTxn.getCollectionRemovals();
|
|
703
|
+
const operations = kyselyTxn.getOperations();
|
|
704
|
+
if (collections.length > 0) {
|
|
705
|
+
const collectionRows = collections.map((collectionId) => ({
|
|
706
|
+
documentId: collectionId,
|
|
707
|
+
collectionId,
|
|
708
|
+
joinedOrdinal: BigInt(0),
|
|
709
|
+
leftOrdinal: null
|
|
710
|
+
}));
|
|
711
|
+
await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
|
|
712
|
+
}
|
|
713
|
+
let operationOrdinals = [];
|
|
714
|
+
if (operations.length > 0) {
|
|
715
|
+
const operationRows = operations.map((op) => ({
|
|
716
|
+
opId: op.id || "",
|
|
717
|
+
documentId: op.documentId,
|
|
718
|
+
documentType: op.documentType,
|
|
719
|
+
scope: op.scope,
|
|
720
|
+
branch: op.branch,
|
|
721
|
+
timestampUtcMs: op.timestampUtcMs,
|
|
722
|
+
index: op.index,
|
|
723
|
+
skip: op.skip,
|
|
724
|
+
hash: op.hash,
|
|
725
|
+
action: op.action,
|
|
726
|
+
sourceRemote: op.sourceRemote
|
|
727
|
+
}));
|
|
728
|
+
operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
|
|
729
|
+
}
|
|
730
|
+
if (memberships.length > 0) for (const m of memberships) {
|
|
731
|
+
const ordinal = operationOrdinals[m.operationIndex];
|
|
732
|
+
await trx.insertInto("document_collections").values({
|
|
733
|
+
documentId: m.documentId,
|
|
734
|
+
collectionId: m.collectionId,
|
|
735
|
+
joinedOrdinal: BigInt(ordinal),
|
|
736
|
+
leftOrdinal: null
|
|
737
|
+
}).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
|
|
738
|
+
joinedOrdinal: BigInt(ordinal),
|
|
739
|
+
leftOrdinal: null
|
|
740
|
+
})).execute();
|
|
741
|
+
}
|
|
742
|
+
if (removals.length > 0) for (const r of removals) {
|
|
743
|
+
const ordinal = operationOrdinals[r.operationIndex];
|
|
744
|
+
await trx.updateTable("document_collections").set({ leftOrdinal: BigInt(ordinal) }).where("collectionId", "=", r.collectionId).where("documentId", "=", r.documentId).where("leftOrdinal", "is", null).execute();
|
|
745
|
+
}
|
|
746
|
+
return operationOrdinals;
|
|
747
|
+
}
|
|
748
|
+
async find(collectionId, cursor, view, paging, signal) {
|
|
749
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
750
|
+
const outerCursor = cursor ?? -1;
|
|
751
|
+
const pagingCursorOrdinal = paging?.cursor !== void 0 ? Number.parseInt(paging.cursor, 10) : -1;
|
|
752
|
+
const buildBranch = (kind) => {
|
|
753
|
+
let qb = this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").selectAll("oi").select(["dc.documentId", "dc.collectionId"]).where("dc.collectionId", "=", collectionId).where(sql`(dc."leftOrdinal" IS NULL OR oi.ordinal < dc."leftOrdinal")`);
|
|
754
|
+
if (kind === "joiner") qb = qb.where("dc.joinedOrdinal", ">", BigInt(outerCursor)).where("oi.ordinal", "<=", outerCursor);
|
|
755
|
+
else qb = qb.where("oi.ordinal", ">", outerCursor);
|
|
756
|
+
qb = qb.where("oi.ordinal", ">", pagingCursorOrdinal);
|
|
757
|
+
if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
|
|
758
|
+
if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
|
|
759
|
+
if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
|
|
760
|
+
return qb;
|
|
761
|
+
};
|
|
762
|
+
let unionQuery = buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc");
|
|
763
|
+
if (paging?.limit) unionQuery = unionQuery.limit(paging.limit + 1);
|
|
764
|
+
const rows = await unionQuery.execute();
|
|
765
|
+
let hasMore = false;
|
|
766
|
+
let items = rows;
|
|
767
|
+
if (paging?.limit && rows.length > paging.limit) {
|
|
768
|
+
hasMore = true;
|
|
769
|
+
items = rows.slice(0, paging.limit);
|
|
770
|
+
}
|
|
771
|
+
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
|
|
772
|
+
const cursorValue = paging?.cursor || "0";
|
|
773
|
+
const limit = paging?.limit || 100;
|
|
774
|
+
return {
|
|
775
|
+
results: items.map((row) => this.rowToOperationIndexEntry(row)),
|
|
776
|
+
options: {
|
|
777
|
+
cursor: cursorValue,
|
|
778
|
+
limit
|
|
779
|
+
},
|
|
780
|
+
nextCursor,
|
|
781
|
+
next: hasMore ? () => this.find(collectionId, cursor, view, {
|
|
782
|
+
cursor: nextCursor,
|
|
783
|
+
limit
|
|
784
|
+
}, signal) : void 0
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
async get(documentId, view, paging, signal) {
|
|
788
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
789
|
+
let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", documentId).orderBy("ordinal", "asc");
|
|
790
|
+
if (view?.branch) query = query.where("branch", "=", view.branch);
|
|
791
|
+
if (view?.scopes && view.scopes.length > 0) query = query.where("scope", "in", view.scopes);
|
|
792
|
+
if (paging?.cursor) {
|
|
793
|
+
const cursorOrdinal = Number.parseInt(paging.cursor, 10);
|
|
794
|
+
query = query.where("ordinal", ">", cursorOrdinal);
|
|
795
|
+
}
|
|
796
|
+
if (paging?.limit) query = query.limit(paging.limit + 1);
|
|
797
|
+
const rows = await query.execute();
|
|
798
|
+
let hasMore = false;
|
|
799
|
+
let items = rows;
|
|
800
|
+
if (paging?.limit && rows.length > paging.limit) {
|
|
801
|
+
hasMore = true;
|
|
802
|
+
items = rows.slice(0, paging.limit);
|
|
803
|
+
}
|
|
804
|
+
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
|
|
805
|
+
const cursorValue = paging?.cursor || "0";
|
|
806
|
+
const limit = paging?.limit || 100;
|
|
807
|
+
return {
|
|
808
|
+
results: items.map((row) => this.rowToOperationIndexEntry(row)),
|
|
809
|
+
options: {
|
|
810
|
+
cursor: cursorValue,
|
|
811
|
+
limit
|
|
812
|
+
},
|
|
813
|
+
nextCursor,
|
|
814
|
+
next: hasMore ? () => this.get(documentId, view, {
|
|
815
|
+
cursor: nextCursor,
|
|
816
|
+
limit
|
|
817
|
+
}, signal) : void 0
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async getSinceOrdinal(ordinal, paging, signal) {
|
|
821
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
822
|
+
let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", ">", ordinal).orderBy("ordinal", "asc");
|
|
823
|
+
if (paging?.cursor) {
|
|
824
|
+
const cursorOrdinal = Number.parseInt(paging.cursor, 10);
|
|
825
|
+
query = query.where("ordinal", ">", cursorOrdinal);
|
|
826
|
+
}
|
|
827
|
+
if (paging?.limit) query = query.limit(paging.limit + 1);
|
|
828
|
+
const rows = await query.execute();
|
|
829
|
+
let hasMore = false;
|
|
830
|
+
let items = rows;
|
|
831
|
+
if (paging?.limit && rows.length > paging.limit) {
|
|
832
|
+
hasMore = true;
|
|
833
|
+
items = rows.slice(0, paging.limit);
|
|
834
|
+
}
|
|
835
|
+
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
|
|
836
|
+
const cursorValue = paging?.cursor || "0";
|
|
837
|
+
const limit = paging?.limit || 100;
|
|
838
|
+
return {
|
|
839
|
+
results: items.map((row) => this.rowToOperationWithContext(row)),
|
|
840
|
+
options: {
|
|
841
|
+
cursor: cursorValue,
|
|
842
|
+
limit
|
|
843
|
+
},
|
|
844
|
+
nextCursor,
|
|
845
|
+
next: hasMore ? () => this.getSinceOrdinal(ordinal, {
|
|
846
|
+
cursor: nextCursor,
|
|
847
|
+
limit
|
|
848
|
+
}, signal) : void 0
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
rowToOperationWithContext(row) {
|
|
852
|
+
return {
|
|
853
|
+
operation: {
|
|
854
|
+
index: row.index,
|
|
855
|
+
timestampUtcMs: row.timestampUtcMs,
|
|
856
|
+
hash: row.hash,
|
|
857
|
+
skip: row.skip,
|
|
858
|
+
action: row.action,
|
|
859
|
+
id: row.opId
|
|
860
|
+
},
|
|
861
|
+
context: {
|
|
862
|
+
documentId: row.documentId,
|
|
863
|
+
documentType: row.documentType,
|
|
864
|
+
scope: row.scope,
|
|
865
|
+
branch: row.branch,
|
|
866
|
+
ordinal: row.ordinal
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
rowToOperationIndexEntry(row) {
|
|
871
|
+
return {
|
|
872
|
+
ordinal: row.ordinal,
|
|
873
|
+
documentId: row.documentId,
|
|
874
|
+
documentType: row.documentType,
|
|
875
|
+
branch: row.branch,
|
|
876
|
+
scope: row.scope,
|
|
877
|
+
index: row.index,
|
|
878
|
+
timestampUtcMs: row.timestampUtcMs,
|
|
879
|
+
hash: row.hash,
|
|
880
|
+
skip: row.skip,
|
|
881
|
+
action: row.action,
|
|
882
|
+
id: row.opId,
|
|
883
|
+
sourceRemote: row.sourceRemote
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async getLatestTimestampForCollection(collectionId, signal) {
|
|
887
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
888
|
+
return (await this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").select("oi.timestampUtcMs").where("dc.collectionId", "=", collectionId).where(sql`(dc."leftOrdinal" IS NULL OR oi.ordinal < dc."leftOrdinal")`).orderBy("oi.ordinal", "desc").limit(1).executeTakeFirst())?.timestampUtcMs ?? null;
|
|
889
|
+
}
|
|
890
|
+
async getCollectionsForDocuments(documentIds) {
|
|
891
|
+
if (documentIds.length === 0) return {};
|
|
892
|
+
const rows = await this.queryExecutor.selectFrom("document_collections").select(["documentId", "collectionId"]).where("documentId", "in", documentIds).where("leftOrdinal", "is", null).execute();
|
|
893
|
+
const result = {};
|
|
894
|
+
for (const row of rows) {
|
|
895
|
+
if (!(row.documentId in result)) result[row.documentId] = [];
|
|
896
|
+
result[row.documentId].push(row.collectionId);
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/cache/buffer/ring-buffer.ts
|
|
903
|
+
/**
|
|
904
|
+
* RingBuffer is a generic circular buffer implementation that stores a fixed number
|
|
905
|
+
* of items. When the buffer is full, new items overwrite the oldest items.
|
|
906
|
+
*
|
|
907
|
+
* This implementation maintains O(1) time complexity for push operations and provides
|
|
908
|
+
* items in chronological order (oldest to newest) via getAll().
|
|
909
|
+
*
|
|
910
|
+
* @template T - The type of items stored in the buffer
|
|
911
|
+
*/
|
|
912
|
+
var RingBuffer = class {
|
|
913
|
+
buffer;
|
|
914
|
+
head = 0;
|
|
915
|
+
size = 0;
|
|
916
|
+
capacity;
|
|
917
|
+
constructor(capacity) {
|
|
918
|
+
if (capacity <= 0) throw new Error("Ring buffer capacity must be greater than 0");
|
|
919
|
+
this.capacity = capacity;
|
|
920
|
+
this.buffer = new Array(capacity);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Adds an item to the buffer. If the buffer is full, overwrites the oldest item.
|
|
924
|
+
*
|
|
925
|
+
* @param item - The item to add
|
|
926
|
+
*/
|
|
927
|
+
push(item) {
|
|
928
|
+
const index = (this.head + this.size) % this.capacity;
|
|
929
|
+
if (this.size < this.capacity) {
|
|
930
|
+
this.buffer[index] = item;
|
|
931
|
+
this.size++;
|
|
932
|
+
} else {
|
|
933
|
+
this.buffer[this.head] = item;
|
|
934
|
+
this.head = (this.head + 1) % this.capacity;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Returns all items in the buffer in chronological order (oldest to newest).
|
|
939
|
+
*
|
|
940
|
+
* @returns Array of items in insertion order
|
|
941
|
+
*/
|
|
942
|
+
getAll() {
|
|
943
|
+
if (this.size === 0) return [];
|
|
944
|
+
const result = [];
|
|
945
|
+
for (let i = 0; i < this.size; i++) {
|
|
946
|
+
const index = (this.head + i) % this.capacity;
|
|
947
|
+
result.push(this.buffer[index]);
|
|
948
|
+
}
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Clears all items from the buffer.
|
|
953
|
+
*/
|
|
954
|
+
clear() {
|
|
955
|
+
this.buffer = new Array(this.capacity);
|
|
956
|
+
this.head = 0;
|
|
957
|
+
this.size = 0;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Gets the current number of items in the buffer.
|
|
961
|
+
*/
|
|
962
|
+
get length() {
|
|
963
|
+
return this.size;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/cache/kysely-write-cache.ts
|
|
968
|
+
function extractModuleVersion(doc) {
|
|
969
|
+
const v = doc.state.document.version;
|
|
970
|
+
return v === 0 ? void 0 : v;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* In-memory write cache with keyframe persistence for PHDocuments.
|
|
974
|
+
*
|
|
975
|
+
* Caches document snapshots in ring buffers with LRU eviction. On cache miss,
|
|
976
|
+
* rebuilds documents from nearest keyframe or full operation history.
|
|
977
|
+
*
|
|
978
|
+
* **Performance Characteristics:**
|
|
979
|
+
* - Cache hit: O(1) lookup in ring buffer
|
|
980
|
+
* - Cold miss: O(n) where n is total operation count, or O(k) where k is operations since keyframe
|
|
981
|
+
* - Warm miss: O(m) where m is operations since cached revision
|
|
982
|
+
* - Eviction: O(1) for LRU tracking and removal
|
|
983
|
+
*
|
|
984
|
+
* **Thread Safety:**
|
|
985
|
+
* Not thread-safe. Designed for single-threaded job executor environment.
|
|
986
|
+
* External synchronization required for concurrent access across multiple executors.
|
|
987
|
+
*
|
|
988
|
+
* **Example:**
|
|
989
|
+
* ```typescript
|
|
990
|
+
* const cache = new KyselyWriteCache(
|
|
991
|
+
* keyframeStore,
|
|
992
|
+
* operationStore,
|
|
993
|
+
* registry,
|
|
994
|
+
* { maxDocuments: 1000, ringBufferSize: 10, keyframeInterval: 10 }
|
|
995
|
+
* );
|
|
996
|
+
*
|
|
997
|
+
* await cache.startup();
|
|
998
|
+
*
|
|
999
|
+
* // Retrieve or rebuild document
|
|
1000
|
+
* const doc = await cache.getState(docId, docType, scope, branch, revision);
|
|
1001
|
+
*
|
|
1002
|
+
* // Cache result after job execution
|
|
1003
|
+
* cache.putState(docId, docType, scope, branch, newRevision, updatedDoc);
|
|
1004
|
+
*
|
|
1005
|
+
* await cache.shutdown();
|
|
1006
|
+
* ```
|
|
1007
|
+
*/
|
|
1008
|
+
var KyselyWriteCache = class KyselyWriteCache {
|
|
1009
|
+
streams;
|
|
1010
|
+
lruTracker;
|
|
1011
|
+
keyframeStore;
|
|
1012
|
+
operationStore;
|
|
1013
|
+
registry;
|
|
1014
|
+
config;
|
|
1015
|
+
constructor(keyframeStore, operationStore, registry, config) {
|
|
1016
|
+
this.keyframeStore = keyframeStore;
|
|
1017
|
+
this.operationStore = operationStore;
|
|
1018
|
+
this.registry = registry;
|
|
1019
|
+
this.config = {
|
|
1020
|
+
maxDocuments: config.maxDocuments,
|
|
1021
|
+
ringBufferSize: config.ringBufferSize,
|
|
1022
|
+
keyframeInterval: config.keyframeInterval
|
|
1023
|
+
};
|
|
1024
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
1025
|
+
this.lruTracker = new LRUTracker();
|
|
1026
|
+
}
|
|
1027
|
+
withScopedStores(operationStore, keyframeStore) {
|
|
1028
|
+
const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
|
|
1029
|
+
scoped.streams = this.streams;
|
|
1030
|
+
scoped.lruTracker = this.lruTracker;
|
|
1031
|
+
return scoped;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Initializes the write cache.
|
|
1035
|
+
* Currently a no-op as keyframe store lifecycle is managed externally.
|
|
1036
|
+
*/
|
|
1037
|
+
async startup() {
|
|
1038
|
+
return Promise.resolve();
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Shuts down the write cache.
|
|
1042
|
+
* Currently a no-op as keyframe store lifecycle is managed externally.
|
|
1043
|
+
*/
|
|
1044
|
+
async shutdown() {
|
|
1045
|
+
return Promise.resolve();
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Retrieves document state at a specific revision from cache or rebuilds it.
|
|
1049
|
+
*
|
|
1050
|
+
* Cache hit path: Returns cached snapshot if available (O(1))
|
|
1051
|
+
* Warm miss path: Rebuilds from cached base revision + incremental ops
|
|
1052
|
+
* Cold miss path: Rebuilds from keyframe or from scratch using all operations
|
|
1053
|
+
*
|
|
1054
|
+
* @param documentId - The document identifier
|
|
1055
|
+
* @param scope - The operation scope
|
|
1056
|
+
* @param branch - The operation branch
|
|
1057
|
+
* @param targetRevision - The target revision, or undefined for newest
|
|
1058
|
+
* @param signal - Optional abort signal to cancel the operation
|
|
1059
|
+
* @returns The document at the target revision
|
|
1060
|
+
* @throws {Error} "Operation aborted" if signal is aborted
|
|
1061
|
+
* @throws {ModuleNotFoundError} If document type not registered in registry
|
|
1062
|
+
* @throws {Error} "Failed to rebuild document" if operation store fails
|
|
1063
|
+
* @throws {Error} If reducer throws during operation application
|
|
1064
|
+
* @throws {Error} If document serialization fails
|
|
1065
|
+
*/
|
|
1066
|
+
async getState(documentId, scope, branch, targetRevision, signal) {
|
|
1067
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1068
|
+
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1069
|
+
const stream = this.streams.get(streamKey);
|
|
1070
|
+
if (stream) {
|
|
1071
|
+
const snapshots = stream.ringBuffer.getAll();
|
|
1072
|
+
if (targetRevision === void 0) {
|
|
1073
|
+
if (snapshots.length > 0) {
|
|
1074
|
+
const newest = snapshots[snapshots.length - 1];
|
|
1075
|
+
this.lruTracker.touch(streamKey);
|
|
1076
|
+
return newest.document;
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
const exactMatch = snapshots.find((s) => s.revision === targetRevision);
|
|
1080
|
+
if (exactMatch) {
|
|
1081
|
+
this.lruTracker.touch(streamKey);
|
|
1082
|
+
return exactMatch.document;
|
|
1083
|
+
}
|
|
1084
|
+
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1085
|
+
if (newestOlder) {
|
|
1086
|
+
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1087
|
+
this.putState(documentId, scope, branch, targetRevision, document);
|
|
1088
|
+
this.lruTracker.touch(streamKey);
|
|
1089
|
+
return document;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1094
|
+
let revision = targetRevision;
|
|
1095
|
+
if (revision === void 0) revision = document.header.revision[scope] || 0;
|
|
1096
|
+
this.putState(documentId, scope, branch, revision, document);
|
|
1097
|
+
return document;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Stores a document snapshot in the cache at a specific revision.
|
|
1101
|
+
*
|
|
1102
|
+
* The cached document is a shallow copy of the input with its operation history
|
|
1103
|
+
* truncated to the last operation per scope and its clipboard cleared. This keeps
|
|
1104
|
+
* memory use and copy costs constant regardless of operation count. Consumers of
|
|
1105
|
+
* getState() must not rely on the full operation history being present; the only
|
|
1106
|
+
* guaranteed invariant is that operations[scope].at(-1) reflects the latest
|
|
1107
|
+
* operation index for each scope.
|
|
1108
|
+
*
|
|
1109
|
+
* Updates LRU tracker and may evict least recently used stream if at capacity.
|
|
1110
|
+
* Asynchronously persists keyframes at configured intervals (fire-and-forget).
|
|
1111
|
+
*
|
|
1112
|
+
* @param documentId - The document identifier
|
|
1113
|
+
* @param scope - The operation scope
|
|
1114
|
+
* @param branch - The operation branch
|
|
1115
|
+
* @param revision - The revision number
|
|
1116
|
+
* @param document - The document to cache
|
|
1117
|
+
* @throws {Error} If document serialization fails
|
|
1118
|
+
*/
|
|
1119
|
+
putState(documentId, scope, branch, revision, document) {
|
|
1120
|
+
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1121
|
+
const stream = this.getOrCreateStream(streamKey);
|
|
1122
|
+
const snapshot = {
|
|
1123
|
+
revision,
|
|
1124
|
+
document: {
|
|
1125
|
+
...document,
|
|
1126
|
+
operations: Object.fromEntries(Object.entries(document.operations).map(([k, ops]) => [k, ops.length ? [ops.at(-1)] : []])),
|
|
1127
|
+
clipboard: []
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
stream.ringBuffer.push(snapshot);
|
|
1131
|
+
if (this.isKeyframeRevision(revision)) this.keyframeStore.putKeyframe(documentId, scope, branch, revision, {
|
|
1132
|
+
...document,
|
|
1133
|
+
operations: {},
|
|
1134
|
+
clipboard: []
|
|
1135
|
+
}).catch((err) => {
|
|
1136
|
+
console.error(`Failed to persist keyframe ${documentId}@${revision}:`, err);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Invalidates cached document streams.
|
|
1141
|
+
*
|
|
1142
|
+
* Supports three invalidation scopes:
|
|
1143
|
+
* - Document-level: invalidate(documentId) - removes all streams for document
|
|
1144
|
+
* - Scope-level: invalidate(documentId, scope) - removes all branches for scope
|
|
1145
|
+
* - Stream-level: invalidate(documentId, scope, branch) - removes specific stream
|
|
1146
|
+
*
|
|
1147
|
+
* @param documentId - The document identifier
|
|
1148
|
+
* @param scope - Optional scope to narrow invalidation
|
|
1149
|
+
* @param branch - Optional branch to narrow invalidation (requires scope)
|
|
1150
|
+
* @returns The number of streams evicted
|
|
1151
|
+
*/
|
|
1152
|
+
invalidate(documentId, scope, branch) {
|
|
1153
|
+
let evicted = 0;
|
|
1154
|
+
if (scope === void 0 && branch === void 0) {
|
|
1155
|
+
for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
|
|
1156
|
+
this.streams.delete(key);
|
|
1157
|
+
this.lruTracker.remove(key);
|
|
1158
|
+
evicted++;
|
|
1159
|
+
}
|
|
1160
|
+
} else if (scope !== void 0 && branch === void 0) {
|
|
1161
|
+
for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:${scope}:`)) {
|
|
1162
|
+
this.streams.delete(key);
|
|
1163
|
+
this.lruTracker.remove(key);
|
|
1164
|
+
evicted++;
|
|
1165
|
+
}
|
|
1166
|
+
} else if (scope !== void 0 && branch !== void 0) {
|
|
1167
|
+
const key = this.makeStreamKey(documentId, scope, branch);
|
|
1168
|
+
if (this.streams.has(key)) {
|
|
1169
|
+
this.streams.delete(key);
|
|
1170
|
+
this.lruTracker.remove(key);
|
|
1171
|
+
evicted = 1;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return evicted;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Clears the entire cache, removing all cached document streams.
|
|
1178
|
+
* Resets LRU tracking state. This operation always succeeds.
|
|
1179
|
+
*/
|
|
1180
|
+
clear() {
|
|
1181
|
+
this.streams.clear();
|
|
1182
|
+
this.lruTracker.clear();
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Retrieves a specific stream for a document. Exposed on the implementation
|
|
1186
|
+
* for testing, but not on the interface.
|
|
1187
|
+
*
|
|
1188
|
+
* @internal
|
|
1189
|
+
*/
|
|
1190
|
+
getStream(documentId, scope, branch) {
|
|
1191
|
+
const key = this.makeStreamKey(documentId, scope, branch);
|
|
1192
|
+
return this.streams.get(key);
|
|
1193
|
+
}
|
|
1194
|
+
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
1195
|
+
if (targetRevision === Number.MAX_SAFE_INTEGER || targetRevision <= 0) return;
|
|
1196
|
+
return this.keyframeStore.findNearestKeyframe(documentId, scope, branch, targetRevision, signal);
|
|
1197
|
+
}
|
|
1198
|
+
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
1199
|
+
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1200
|
+
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
1201
|
+
let document;
|
|
1202
|
+
let startRevision;
|
|
1203
|
+
let documentType;
|
|
1204
|
+
if (keyframe) {
|
|
1205
|
+
document = keyframe.document;
|
|
1206
|
+
startRevision = keyframe.revision;
|
|
1207
|
+
documentType = keyframe.document.header.documentType;
|
|
1208
|
+
} else {
|
|
1209
|
+
startRevision = -1;
|
|
1210
|
+
const createOpResult = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, {
|
|
1211
|
+
cursor: "0",
|
|
1212
|
+
limit: 1
|
|
1213
|
+
}, signal);
|
|
1214
|
+
if (createOpResult.results.length === 0) throw new Error(`Failed to rebuild document ${documentId}: no CREATE_DOCUMENT operation found in document scope`);
|
|
1215
|
+
const createOp = createOpResult.results[0];
|
|
1216
|
+
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Failed to rebuild document ${documentId}: first operation in document scope must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
1217
|
+
const documentCreateAction = createOp.action;
|
|
1218
|
+
documentType = documentCreateAction.input.model;
|
|
1219
|
+
if (!documentType) throw new Error(`Failed to rebuild document ${documentId}: CREATE_DOCUMENT action missing model in input`);
|
|
1220
|
+
document = createDocumentFromAction(documentCreateAction);
|
|
1221
|
+
let docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1222
|
+
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, 0, void 0, void 0, signal);
|
|
1223
|
+
for (const operation of docScopeOps.results) {
|
|
1224
|
+
if (operation.index === 0) continue;
|
|
1225
|
+
if (operation.action.type === "UPGRADE_DOCUMENT") {
|
|
1226
|
+
const upgradeAction = operation.action;
|
|
1227
|
+
document = applyUpgradeDocumentAction(document, upgradeAction);
|
|
1228
|
+
docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1229
|
+
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1230
|
+
else {
|
|
1231
|
+
const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
|
|
1232
|
+
document = docModule.reducer(document, operation.action, void 0, {
|
|
1233
|
+
skip: operation.skip,
|
|
1234
|
+
protocolVersion
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const module = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1240
|
+
let cursor = void 0;
|
|
1241
|
+
const pageSize = 100;
|
|
1242
|
+
let hasMorePages;
|
|
1243
|
+
do {
|
|
1244
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1245
|
+
const paging = {
|
|
1246
|
+
cursor: cursor || "0",
|
|
1247
|
+
limit: pageSize
|
|
1248
|
+
};
|
|
1249
|
+
try {
|
|
1250
|
+
const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
|
|
1251
|
+
for (const operation of result.results) {
|
|
1252
|
+
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1253
|
+
const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
|
|
1254
|
+
document = module.reducer(document, operation.action, void 0, {
|
|
1255
|
+
skip: operation.skip,
|
|
1256
|
+
protocolVersion
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
1260
|
+
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
1261
|
+
if (hasMorePages) cursor = result.nextCursor;
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1264
|
+
}
|
|
1265
|
+
} while (hasMorePages);
|
|
1266
|
+
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1267
|
+
document.header.revision = revisions.revision;
|
|
1268
|
+
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1269
|
+
return document;
|
|
1270
|
+
}
|
|
1271
|
+
async warmMissRebuild(baseDocument, baseRevision, documentId, scope, branch, targetRevision, signal) {
|
|
1272
|
+
const documentType = baseDocument.header.documentType;
|
|
1273
|
+
const module = this.registry.getModule(documentType);
|
|
1274
|
+
let document = baseDocument;
|
|
1275
|
+
try {
|
|
1276
|
+
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
1277
|
+
for (const operation of pagedResults.results) {
|
|
1278
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1279
|
+
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1280
|
+
const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
|
|
1281
|
+
document = module.reducer(document, operation.action, void 0, {
|
|
1282
|
+
skip: operation.skip,
|
|
1283
|
+
protocolVersion
|
|
1284
|
+
});
|
|
1285
|
+
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
1286
|
+
}
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1289
|
+
}
|
|
1290
|
+
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1291
|
+
document.header.revision = revisions.revision;
|
|
1292
|
+
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1293
|
+
return document;
|
|
1294
|
+
}
|
|
1295
|
+
findNearestOlderSnapshot(snapshots, targetRevision) {
|
|
1296
|
+
let nearest = void 0;
|
|
1297
|
+
for (const snapshot of snapshots) if (snapshot.revision < targetRevision) {
|
|
1298
|
+
if (!nearest || snapshot.revision > nearest.revision) nearest = snapshot;
|
|
1299
|
+
}
|
|
1300
|
+
return nearest;
|
|
1301
|
+
}
|
|
1302
|
+
makeStreamKey(documentId, scope, branch) {
|
|
1303
|
+
return `${documentId}:${scope}:${branch}`;
|
|
1304
|
+
}
|
|
1305
|
+
getOrCreateStream(key) {
|
|
1306
|
+
let stream = this.streams.get(key);
|
|
1307
|
+
if (!stream) {
|
|
1308
|
+
if (this.streams.size >= this.config.maxDocuments) {
|
|
1309
|
+
const evictKey = this.lruTracker.evict();
|
|
1310
|
+
if (evictKey) this.streams.delete(evictKey);
|
|
1311
|
+
}
|
|
1312
|
+
stream = {
|
|
1313
|
+
key,
|
|
1314
|
+
ringBuffer: new RingBuffer(this.config.ringBufferSize)
|
|
1315
|
+
};
|
|
1316
|
+
this.streams.set(key, stream);
|
|
1317
|
+
}
|
|
1318
|
+
this.lruTracker.touch(key);
|
|
1319
|
+
return stream;
|
|
1320
|
+
}
|
|
1321
|
+
isKeyframeRevision(revision) {
|
|
1322
|
+
return revision > 0 && revision % this.config.keyframeInterval === 0;
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/events/event-bus.ts
|
|
1327
|
+
var EventBus = class {
|
|
1328
|
+
eventTypeToSubscribers = /* @__PURE__ */ new Map();
|
|
1329
|
+
subscribe(type, subscriber) {
|
|
1330
|
+
let list = this.eventTypeToSubscribers.get(type);
|
|
1331
|
+
if (!list) {
|
|
1332
|
+
list = [];
|
|
1333
|
+
this.eventTypeToSubscribers.set(type, list);
|
|
1334
|
+
}
|
|
1335
|
+
list.push(subscriber);
|
|
1336
|
+
let done = false;
|
|
1337
|
+
return () => {
|
|
1338
|
+
if (done) return;
|
|
1339
|
+
done = true;
|
|
1340
|
+
const arr = this.eventTypeToSubscribers.get(type);
|
|
1341
|
+
if (!arr) return;
|
|
1342
|
+
const idx = arr.indexOf(subscriber);
|
|
1343
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
1344
|
+
if (arr.length === 0) this.eventTypeToSubscribers.delete(type);
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
async emit(type, data) {
|
|
1348
|
+
const list = this.eventTypeToSubscribers.get(type);
|
|
1349
|
+
if (!list || list.length === 0) return;
|
|
1350
|
+
const snapshot = list.slice();
|
|
1351
|
+
const errors = [];
|
|
1352
|
+
for (const fn of snapshot) try {
|
|
1353
|
+
await Promise.resolve(fn(type, data));
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
errors.push(err);
|
|
1356
|
+
}
|
|
1357
|
+
if (errors.length > 0) throw new EventBusAggregateError(errors);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/executor/execution-scope.ts
|
|
1362
|
+
var DefaultExecutionScope = class {
|
|
1363
|
+
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
1364
|
+
this.operationStore = operationStore;
|
|
1365
|
+
this.operationIndex = operationIndex;
|
|
1366
|
+
this.writeCache = writeCache;
|
|
1367
|
+
this.documentMetaCache = documentMetaCache;
|
|
1368
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
1369
|
+
}
|
|
1370
|
+
async run(fn, signal) {
|
|
1371
|
+
signal?.throwIfAborted();
|
|
1372
|
+
return fn({
|
|
1373
|
+
operationStore: this.operationStore,
|
|
1374
|
+
operationIndex: this.operationIndex,
|
|
1375
|
+
writeCache: this.writeCache,
|
|
1376
|
+
documentMetaCache: this.documentMetaCache,
|
|
1377
|
+
collectionMembershipCache: this.collectionMembershipCache
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
var KyselyExecutionScope = class {
|
|
1382
|
+
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
1383
|
+
this.db = db;
|
|
1384
|
+
this.operationStore = operationStore;
|
|
1385
|
+
this.operationIndex = operationIndex;
|
|
1386
|
+
this.keyframeStore = keyframeStore;
|
|
1387
|
+
this.writeCache = writeCache;
|
|
1388
|
+
this.documentMetaCache = documentMetaCache;
|
|
1389
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
1390
|
+
}
|
|
1391
|
+
async run(fn, signal) {
|
|
1392
|
+
signal?.throwIfAborted();
|
|
1393
|
+
return this.db.transaction().execute(async (trx) => {
|
|
1394
|
+
const scopedOperationStore = this.operationStore.withTransaction(trx);
|
|
1395
|
+
const scopedOperationIndex = this.operationIndex.withTransaction(trx);
|
|
1396
|
+
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
1397
|
+
return fn({
|
|
1398
|
+
operationStore: scopedOperationStore,
|
|
1399
|
+
operationIndex: scopedOperationIndex,
|
|
1400
|
+
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
1401
|
+
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
1402
|
+
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region src/utils/reshuffle.ts
|
|
1409
|
+
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
1410
|
+
"CREATE_DOCUMENT",
|
|
1411
|
+
"DELETE_DOCUMENT",
|
|
1412
|
+
"UPGRADE_DOCUMENT",
|
|
1413
|
+
"ADD_RELATIONSHIP",
|
|
1414
|
+
"REMOVE_RELATIONSHIP",
|
|
1415
|
+
"UPDATE_RELATIONSHIP",
|
|
1416
|
+
"ADD_FOLDER",
|
|
1417
|
+
"UPDATE_FOLDER",
|
|
1418
|
+
"REMOVE_FOLDER"
|
|
1419
|
+
]);
|
|
1420
|
+
/**
|
|
1421
|
+
* Reshuffles operations by timestamp, then applies deterministic tie-breaking.
|
|
1422
|
+
* Used for merging concurrent operations from different branches.
|
|
1423
|
+
*
|
|
1424
|
+
* For strict document-structure actions (e.g., CREATE_DOCUMENT/UPGRADE_DOCUMENT),
|
|
1425
|
+
* logical index (index - skip) is prioritized to preserve causal replay order.
|
|
1426
|
+
*
|
|
1427
|
+
* For other actions, action ID is prioritized to ensure a canonical cross-reactor order
|
|
1428
|
+
* for concurrent operations that may have diverged local indices due to prior reshuffles.
|
|
1429
|
+
* Logical index and operation ID are then used as deterministic tie-breakers.
|
|
1430
|
+
*
|
|
1431
|
+
* Example:
|
|
1432
|
+
* [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, 2:0, B3:0, B4:2, B5:0]
|
|
1433
|
+
* GC => [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, B4:2, B5:0]
|
|
1434
|
+
* Split => [0:0, 1:0] + [2:0, A3:0, A4:0, A5:0] + [B4:2, B5:0]
|
|
1435
|
+
* Reshuffle(6:4) => [6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
1436
|
+
* merge => [0:0, 1:0, 6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
1437
|
+
*/
|
|
1438
|
+
function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
1439
|
+
return [...opsA, ...opsB].sort((a, b) => {
|
|
1440
|
+
const timestampDiff = new Date(a.timestampUtcMs).getTime() - new Date(b.timestampUtcMs).getTime();
|
|
1441
|
+
if (timestampDiff !== 0) return timestampDiff;
|
|
1442
|
+
const shouldPrioritizeLogicalIndex = STRICT_ORDER_ACTION_TYPES.has(a.action?.type ?? "") || STRICT_ORDER_ACTION_TYPES.has(b.action?.type ?? "");
|
|
1443
|
+
const logicalIndexDiff = a.index - a.skip - (b.index - b.skip);
|
|
1444
|
+
if (shouldPrioritizeLogicalIndex) {
|
|
1445
|
+
if (logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1446
|
+
}
|
|
1447
|
+
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
1448
|
+
if (actionIdDiff !== 0) return actionIdDiff;
|
|
1449
|
+
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1450
|
+
return a.id.localeCompare(b.id);
|
|
1451
|
+
}).map((op, i) => ({
|
|
1452
|
+
...op,
|
|
1453
|
+
index: startIndex.index + i,
|
|
1454
|
+
skip: i === 0 ? startIndex.skip : 0
|
|
1455
|
+
}));
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/cache/operation-index-types.ts
|
|
1459
|
+
function driveCollectionId(branch, driveId) {
|
|
1460
|
+
return `drive.${branch}.${driveId}`;
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/executor/document-action-handler.ts
|
|
1464
|
+
var DocumentActionHandler = class {
|
|
1465
|
+
constructor(registry, logger, driveContainerTypes) {
|
|
1466
|
+
this.registry = registry;
|
|
1467
|
+
this.logger = logger;
|
|
1468
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
1469
|
+
}
|
|
1470
|
+
async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
1471
|
+
switch (action.type) {
|
|
1472
|
+
case "CREATE_DOCUMENT": return this.executeCreate(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
|
|
1473
|
+
case "DELETE_DOCUMENT": return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1474
|
+
case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
|
|
1475
|
+
case "ADD_RELATIONSHIP": return this.executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1476
|
+
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1477
|
+
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1478
|
+
default: return buildErrorResult(job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), startTime);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
async executeCreate(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
1482
|
+
if (job.scope !== "document") return {
|
|
1483
|
+
job,
|
|
1484
|
+
success: false,
|
|
1485
|
+
error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
|
|
1486
|
+
duration: Date.now() - startTime
|
|
1487
|
+
};
|
|
1488
|
+
const document = createDocumentFromAction(action);
|
|
1489
|
+
let operation = createOperation(action, 0, skip, {
|
|
1490
|
+
documentId: document.header.id,
|
|
1491
|
+
scope: job.scope,
|
|
1492
|
+
branch: job.branch
|
|
1493
|
+
});
|
|
1494
|
+
const resultingStateObj = {
|
|
1495
|
+
header: document.header,
|
|
1496
|
+
...document.state
|
|
1497
|
+
};
|
|
1498
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
1499
|
+
const writeResult = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
1500
|
+
if (!Array.isArray(writeResult)) return writeResult;
|
|
1501
|
+
operation = writeResult[0];
|
|
1502
|
+
updateDocumentRevision(document, job.scope, operation.index);
|
|
1503
|
+
document.operations = {
|
|
1504
|
+
...document.operations,
|
|
1505
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1506
|
+
};
|
|
1507
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
1508
|
+
indexTxn.write([{
|
|
1509
|
+
...operation,
|
|
1510
|
+
documentId: document.header.id,
|
|
1511
|
+
documentType: document.header.documentType,
|
|
1512
|
+
branch: job.branch,
|
|
1513
|
+
scope: job.scope,
|
|
1514
|
+
sourceRemote
|
|
1515
|
+
}]);
|
|
1516
|
+
if (this.driveContainerTypes.has(document.header.documentType)) {
|
|
1517
|
+
const collectionId = driveCollectionId(job.branch, document.header.id);
|
|
1518
|
+
indexTxn.createCollection(collectionId);
|
|
1519
|
+
indexTxn.addToCollection(collectionId, document.header.id);
|
|
1520
|
+
}
|
|
1521
|
+
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
1522
|
+
state: document.state.document,
|
|
1523
|
+
documentType: document.header.documentType,
|
|
1524
|
+
documentScopeRevision: 1
|
|
1525
|
+
});
|
|
1526
|
+
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
1527
|
+
}
|
|
1528
|
+
async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
1529
|
+
const input = action.input;
|
|
1530
|
+
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1531
|
+
const documentId = input.documentId;
|
|
1532
|
+
let document;
|
|
1533
|
+
try {
|
|
1534
|
+
document = await stores.writeCache.getState(documentId, job.scope, job.branch, void 0, signal);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1537
|
+
}
|
|
1538
|
+
const documentState = document.state.document;
|
|
1539
|
+
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1540
|
+
let operation = createOperation(action, getNextIndexForScope(document, job.scope), 0, {
|
|
1541
|
+
documentId,
|
|
1542
|
+
scope: job.scope,
|
|
1543
|
+
branch: job.branch
|
|
1544
|
+
});
|
|
1545
|
+
applyDeleteDocumentAction(document, action);
|
|
1546
|
+
const resultingStateObj = {
|
|
1547
|
+
header: document.header,
|
|
1548
|
+
document: document.state.document
|
|
1549
|
+
};
|
|
1550
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
1551
|
+
const writeResult = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
1552
|
+
if (!Array.isArray(writeResult)) return writeResult;
|
|
1553
|
+
operation = writeResult[0];
|
|
1554
|
+
updateDocumentRevision(document, job.scope, operation.index);
|
|
1555
|
+
document.operations = {
|
|
1556
|
+
...document.operations,
|
|
1557
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1558
|
+
};
|
|
1559
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
1560
|
+
indexTxn.write([{
|
|
1561
|
+
...operation,
|
|
1562
|
+
documentId,
|
|
1563
|
+
documentType: document.header.documentType,
|
|
1564
|
+
branch: job.branch,
|
|
1565
|
+
scope: job.scope,
|
|
1566
|
+
sourceRemote
|
|
1567
|
+
}]);
|
|
1568
|
+
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
1569
|
+
state: document.state.document,
|
|
1570
|
+
documentType: document.header.documentType,
|
|
1571
|
+
documentScopeRevision: operation.index + 1
|
|
1572
|
+
});
|
|
1573
|
+
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
1574
|
+
}
|
|
1575
|
+
async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
1576
|
+
const input = action.input;
|
|
1577
|
+
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1578
|
+
const documentId = input.documentId;
|
|
1579
|
+
const fromVersion = input.fromVersion;
|
|
1580
|
+
const toVersion = input.toVersion;
|
|
1581
|
+
let document;
|
|
1582
|
+
try {
|
|
1583
|
+
document = await stores.writeCache.getState(documentId, job.scope, job.branch, void 0, signal);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1586
|
+
}
|
|
1587
|
+
const documentState = document.state.document;
|
|
1588
|
+
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1589
|
+
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
1590
|
+
let upgradePath;
|
|
1591
|
+
if (fromVersion > 0 && fromVersion < toVersion) try {
|
|
1592
|
+
upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1595
|
+
}
|
|
1596
|
+
if (fromVersion === toVersion && fromVersion > 0) return {
|
|
1597
|
+
job,
|
|
1598
|
+
success: true,
|
|
1599
|
+
operations: [],
|
|
1600
|
+
operationsWithContext: [],
|
|
1601
|
+
duration: Date.now() - startTime
|
|
1602
|
+
};
|
|
1603
|
+
try {
|
|
1604
|
+
document = applyUpgradeDocumentAction(document, action, upgradePath);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1607
|
+
}
|
|
1608
|
+
let operation = createOperation(action, nextIndex, skip, {
|
|
1609
|
+
documentId,
|
|
1610
|
+
scope: job.scope,
|
|
1611
|
+
branch: job.branch
|
|
1612
|
+
});
|
|
1613
|
+
const resultingStateObj = {
|
|
1614
|
+
header: document.header,
|
|
1615
|
+
...document.state
|
|
1616
|
+
};
|
|
1617
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
1618
|
+
const writeResult = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
1619
|
+
if (!Array.isArray(writeResult)) return writeResult;
|
|
1620
|
+
operation = writeResult[0];
|
|
1621
|
+
updateDocumentRevision(document, job.scope, operation.index);
|
|
1622
|
+
document.operations = {
|
|
1623
|
+
...document.operations,
|
|
1624
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1625
|
+
};
|
|
1626
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
1627
|
+
indexTxn.write([{
|
|
1628
|
+
...operation,
|
|
1629
|
+
documentId,
|
|
1630
|
+
documentType: document.header.documentType,
|
|
1631
|
+
branch: job.branch,
|
|
1632
|
+
scope: job.scope,
|
|
1633
|
+
sourceRemote
|
|
1634
|
+
}]);
|
|
1635
|
+
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
1636
|
+
state: document.state.document,
|
|
1637
|
+
documentType: document.header.documentType,
|
|
1638
|
+
documentScopeRevision: operation.index + 1
|
|
1639
|
+
});
|
|
1640
|
+
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
1641
|
+
}
|
|
1642
|
+
executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
1643
|
+
return this.withRelationshipAction("ADD_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, (input) => input.sourceId === input.targetId ? /* @__PURE__ */ new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)") : null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
1644
|
+
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
1645
|
+
const collectionId = driveCollectionId(j.branch, input.sourceId);
|
|
1646
|
+
txn.addToCollection(collectionId, input.targetId);
|
|
1647
|
+
s.collectionMembershipCache.invalidate(input.targetId);
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
1652
|
+
return this.withRelationshipAction("REMOVE_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
1653
|
+
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
1654
|
+
const collectionId = driveCollectionId(j.branch, input.sourceId);
|
|
1655
|
+
txn.removeFromCollection(collectionId, input.targetId);
|
|
1656
|
+
s.collectionMembershipCache.invalidate(input.targetId);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
1661
|
+
return this.withRelationshipAction("UPDATE_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, null, null);
|
|
1662
|
+
}
|
|
1663
|
+
async withRelationshipAction(actionTypeName, job, action, startTime, indexTxn, stores, sourceRemote, signal, preValidate, postWrite) {
|
|
1664
|
+
if (job.scope !== "document") return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} must be in "document" scope, got "${job.scope}"`), startTime);
|
|
1665
|
+
const input = action.input;
|
|
1666
|
+
if (!input.sourceId || !input.targetId || !input.relationshipType) return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} action requires sourceId, targetId, and relationshipType in input`), startTime);
|
|
1667
|
+
if (preValidate !== null) {
|
|
1668
|
+
const validationError = preValidate(input);
|
|
1669
|
+
if (validationError !== null) return buildErrorResult(job, validationError, startTime);
|
|
1670
|
+
}
|
|
1671
|
+
let sourceDoc;
|
|
1672
|
+
try {
|
|
1673
|
+
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1676
|
+
}
|
|
1677
|
+
let operation = createOperation(action, getNextIndexForScope(sourceDoc, job.scope), 0, {
|
|
1678
|
+
documentId: input.sourceId,
|
|
1679
|
+
scope: job.scope,
|
|
1680
|
+
branch: job.branch
|
|
1681
|
+
});
|
|
1682
|
+
const writeResult = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
1683
|
+
if (!Array.isArray(writeResult)) return writeResult;
|
|
1684
|
+
operation = writeResult[0];
|
|
1685
|
+
sourceDoc.header.lastModifiedAtUtcIso = operation.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString();
|
|
1686
|
+
updateDocumentRevision(sourceDoc, job.scope, operation.index);
|
|
1687
|
+
sourceDoc.operations = {
|
|
1688
|
+
...sourceDoc.operations,
|
|
1689
|
+
[job.scope]: [...sourceDoc.operations[job.scope] ?? [], operation]
|
|
1690
|
+
};
|
|
1691
|
+
const scopeState = sourceDoc.state[job.scope];
|
|
1692
|
+
const resultingStateObj = {
|
|
1693
|
+
header: structuredClone(sourceDoc.header),
|
|
1694
|
+
[job.scope]: scopeState === void 0 ? {} : structuredClone(scopeState)
|
|
1695
|
+
};
|
|
1696
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
1697
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
1698
|
+
indexTxn.write([{
|
|
1699
|
+
...operation,
|
|
1700
|
+
documentId: input.sourceId,
|
|
1701
|
+
documentType: sourceDoc.header.documentType,
|
|
1702
|
+
branch: job.branch,
|
|
1703
|
+
scope: job.scope,
|
|
1704
|
+
sourceRemote
|
|
1705
|
+
}]);
|
|
1706
|
+
if (postWrite !== null) postWrite({
|
|
1707
|
+
indexTxn,
|
|
1708
|
+
stores,
|
|
1709
|
+
sourceDoc,
|
|
1710
|
+
input,
|
|
1711
|
+
job
|
|
1712
|
+
});
|
|
1713
|
+
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
1714
|
+
state: sourceDoc.state.document,
|
|
1715
|
+
documentType: sourceDoc.header.documentType,
|
|
1716
|
+
documentScopeRevision: operation.index + 1
|
|
1717
|
+
});
|
|
1718
|
+
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
1719
|
+
}
|
|
1720
|
+
async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime, stores, signal) {
|
|
1721
|
+
let storedOperations;
|
|
1722
|
+
try {
|
|
1723
|
+
storedOperations = await stores.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
|
|
1724
|
+
txn.addOperations(operation);
|
|
1725
|
+
}, signal);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
1728
|
+
stores.writeCache.invalidate(documentId, scope, branch);
|
|
1729
|
+
return {
|
|
1730
|
+
job,
|
|
1731
|
+
success: false,
|
|
1732
|
+
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
1733
|
+
duration: Date.now() - startTime
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
return storedOperations;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/executor/signature-verifier.ts
|
|
1741
|
+
var SignatureVerifier = class {
|
|
1742
|
+
constructor(verifier) {
|
|
1743
|
+
this.verifier = verifier;
|
|
1744
|
+
}
|
|
1745
|
+
async verifyActions(documentId, branch, actions) {
|
|
1746
|
+
if (!this.verifier) return;
|
|
1747
|
+
for (const action of actions) {
|
|
1748
|
+
const signer = action.context?.signer;
|
|
1749
|
+
if (!signer) continue;
|
|
1750
|
+
if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Action ${action.id} has signer but no signatures`);
|
|
1751
|
+
const publicKey = signer.app.key;
|
|
1752
|
+
let isValid;
|
|
1753
|
+
try {
|
|
1754
|
+
const tempOperation = {
|
|
1755
|
+
id: deriveOperationId(documentId, action.scope, branch, action.id),
|
|
1756
|
+
index: 0,
|
|
1757
|
+
timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1758
|
+
hash: "",
|
|
1759
|
+
skip: 0,
|
|
1760
|
+
action
|
|
1761
|
+
};
|
|
1762
|
+
isValid = await this.verifier(tempOperation, publicKey);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1765
|
+
throw new InvalidSignatureError(documentId, `Action ${action.id} verification failed: ${errorMessage}`);
|
|
1766
|
+
}
|
|
1767
|
+
if (!isValid) throw new InvalidSignatureError(documentId, `Action ${action.id} signature verification returned false`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
async verifyOperations(documentId, operations) {
|
|
1771
|
+
if (!this.verifier) return;
|
|
1772
|
+
for (let i = 0; i < operations.length; i++) {
|
|
1773
|
+
const operation = operations[i];
|
|
1774
|
+
const signer = operation.action.context?.signer;
|
|
1775
|
+
if (!signer) continue;
|
|
1776
|
+
if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
1777
|
+
const publicKey = signer.app.key;
|
|
1778
|
+
let isValid;
|
|
1779
|
+
try {
|
|
1780
|
+
isValid = await this.verifier(operation, publicKey);
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1783
|
+
throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
1784
|
+
}
|
|
1785
|
+
if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/executor/simple-job-executor.ts
|
|
1791
|
+
const MAX_SKIP_THRESHOLD = 1e3;
|
|
1792
|
+
const ISO_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
1793
|
+
function isValidISOTimestamp(value) {
|
|
1794
|
+
if (!ISO_TIMESTAMP_REGEX.test(value)) return false;
|
|
1795
|
+
return !isNaN(new Date(value).getTime());
|
|
1796
|
+
}
|
|
1797
|
+
const documentScopeActions = [
|
|
1798
|
+
"CREATE_DOCUMENT",
|
|
1799
|
+
"DELETE_DOCUMENT",
|
|
1800
|
+
"UPGRADE_DOCUMENT",
|
|
1801
|
+
"ADD_RELATIONSHIP",
|
|
1802
|
+
"REMOVE_RELATIONSHIP",
|
|
1803
|
+
"UPDATE_RELATIONSHIP"
|
|
1804
|
+
];
|
|
1805
|
+
/**
|
|
1806
|
+
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
1807
|
+
*/
|
|
1808
|
+
var SimpleJobExecutor = class {
|
|
1809
|
+
config;
|
|
1810
|
+
signatureVerifierModule;
|
|
1811
|
+
documentActionHandler;
|
|
1812
|
+
executionScope;
|
|
1813
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, signatureVerifier, executionScope) {
|
|
1814
|
+
this.logger = logger;
|
|
1815
|
+
this.registry = registry;
|
|
1816
|
+
this.operationStore = operationStore;
|
|
1817
|
+
this.eventBus = eventBus;
|
|
1818
|
+
this.writeCache = writeCache;
|
|
1819
|
+
this.operationIndex = operationIndex;
|
|
1820
|
+
this.documentMetaCache = documentMetaCache;
|
|
1821
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
1822
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
1823
|
+
this.config = {
|
|
1824
|
+
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
1825
|
+
maxConcurrency: config.maxConcurrency ?? 1,
|
|
1826
|
+
jobTimeoutMs: config.jobTimeoutMs ?? 3e4,
|
|
1827
|
+
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
1828
|
+
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
1829
|
+
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
1830
|
+
};
|
|
1831
|
+
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
1832
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes);
|
|
1833
|
+
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Execute a single job by applying all its actions through the appropriate reducers.
|
|
1837
|
+
* Actions are processed sequentially in order.
|
|
1838
|
+
*/
|
|
1839
|
+
async executeJob(job, signal) {
|
|
1840
|
+
const startTime = Date.now();
|
|
1841
|
+
const touchedCacheEntries = [];
|
|
1842
|
+
let pendingEvent;
|
|
1843
|
+
let result;
|
|
1844
|
+
try {
|
|
1845
|
+
result = await this.executionScope.run(async (stores) => {
|
|
1846
|
+
const indexTxn = stores.operationIndex.start();
|
|
1847
|
+
if (job.kind === "load") {
|
|
1848
|
+
const loadResult = await this.executeLoadJob(job, startTime, indexTxn, stores, signal);
|
|
1849
|
+
if (loadResult.success && loadResult.operationsWithContext) {
|
|
1850
|
+
for (const owc of loadResult.operationsWithContext) touchedCacheEntries.push({
|
|
1851
|
+
documentId: owc.context.documentId,
|
|
1852
|
+
scope: owc.context.scope,
|
|
1853
|
+
branch: owc.context.branch
|
|
1854
|
+
});
|
|
1855
|
+
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
1856
|
+
for (let i = 0; i < loadResult.operationsWithContext.length; i++) loadResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
1857
|
+
const collectionMemberships = loadResult.operationsWithContext.length > 0 ? await this.getCollectionMembershipsForOperations(loadResult.operationsWithContext, stores) : {};
|
|
1858
|
+
pendingEvent = {
|
|
1859
|
+
jobId: job.id,
|
|
1860
|
+
operations: loadResult.operationsWithContext,
|
|
1861
|
+
jobMeta: job.meta,
|
|
1862
|
+
collectionMemberships
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
return loadResult;
|
|
1866
|
+
}
|
|
1867
|
+
const actionResult = await this.processActions(job, job.actions, startTime, indexTxn, stores, void 0, void 0, "", signal);
|
|
1868
|
+
if (!actionResult.success) return {
|
|
1869
|
+
job,
|
|
1870
|
+
success: false,
|
|
1871
|
+
error: actionResult.error,
|
|
1872
|
+
duration: Date.now() - startTime
|
|
1873
|
+
};
|
|
1874
|
+
if (actionResult.operationsWithContext.length > 0) for (const owc of actionResult.operationsWithContext) touchedCacheEntries.push({
|
|
1875
|
+
documentId: owc.context.documentId,
|
|
1876
|
+
scope: owc.context.scope,
|
|
1877
|
+
branch: owc.context.branch
|
|
1878
|
+
});
|
|
1879
|
+
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
1880
|
+
if (actionResult.operationsWithContext.length > 0) {
|
|
1881
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
1882
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(actionResult.operationsWithContext, stores);
|
|
1883
|
+
pendingEvent = {
|
|
1884
|
+
jobId: job.id,
|
|
1885
|
+
operations: actionResult.operationsWithContext,
|
|
1886
|
+
jobMeta: job.meta,
|
|
1887
|
+
collectionMemberships
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
job,
|
|
1892
|
+
success: true,
|
|
1893
|
+
operations: actionResult.generatedOperations,
|
|
1894
|
+
operationsWithContext: actionResult.operationsWithContext,
|
|
1895
|
+
duration: Date.now() - startTime
|
|
1896
|
+
};
|
|
1897
|
+
}, signal);
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
for (const entry of touchedCacheEntries) {
|
|
1900
|
+
this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
|
|
1901
|
+
this.documentMetaCache.invalidate(entry.documentId, entry.branch);
|
|
1902
|
+
}
|
|
1903
|
+
throw error;
|
|
1904
|
+
}
|
|
1905
|
+
if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
|
|
1906
|
+
this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
|
|
1907
|
+
});
|
|
1908
|
+
return result;
|
|
1909
|
+
}
|
|
1910
|
+
async getCollectionMembershipsForOperations(operations, stores) {
|
|
1911
|
+
const documentIds = [...new Set(operations.map((op) => op.context.documentId))];
|
|
1912
|
+
return stores.collectionMembershipCache.getCollectionsForDocuments(documentIds);
|
|
1913
|
+
}
|
|
1914
|
+
async processActions(job, actions, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "", signal) {
|
|
1915
|
+
const generatedOperations = [];
|
|
1916
|
+
const operationsWithContext = [];
|
|
1917
|
+
try {
|
|
1918
|
+
await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
return {
|
|
1921
|
+
success: false,
|
|
1922
|
+
generatedOperations,
|
|
1923
|
+
operationsWithContext,
|
|
1924
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
1928
|
+
success: false,
|
|
1929
|
+
generatedOperations,
|
|
1930
|
+
operationsWithContext,
|
|
1931
|
+
error: /* @__PURE__ */ new Error(`Invalid timestamp "${action.timestampUtcMs}" on action ${action.type} (id: ${action.id})`)
|
|
1932
|
+
};
|
|
1933
|
+
let lastYield = performance.now();
|
|
1934
|
+
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
1935
|
+
const action = actions[actionIndex];
|
|
1936
|
+
const skip = skipValues?.[actionIndex] ?? 0;
|
|
1937
|
+
const sourceOperation = sourceOperations?.[actionIndex];
|
|
1938
|
+
const result = documentScopeActions.includes(action.type) ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal) : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote, signal);
|
|
1939
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
1940
|
+
if (error !== null) return {
|
|
1941
|
+
success: false,
|
|
1942
|
+
generatedOperations,
|
|
1943
|
+
operationsWithContext,
|
|
1944
|
+
error: error.error
|
|
1945
|
+
};
|
|
1946
|
+
if (performance.now() - lastYield > this.config.yieldDeadlineMs) {
|
|
1947
|
+
await yieldToMain();
|
|
1948
|
+
lastYield = performance.now();
|
|
1949
|
+
if (signal?.aborted) return {
|
|
1950
|
+
success: false,
|
|
1951
|
+
generatedOperations,
|
|
1952
|
+
operationsWithContext,
|
|
1953
|
+
error: /* @__PURE__ */ new Error("Aborted")
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return {
|
|
1958
|
+
success: true,
|
|
1959
|
+
generatedOperations,
|
|
1960
|
+
operationsWithContext
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "", signal) {
|
|
1964
|
+
let docMeta;
|
|
1965
|
+
try {
|
|
1966
|
+
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1969
|
+
}
|
|
1970
|
+
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
1971
|
+
if (isUndoRedo(action) || action.type === "PRUNE" || action.type === "NOOP" && skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
1972
|
+
let document;
|
|
1973
|
+
try {
|
|
1974
|
+
document = await stores.writeCache.getState(job.documentId, job.scope, job.branch, void 0, signal);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1977
|
+
}
|
|
1978
|
+
let module;
|
|
1979
|
+
try {
|
|
1980
|
+
const moduleVersion = docMeta.state.version === 0 ? void 0 : docMeta.state.version;
|
|
1981
|
+
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1984
|
+
}
|
|
1985
|
+
let updatedDocument;
|
|
1986
|
+
try {
|
|
1987
|
+
const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
|
|
1988
|
+
const reducerOptions = sourceOperation ? {
|
|
1989
|
+
skip,
|
|
1990
|
+
branch: job.branch,
|
|
1991
|
+
replayOptions: { operation: sourceOperation },
|
|
1992
|
+
protocolVersion
|
|
1993
|
+
} : {
|
|
1994
|
+
skip,
|
|
1995
|
+
branch: job.branch,
|
|
1996
|
+
protocolVersion
|
|
1997
|
+
};
|
|
1998
|
+
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
|
|
2001
|
+
const enhancedError = new Error(contextMessage);
|
|
2002
|
+
if (error instanceof Error && error.stack) enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
2003
|
+
return buildErrorResult(job, enhancedError, startTime);
|
|
2004
|
+
}
|
|
2005
|
+
const scope = job.scope;
|
|
2006
|
+
const operations = updatedDocument.operations[scope];
|
|
2007
|
+
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
2008
|
+
const newOperation = operations[operations.length - 1];
|
|
2009
|
+
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
2010
|
+
const resultingState = JSON.stringify({
|
|
2011
|
+
...updatedDocument.state,
|
|
2012
|
+
header: updatedDocument.header
|
|
2013
|
+
});
|
|
2014
|
+
let storedOperations;
|
|
2015
|
+
try {
|
|
2016
|
+
storedOperations = await stores.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
2017
|
+
txn.addOperations(newOperation);
|
|
2018
|
+
}, signal);
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
|
|
2021
|
+
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2022
|
+
return {
|
|
2023
|
+
job,
|
|
2024
|
+
success: false,
|
|
2025
|
+
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2026
|
+
duration: Date.now() - startTime
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
const storedOperation = storedOperations[0];
|
|
2030
|
+
updatedDocument.header.revision = {
|
|
2031
|
+
...updatedDocument.header.revision,
|
|
2032
|
+
[scope]: storedOperation.index + 1
|
|
2033
|
+
};
|
|
2034
|
+
stores.writeCache.putState(job.documentId, scope, job.branch, storedOperation.index, updatedDocument);
|
|
2035
|
+
indexTxn.write([{
|
|
2036
|
+
...storedOperation,
|
|
2037
|
+
documentId: job.documentId,
|
|
2038
|
+
documentType: document.header.documentType,
|
|
2039
|
+
branch: job.branch,
|
|
2040
|
+
scope,
|
|
2041
|
+
sourceRemote
|
|
2042
|
+
}]);
|
|
2043
|
+
return {
|
|
2044
|
+
job,
|
|
2045
|
+
success: true,
|
|
2046
|
+
operations: [storedOperation],
|
|
2047
|
+
operationsWithContext: [{
|
|
2048
|
+
operation: storedOperation,
|
|
2049
|
+
context: {
|
|
2050
|
+
documentId: job.documentId,
|
|
2051
|
+
scope,
|
|
2052
|
+
branch: job.branch,
|
|
2053
|
+
documentType: document.header.documentType,
|
|
2054
|
+
resultingState,
|
|
2055
|
+
ordinal: 0
|
|
2056
|
+
}
|
|
2057
|
+
}],
|
|
2058
|
+
duration: Date.now() - startTime
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
async executeLoadJob(job, startTime, indexTxn, stores, signal) {
|
|
2062
|
+
if (job.operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("Load job must include at least one operation"), startTime);
|
|
2063
|
+
let docMeta;
|
|
2064
|
+
try {
|
|
2065
|
+
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
2066
|
+
} catch {}
|
|
2067
|
+
if (docMeta?.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2068
|
+
const scope = job.scope;
|
|
2069
|
+
let latestRevision;
|
|
2070
|
+
try {
|
|
2071
|
+
latestRevision = (await stores.operationStore.getRevisions(job.documentId, job.branch, signal)).revision[scope] ?? 0;
|
|
2072
|
+
} catch {
|
|
2073
|
+
latestRevision = 0;
|
|
2074
|
+
}
|
|
2075
|
+
for (const operation of job.operations) if (operation.timestampUtcMs && !isValidISOTimestamp(operation.timestampUtcMs)) return {
|
|
2076
|
+
job,
|
|
2077
|
+
success: false,
|
|
2078
|
+
error: /* @__PURE__ */ new Error(`Invalid timestamp "${operation.timestampUtcMs}" on operation (index: ${operation.index})`),
|
|
2079
|
+
duration: Date.now() - startTime
|
|
2080
|
+
};
|
|
2081
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
2082
|
+
let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
|
|
2083
|
+
for (const operation of job.operations) {
|
|
2084
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
2085
|
+
const ts = operation.timestampUtcMs || "";
|
|
2086
|
+
if (ts < minIncomingTimestamp) minIncomingTimestamp = ts;
|
|
2087
|
+
}
|
|
2088
|
+
let conflictingOps;
|
|
2089
|
+
try {
|
|
2090
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
2091
|
+
} catch {
|
|
2092
|
+
conflictingOps = [];
|
|
2093
|
+
}
|
|
2094
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
2095
|
+
if (conflictingOps.length > 0) {
|
|
2096
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
2097
|
+
try {
|
|
2098
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
2099
|
+
} catch {
|
|
2100
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const incomingActionIds = new Set(job.operations.map((op) => op.action.id));
|
|
2104
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
2105
|
+
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
2106
|
+
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
2107
|
+
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
2108
|
+
}
|
|
2109
|
+
return true;
|
|
2110
|
+
});
|
|
2111
|
+
const existingOpsToReshuffle = nonSupersededOps;
|
|
2112
|
+
if (existingOpsToReshuffle.length > this.config.maxSkipThreshold) return {
|
|
2113
|
+
job,
|
|
2114
|
+
success: false,
|
|
2115
|
+
error: /* @__PURE__ */ new Error(`Excessive reshuffle detected: existing op count of ${existingOpsToReshuffle.length} exceeds threshold of ${this.config.maxSkipThreshold}. This indicates a significant divergence between local and incoming operations.`),
|
|
2116
|
+
duration: Date.now() - startTime
|
|
2117
|
+
};
|
|
2118
|
+
let skipCount = existingOpsToReshuffle.length;
|
|
2119
|
+
if (existingOpsToReshuffle.length > 0) {
|
|
2120
|
+
let minLogicalIndex = Number.POSITIVE_INFINITY;
|
|
2121
|
+
for (const op of existingOpsToReshuffle) {
|
|
2122
|
+
const logical = op.index - op.skip;
|
|
2123
|
+
if (logical < minLogicalIndex) minLogicalIndex = logical;
|
|
2124
|
+
}
|
|
2125
|
+
const logicalSkip = latestRevision - minLogicalIndex;
|
|
2126
|
+
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
2127
|
+
}
|
|
2128
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
2129
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
2130
|
+
const incomingOpsToApply = job.operations.filter((op) => {
|
|
2131
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
2132
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
2133
|
+
seenIncomingActionIds.add(op.action.id);
|
|
2134
|
+
return true;
|
|
2135
|
+
});
|
|
2136
|
+
if (incomingOpsToApply.length === 0) return {
|
|
2137
|
+
job,
|
|
2138
|
+
success: true,
|
|
2139
|
+
operations: [],
|
|
2140
|
+
operationsWithContext: [],
|
|
2141
|
+
duration: Date.now() - startTime
|
|
2142
|
+
};
|
|
2143
|
+
const reshuffledOperations = existingOpsToReshuffle.length === 0 && skipCount === 0 ? incomingOpsToApply.slice().sort((a, b) => a.index - b.index).map((operation, i) => ({
|
|
2144
|
+
...operation,
|
|
2145
|
+
index: latestRevision + i
|
|
2146
|
+
})) : reshuffleByTimestamp({
|
|
2147
|
+
index: latestRevision,
|
|
2148
|
+
skip: skipCount
|
|
2149
|
+
}, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
|
|
2150
|
+
...operation,
|
|
2151
|
+
id: operation.id
|
|
2152
|
+
})));
|
|
2153
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP") operation.skip = 1;
|
|
2154
|
+
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
2155
|
+
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
2156
|
+
const effectiveSourceRemote = skipCount > 0 ? "" : job.meta.sourceRemote || "";
|
|
2157
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote, signal);
|
|
2158
|
+
if (!result.success) return {
|
|
2159
|
+
job,
|
|
2160
|
+
success: false,
|
|
2161
|
+
error: result.error,
|
|
2162
|
+
duration: Date.now() - startTime
|
|
2163
|
+
};
|
|
2164
|
+
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2165
|
+
if (scope === "document") stores.documentMetaCache.invalidate(job.documentId, job.branch);
|
|
2166
|
+
return {
|
|
2167
|
+
job,
|
|
2168
|
+
success: true,
|
|
2169
|
+
operations: result.generatedOperations,
|
|
2170
|
+
operationsWithContext: result.operationsWithContext,
|
|
2171
|
+
duration: Date.now() - startTime
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
2175
|
+
if (!result.success) return result;
|
|
2176
|
+
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
2177
|
+
if (result.operationsWithContext) operationsWithContext.push(...result.operationsWithContext);
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
//#endregion
|
|
2182
|
+
//#region src/registry/implementation.ts
|
|
2183
|
+
/**
|
|
2184
|
+
* In-memory implementation of the IDocumentModelRegistry interface.
|
|
2185
|
+
* Manages document model modules with version-aware storage and upgrade manifest support.
|
|
2186
|
+
*/
|
|
2187
|
+
var DocumentModelRegistry = class {
|
|
2188
|
+
modules = [];
|
|
2189
|
+
manifests = [];
|
|
2190
|
+
registerModules(...modules) {
|
|
2191
|
+
return modules.map((module) => {
|
|
2192
|
+
try {
|
|
2193
|
+
const documentType = module.documentModel.global.id;
|
|
2194
|
+
const version = module.version ?? 1;
|
|
2195
|
+
for (let i = 0; i < this.modules.length; i++) {
|
|
2196
|
+
const existing = this.modules[i];
|
|
2197
|
+
const existingType = existing.documentModel.global.id;
|
|
2198
|
+
const existingVersion = existing.version ?? 1;
|
|
2199
|
+
if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
|
|
2200
|
+
}
|
|
2201
|
+
this.modules.push(module);
|
|
2202
|
+
return {
|
|
2203
|
+
status: "success",
|
|
2204
|
+
item: module
|
|
2205
|
+
};
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
return {
|
|
2208
|
+
status: "error",
|
|
2209
|
+
item: module,
|
|
2210
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
unregisterModules(...documentTypes) {
|
|
2216
|
+
let allFound = true;
|
|
2217
|
+
for (const documentType of documentTypes) {
|
|
2218
|
+
if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
|
|
2219
|
+
this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
|
|
2220
|
+
}
|
|
2221
|
+
return allFound;
|
|
2222
|
+
}
|
|
2223
|
+
getModule(documentType, version) {
|
|
2224
|
+
let latestModule;
|
|
2225
|
+
let latestVersion = -1;
|
|
2226
|
+
for (let i = 0; i < this.modules.length; i++) {
|
|
2227
|
+
const module = this.modules[i];
|
|
2228
|
+
const moduleType = module.documentModel.global.id;
|
|
2229
|
+
const moduleVersion = module.version ?? 1;
|
|
2230
|
+
if (moduleType === documentType) {
|
|
2231
|
+
if (version !== void 0 && moduleVersion === version) return module;
|
|
2232
|
+
if (moduleVersion > latestVersion) {
|
|
2233
|
+
latestModule = module;
|
|
2234
|
+
latestVersion = moduleVersion;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (version === void 0 && latestModule !== void 0) return latestModule;
|
|
2239
|
+
throw new ModuleNotFoundError(documentType, version);
|
|
2240
|
+
}
|
|
2241
|
+
getAllModules() {
|
|
2242
|
+
return [...this.modules];
|
|
2243
|
+
}
|
|
2244
|
+
clear() {
|
|
2245
|
+
this.modules = [];
|
|
2246
|
+
this.manifests = [];
|
|
2247
|
+
}
|
|
2248
|
+
getSupportedVersions(documentType) {
|
|
2249
|
+
const versions = [];
|
|
2250
|
+
for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
|
|
2251
|
+
if (versions.length === 0) throw new ModuleNotFoundError(documentType);
|
|
2252
|
+
return versions.sort((a, b) => a - b);
|
|
2253
|
+
}
|
|
2254
|
+
getLatestVersion(documentType) {
|
|
2255
|
+
let latest = -1;
|
|
2256
|
+
let found = false;
|
|
2257
|
+
for (const module of this.modules) if (module.documentModel.global.id === documentType) {
|
|
2258
|
+
found = true;
|
|
2259
|
+
const version = module.version ?? 1;
|
|
2260
|
+
if (version > latest) latest = version;
|
|
2261
|
+
}
|
|
2262
|
+
if (!found) throw new ModuleNotFoundError(documentType);
|
|
2263
|
+
return latest;
|
|
2264
|
+
}
|
|
2265
|
+
registerUpgradeManifests(...manifestsToRegister) {
|
|
2266
|
+
return manifestsToRegister.map((manifestToRegister) => {
|
|
2267
|
+
try {
|
|
2268
|
+
if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
|
|
2269
|
+
for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
|
|
2270
|
+
this.manifests.push(manifestToRegister);
|
|
2271
|
+
return {
|
|
2272
|
+
status: "success",
|
|
2273
|
+
item: manifestToRegister
|
|
2274
|
+
};
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
return {
|
|
2277
|
+
status: "error",
|
|
2278
|
+
item: manifestToRegister,
|
|
2279
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
unregisterUpgradeManifests(...documentTypes) {
|
|
2285
|
+
let allFound = true;
|
|
2286
|
+
for (const documentType of documentTypes) {
|
|
2287
|
+
if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
|
|
2288
|
+
this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
|
|
2289
|
+
}
|
|
2290
|
+
return allFound;
|
|
2291
|
+
}
|
|
2292
|
+
getUpgradeManifest(documentType) {
|
|
2293
|
+
for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
|
|
2294
|
+
throw new ManifestNotFoundError(documentType);
|
|
2295
|
+
}
|
|
2296
|
+
computeUpgradePath(documentType, fromVersion, toVersion) {
|
|
2297
|
+
if (fromVersion === toVersion) return [];
|
|
2298
|
+
if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
|
|
2299
|
+
const manifest = this.getUpgradeManifest(documentType);
|
|
2300
|
+
const path = [];
|
|
2301
|
+
for (let v = fromVersion + 1; v <= toVersion; v++) {
|
|
2302
|
+
const key = `v${v}`;
|
|
2303
|
+
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
|
|
2304
|
+
const transition = manifest.upgrades[key];
|
|
2305
|
+
path.push(transition);
|
|
2306
|
+
}
|
|
2307
|
+
return path;
|
|
2308
|
+
}
|
|
2309
|
+
getUpgradeReducer(documentType, fromVersion, toVersion) {
|
|
2310
|
+
if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
|
|
2311
|
+
const manifest = this.getUpgradeManifest(documentType);
|
|
2312
|
+
const key = `v${toVersion}`;
|
|
2313
|
+
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
|
|
2314
|
+
return manifest.upgrades[key].upgradeReducer;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
//#endregion
|
|
2318
|
+
//#region src/storage/kysely/keyframe-store.ts
|
|
2319
|
+
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
2320
|
+
trx;
|
|
2321
|
+
constructor(db) {
|
|
2322
|
+
this.db = db;
|
|
2323
|
+
}
|
|
2324
|
+
get queryExecutor() {
|
|
2325
|
+
return this.trx ?? this.db;
|
|
2326
|
+
}
|
|
2327
|
+
withTransaction(trx) {
|
|
2328
|
+
const instance = new KyselyKeyframeStore(this.db);
|
|
2329
|
+
instance.trx = trx;
|
|
2330
|
+
return instance;
|
|
2331
|
+
}
|
|
2332
|
+
async putKeyframe(documentId, scope, branch, revision, document, signal) {
|
|
2333
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2334
|
+
await this.queryExecutor.insertInto("Keyframe").values({
|
|
2335
|
+
documentId,
|
|
2336
|
+
documentType: document.header.documentType,
|
|
2337
|
+
scope,
|
|
2338
|
+
branch,
|
|
2339
|
+
revision,
|
|
2340
|
+
document
|
|
2341
|
+
}).onConflict((oc) => oc.columns([
|
|
2342
|
+
"documentId",
|
|
2343
|
+
"scope",
|
|
2344
|
+
"branch",
|
|
2345
|
+
"revision"
|
|
2346
|
+
]).doUpdateSet({ document })).execute();
|
|
2347
|
+
}
|
|
2348
|
+
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
2349
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2350
|
+
const row = await this.queryExecutor.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("revision", "<=", targetRevision).orderBy("revision", "desc").limit(1).executeTakeFirst();
|
|
2351
|
+
if (!row) return;
|
|
2352
|
+
return {
|
|
2353
|
+
revision: row.revision,
|
|
2354
|
+
document: row.document
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
async listKeyframes(documentId, scope, branch, signal) {
|
|
2358
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2359
|
+
let query = this.queryExecutor.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).orderBy("revision", "asc");
|
|
2360
|
+
if (scope !== void 0) query = query.where("scope", "=", scope);
|
|
2361
|
+
if (branch !== void 0) query = query.where("branch", "=", branch);
|
|
2362
|
+
return (await query.execute()).map((row) => ({
|
|
2363
|
+
scope: row.scope,
|
|
2364
|
+
branch: row.branch,
|
|
2365
|
+
revision: row.revision,
|
|
2366
|
+
document: row.document
|
|
2367
|
+
}));
|
|
2368
|
+
}
|
|
2369
|
+
async deleteKeyframes(documentId, scope, branch, signal) {
|
|
2370
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2371
|
+
let query = this.queryExecutor.deleteFrom("Keyframe").where("documentId", "=", documentId);
|
|
2372
|
+
if (scope !== void 0 && branch !== void 0) query = query.where("scope", "=", scope).where("branch", "=", branch);
|
|
2373
|
+
else if (scope !== void 0) query = query.where("scope", "=", scope);
|
|
2374
|
+
const result = await query.executeTakeFirst();
|
|
2375
|
+
return Number(result.numDeletedRows || 0n);
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
//#endregion
|
|
2379
|
+
//#region src/storage/kysely/pagination.ts
|
|
2380
|
+
const DEFAULT_LIMIT = 100;
|
|
2381
|
+
function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
2382
|
+
let hasMore = false;
|
|
2383
|
+
let items = rows;
|
|
2384
|
+
if (paging?.limit && rows.length > paging.limit) {
|
|
2385
|
+
hasMore = true;
|
|
2386
|
+
items = rows.slice(0, paging.limit);
|
|
2387
|
+
}
|
|
2388
|
+
const nextCursor = hasMore && items.length > 0 ? cursorOf(items[items.length - 1]).toString() : void 0;
|
|
2389
|
+
const cursor = paging?.cursor || "0";
|
|
2390
|
+
const limit = paging?.limit || DEFAULT_LIMIT;
|
|
2391
|
+
return {
|
|
2392
|
+
results: items.map(toItem),
|
|
2393
|
+
options: {
|
|
2394
|
+
cursor,
|
|
2395
|
+
limit
|
|
2396
|
+
},
|
|
2397
|
+
nextCursor,
|
|
2398
|
+
next: hasMore ? () => refetch(nextCursor, limit) : void 0
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/storage/interfaces.ts
|
|
2403
|
+
/**
|
|
2404
|
+
* Thrown when an operation with the same identity already exists in the store.
|
|
2405
|
+
*/
|
|
2406
|
+
var DuplicateOperationError = class extends Error {
|
|
2407
|
+
constructor(description) {
|
|
2408
|
+
super(`Duplicate operation: ${description}`);
|
|
2409
|
+
this.name = "DuplicateOperationError";
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
/**
|
|
2413
|
+
* Thrown when a concurrent write conflict is detected during an atomic apply.
|
|
2414
|
+
*/
|
|
2415
|
+
var OptimisticLockError = class extends Error {
|
|
2416
|
+
constructor(message) {
|
|
2417
|
+
super(message);
|
|
2418
|
+
this.name = "OptimisticLockError";
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
/**
|
|
2422
|
+
* Thrown when the caller-provided revision does not match the current
|
|
2423
|
+
* stored revision, indicating a stale read.
|
|
2424
|
+
*/
|
|
2425
|
+
var RevisionMismatchError = class extends Error {
|
|
2426
|
+
constructor(expected, actual) {
|
|
2427
|
+
super(`Revision mismatch: expected ${expected}, got ${actual}`);
|
|
2428
|
+
this.name = "RevisionMismatchError";
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
//#endregion
|
|
2432
|
+
//#region src/storage/txn.ts
|
|
2433
|
+
var AtomicTransaction = class {
|
|
2434
|
+
operations = [];
|
|
2435
|
+
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
2436
|
+
this.documentId = documentId;
|
|
2437
|
+
this.documentType = documentType;
|
|
2438
|
+
this.scope = scope;
|
|
2439
|
+
this.branch = branch;
|
|
2440
|
+
this.baseRevision = baseRevision;
|
|
2441
|
+
}
|
|
2442
|
+
addOperations(...operations) {
|
|
2443
|
+
for (const op of operations) this.operations.push({
|
|
2444
|
+
jobId: v4(),
|
|
2445
|
+
opId: op.id,
|
|
2446
|
+
prevOpId: "",
|
|
2447
|
+
documentId: this.documentId,
|
|
2448
|
+
documentType: this.documentType,
|
|
2449
|
+
scope: this.scope,
|
|
2450
|
+
branch: this.branch,
|
|
2451
|
+
timestampUtcMs: new Date(op.timestampUtcMs),
|
|
2452
|
+
index: op.index,
|
|
2453
|
+
action: JSON.stringify(op.action),
|
|
2454
|
+
skip: op.skip,
|
|
2455
|
+
error: op.error || null,
|
|
2456
|
+
hash: op.hash
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
getOperations() {
|
|
2460
|
+
return this.operations;
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
//#endregion
|
|
2464
|
+
//#region src/storage/kysely/store.ts
|
|
2465
|
+
var _UniqueConstraintContext = class extends Error {
|
|
2466
|
+
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
2467
|
+
super("unique constraint");
|
|
2468
|
+
this.documentId = documentId;
|
|
2469
|
+
this.scope = scope;
|
|
2470
|
+
this.branch = branch;
|
|
2471
|
+
this.revision = revision;
|
|
2472
|
+
this.stagedOps = stagedOps;
|
|
2473
|
+
this.name = "UniqueConstraintContext";
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
var KyselyOperationStore = class KyselyOperationStore {
|
|
2477
|
+
trx;
|
|
2478
|
+
constructor(db) {
|
|
2479
|
+
this.db = db;
|
|
2480
|
+
}
|
|
2481
|
+
get queryExecutor() {
|
|
2482
|
+
return this.trx ?? this.db;
|
|
2483
|
+
}
|
|
2484
|
+
withTransaction(trx) {
|
|
2485
|
+
const instance = new KyselyOperationStore(this.db);
|
|
2486
|
+
instance.trx = trx;
|
|
2487
|
+
return instance;
|
|
2488
|
+
}
|
|
2489
|
+
async apply(documentId, documentType, scope, branch, revision, fn, signal) {
|
|
2490
|
+
if (this.trx) {
|
|
2491
|
+
let executeResult = null;
|
|
2492
|
+
let uniqueCtx = null;
|
|
2493
|
+
try {
|
|
2494
|
+
executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
2497
|
+
else throw error;
|
|
2498
|
+
}
|
|
2499
|
+
if (uniqueCtx !== null) return this.resolveUniqueConstraint(uniqueCtx);
|
|
2500
|
+
return executeResult;
|
|
2501
|
+
} else {
|
|
2502
|
+
let transactionResult = null;
|
|
2503
|
+
let uniqueCtx = null;
|
|
2504
|
+
try {
|
|
2505
|
+
transactionResult = await this.db.transaction().execute(async (trx) => {
|
|
2506
|
+
return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
2507
|
+
});
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
2510
|
+
else throw error;
|
|
2511
|
+
}
|
|
2512
|
+
if (uniqueCtx !== null) return this.resolveUniqueConstraint(uniqueCtx);
|
|
2513
|
+
return transactionResult;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
async resolveUniqueConstraint(ctx) {
|
|
2517
|
+
let replayOps = null;
|
|
2518
|
+
try {
|
|
2519
|
+
replayOps = await this.findIdempotentReplay(this.db, ctx.documentId, ctx.scope, ctx.branch, ctx.revision, ctx.stagedOps);
|
|
2520
|
+
} catch {}
|
|
2521
|
+
if (replayOps !== null) return replayOps;
|
|
2522
|
+
const op = ctx.stagedOps[0];
|
|
2523
|
+
throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
|
|
2524
|
+
}
|
|
2525
|
+
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
|
|
2526
|
+
throwIfAborted(signal);
|
|
2527
|
+
const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
|
|
2528
|
+
await fn(atomicTxn);
|
|
2529
|
+
const operations = atomicTxn.getOperations();
|
|
2530
|
+
if (operations.length === 0) return [];
|
|
2531
|
+
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
2532
|
+
const currentRevision = latestOp ? latestOp.index : -1;
|
|
2533
|
+
if (currentRevision !== revision - 1) {
|
|
2534
|
+
let replayOps = null;
|
|
2535
|
+
try {
|
|
2536
|
+
replayOps = await this.findIdempotentReplay(trx, documentId, scope, branch, revision, operations);
|
|
2537
|
+
} catch {}
|
|
2538
|
+
if (replayOps !== null) return replayOps;
|
|
2539
|
+
throw new RevisionMismatchError(currentRevision + 1, revision);
|
|
2540
|
+
}
|
|
2541
|
+
let prevOpId = latestOp?.opId || "";
|
|
2542
|
+
for (const op of operations) {
|
|
2543
|
+
op.prevOpId = prevOpId;
|
|
2544
|
+
prevOpId = op.opId;
|
|
2545
|
+
}
|
|
2546
|
+
try {
|
|
2547
|
+
await trx.insertInto("Operation").values(operations).execute();
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
if (error instanceof Error && error.message.includes("unique constraint")) throw new _UniqueConstraintContext(documentId, scope, branch, revision, operations);
|
|
2550
|
+
throw error;
|
|
2551
|
+
}
|
|
2552
|
+
return operations.map((op) => ({
|
|
2553
|
+
index: op.index,
|
|
2554
|
+
timestampUtcMs: op.timestampUtcMs.toISOString(),
|
|
2555
|
+
hash: op.hash,
|
|
2556
|
+
skip: op.skip,
|
|
2557
|
+
error: op.error || void 0,
|
|
2558
|
+
id: op.opId,
|
|
2559
|
+
action: JSON.parse(op.action)
|
|
2560
|
+
}));
|
|
2561
|
+
}
|
|
2562
|
+
async findIdempotentReplay(executor, documentId, scope, branch, revision, stagedOps) {
|
|
2563
|
+
const minIndex = revision;
|
|
2564
|
+
const maxIndex = revision + stagedOps.length - 1;
|
|
2565
|
+
const storedRows = await executor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">=", minIndex).where("index", "<=", maxIndex).orderBy("index", "asc").execute();
|
|
2566
|
+
if (storedRows.length !== stagedOps.length) return null;
|
|
2567
|
+
for (let i = 0; i < stagedOps.length; i++) {
|
|
2568
|
+
const staged = stagedOps[i];
|
|
2569
|
+
const stored = storedRows[i];
|
|
2570
|
+
if (stored.opId !== staged.opId || stored.index !== staged.index || stored.skip !== staged.skip) return null;
|
|
2571
|
+
}
|
|
2572
|
+
return storedRows.map((row) => this.rowToOperation(row));
|
|
2573
|
+
}
|
|
2574
|
+
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
2575
|
+
throwIfAborted(signal);
|
|
2576
|
+
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
2577
|
+
if (filter) {
|
|
2578
|
+
if (filter.actionTypes && filter.actionTypes.length > 0) {
|
|
2579
|
+
const actionTypesArray = filter.actionTypes.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
|
|
2580
|
+
query = query.where(sql`action->>'type' = ANY(ARRAY[${sql.raw(actionTypesArray)}]::text[])`);
|
|
2581
|
+
}
|
|
2582
|
+
if (filter.timestampFrom) query = query.where("timestampUtcMs", ">=", new Date(filter.timestampFrom));
|
|
2583
|
+
if (filter.timestampTo) query = query.where("timestampUtcMs", "<=", new Date(filter.timestampTo));
|
|
2584
|
+
if (filter.sinceRevision !== void 0) query = query.where("index", ">=", filter.sinceRevision);
|
|
2585
|
+
}
|
|
2586
|
+
if (paging) {
|
|
2587
|
+
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
2588
|
+
if (cursorValue > 0) query = query.where("index", ">", cursorValue);
|
|
2589
|
+
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
2590
|
+
}
|
|
2591
|
+
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
2592
|
+
cursor,
|
|
2593
|
+
limit
|
|
2594
|
+
}, signal));
|
|
2595
|
+
}
|
|
2596
|
+
async getSinceId(id, paging, signal) {
|
|
2597
|
+
throwIfAborted(signal);
|
|
2598
|
+
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
|
|
2599
|
+
if (paging) {
|
|
2600
|
+
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
2601
|
+
if (cursorValue > 0) query = query.where("id", ">", cursorValue);
|
|
2602
|
+
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
2603
|
+
}
|
|
2604
|
+
return paginateRows(await query.execute(), paging, (row) => row.id, (row) => this.rowToOperationWithContext(row), (cursor, limit) => this.getSinceId(id, {
|
|
2605
|
+
cursor,
|
|
2606
|
+
limit
|
|
2607
|
+
}, signal));
|
|
2608
|
+
}
|
|
2609
|
+
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
2610
|
+
throwIfAborted(signal);
|
|
2611
|
+
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
|
|
2612
|
+
if (paging) {
|
|
2613
|
+
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
2614
|
+
if (cursorValue > 0) query = query.where("index", ">", cursorValue);
|
|
2615
|
+
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
2616
|
+
}
|
|
2617
|
+
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
2618
|
+
cursor,
|
|
2619
|
+
limit
|
|
2620
|
+
}, signal));
|
|
2621
|
+
}
|
|
2622
|
+
async getRevisions(documentId, branch, signal) {
|
|
2623
|
+
throwIfAborted(signal);
|
|
2624
|
+
const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select([
|
|
2625
|
+
"o1.scope",
|
|
2626
|
+
"o1.index",
|
|
2627
|
+
"o1.timestampUtcMs"
|
|
2628
|
+
]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
|
|
2629
|
+
const revision = {};
|
|
2630
|
+
let latestTimestamp = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2631
|
+
for (const row of scopeRevisions) {
|
|
2632
|
+
revision[row.scope] = row.index + 1;
|
|
2633
|
+
const timestamp = row.timestampUtcMs.toISOString();
|
|
2634
|
+
if (timestamp > latestTimestamp) latestTimestamp = timestamp;
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
revision,
|
|
2638
|
+
latestTimestamp
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
rowToOperation(row) {
|
|
2642
|
+
return {
|
|
2643
|
+
index: row.index,
|
|
2644
|
+
timestampUtcMs: row.timestampUtcMs.toISOString(),
|
|
2645
|
+
hash: row.hash,
|
|
2646
|
+
skip: row.skip,
|
|
2647
|
+
error: row.error || void 0,
|
|
2648
|
+
id: row.opId,
|
|
2649
|
+
action: row.action
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
rowToOperationWithContext(row) {
|
|
2653
|
+
return {
|
|
2654
|
+
operation: this.rowToOperation(row),
|
|
2655
|
+
context: {
|
|
2656
|
+
documentId: row.documentId,
|
|
2657
|
+
documentType: row.documentType,
|
|
2658
|
+
scope: row.scope,
|
|
2659
|
+
branch: row.branch,
|
|
2660
|
+
ordinal: row.id
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
//#endregion
|
|
2666
|
+
//#region src/storage/pool-instrumentation.ts
|
|
2667
|
+
/**
|
|
2668
|
+
* Wraps an existing pg.Pool with acquire-wait timing and an event
|
|
2669
|
+
* subscription surface. The pool is mutated in place: pool.connect()
|
|
2670
|
+
* is replaced with a timing wrapper so all callers (Kysely included)
|
|
2671
|
+
* pick up the instrumentation transparently.
|
|
2672
|
+
*/
|
|
2673
|
+
function instrumentPgPool(pool, name) {
|
|
2674
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2675
|
+
const originalConnect = pool.connect.bind(pool);
|
|
2676
|
+
const wrappedConnect = async () => {
|
|
2677
|
+
const start = performance.now();
|
|
2678
|
+
const client = await originalConnect();
|
|
2679
|
+
const durationMs = performance.now() - start;
|
|
2680
|
+
for (const listener of listeners) try {
|
|
2681
|
+
listener(durationMs);
|
|
2682
|
+
} catch {}
|
|
2683
|
+
return client;
|
|
2684
|
+
};
|
|
2685
|
+
pool.connect = wrappedConnect;
|
|
2686
|
+
return {
|
|
2687
|
+
name,
|
|
2688
|
+
getStats() {
|
|
2689
|
+
return {
|
|
2690
|
+
size: pool.totalCount,
|
|
2691
|
+
idle: pool.idleCount,
|
|
2692
|
+
waiting: pool.waitingCount
|
|
2693
|
+
};
|
|
2694
|
+
},
|
|
2695
|
+
onAcquire(listener) {
|
|
2696
|
+
listeners.add(listener);
|
|
2697
|
+
return () => {
|
|
2698
|
+
listeners.delete(listener);
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function createForwardingPoolInstrumentation(name) {
|
|
2704
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2705
|
+
let stats = {
|
|
2706
|
+
size: 0,
|
|
2707
|
+
idle: 0,
|
|
2708
|
+
waiting: 0
|
|
2709
|
+
};
|
|
2710
|
+
return {
|
|
2711
|
+
name,
|
|
2712
|
+
getStats() {
|
|
2713
|
+
return stats;
|
|
2714
|
+
},
|
|
2715
|
+
onAcquire(listener) {
|
|
2716
|
+
listeners.add(listener);
|
|
2717
|
+
return () => {
|
|
2718
|
+
listeners.delete(listener);
|
|
2719
|
+
};
|
|
2720
|
+
},
|
|
2721
|
+
pushSamples(durations) {
|
|
2722
|
+
for (const durationMs of durations) for (const listener of listeners) try {
|
|
2723
|
+
listener(durationMs);
|
|
2724
|
+
} catch {}
|
|
2725
|
+
},
|
|
2726
|
+
updateStats(next) {
|
|
2727
|
+
stats = next;
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/storage/migrations/001_create_operation_table.ts
|
|
2733
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
2734
|
+
async function up$13(db) {
|
|
2735
|
+
await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
|
|
2736
|
+
"documentId",
|
|
2737
|
+
"scope",
|
|
2738
|
+
"branch",
|
|
2739
|
+
"index"
|
|
2740
|
+
]).addUniqueConstraint("unique_operation_instance", [
|
|
2741
|
+
"opId",
|
|
2742
|
+
"index",
|
|
2743
|
+
"skip"
|
|
2744
|
+
]).execute();
|
|
2745
|
+
await db.schema.createIndex("streamOperations").on("Operation").columns([
|
|
2746
|
+
"documentId",
|
|
2747
|
+
"scope",
|
|
2748
|
+
"branch",
|
|
2749
|
+
"id"
|
|
2750
|
+
]).execute();
|
|
2751
|
+
await db.schema.createIndex("branchlessStreamOperations").on("Operation").columns([
|
|
2752
|
+
"documentId",
|
|
2753
|
+
"scope",
|
|
2754
|
+
"id"
|
|
2755
|
+
]).execute();
|
|
2756
|
+
}
|
|
2757
|
+
//#endregion
|
|
2758
|
+
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
2759
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
2760
|
+
async function up$12(db) {
|
|
2761
|
+
await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
|
|
2762
|
+
"documentId",
|
|
2763
|
+
"scope",
|
|
2764
|
+
"branch",
|
|
2765
|
+
"revision"
|
|
2766
|
+
]).execute();
|
|
2767
|
+
await db.schema.createIndex("keyframe_lookup").on("Keyframe").columns([
|
|
2768
|
+
"documentId",
|
|
2769
|
+
"scope",
|
|
2770
|
+
"branch",
|
|
2771
|
+
"revision"
|
|
2772
|
+
]).execute();
|
|
2773
|
+
}
|
|
2774
|
+
//#endregion
|
|
2775
|
+
//#region src/storage/migrations/003_create_document_table.ts
|
|
2776
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
|
|
2777
|
+
async function up$11(db) {
|
|
2778
|
+
await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2781
|
+
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
2782
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
|
|
2783
|
+
async function up$10(db) {
|
|
2784
|
+
await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
|
|
2785
|
+
"sourceId",
|
|
2786
|
+
"targetId",
|
|
2787
|
+
"relationshipType"
|
|
2788
|
+
]).execute();
|
|
2789
|
+
await db.schema.createIndex("idx_relationship_source").on("DocumentRelationship").column("sourceId").execute();
|
|
2790
|
+
await db.schema.createIndex("idx_relationship_target").on("DocumentRelationship").column("targetId").execute();
|
|
2791
|
+
await db.schema.createIndex("idx_relationship_type").on("DocumentRelationship").column("relationshipType").execute();
|
|
2792
|
+
}
|
|
2793
|
+
//#endregion
|
|
2794
|
+
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
2795
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
|
|
2796
|
+
async function up$9(db) {
|
|
2797
|
+
await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2798
|
+
}
|
|
2799
|
+
//#endregion
|
|
2800
|
+
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
2801
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
|
|
2802
|
+
async function up$8(db) {
|
|
2803
|
+
await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
|
|
2804
|
+
"documentId",
|
|
2805
|
+
"scope",
|
|
2806
|
+
"branch"
|
|
2807
|
+
]).execute();
|
|
2808
|
+
await db.schema.createIndex("idx_slug_scope_branch").on("DocumentSnapshot").columns([
|
|
2809
|
+
"slug",
|
|
2810
|
+
"scope",
|
|
2811
|
+
"branch"
|
|
2812
|
+
]).execute();
|
|
2813
|
+
await db.schema.createIndex("idx_doctype_scope_branch").on("DocumentSnapshot").columns([
|
|
2814
|
+
"documentType",
|
|
2815
|
+
"scope",
|
|
2816
|
+
"branch"
|
|
2817
|
+
]).execute();
|
|
2818
|
+
await db.schema.createIndex("idx_last_updated").on("DocumentSnapshot").column("lastUpdatedAt").execute();
|
|
2819
|
+
await db.schema.createIndex("idx_is_deleted").on("DocumentSnapshot").column("isDeleted").execute();
|
|
2820
|
+
}
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
2823
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
|
|
2824
|
+
async function up$7(db) {
|
|
2825
|
+
await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
|
|
2826
|
+
"documentId",
|
|
2827
|
+
"scope",
|
|
2828
|
+
"branch"
|
|
2829
|
+
]).execute();
|
|
2830
|
+
await db.schema.createIndex("idx_slug_documentid").on("SlugMapping").column("documentId").execute();
|
|
2831
|
+
}
|
|
2832
|
+
//#endregion
|
|
2833
|
+
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
2834
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
|
|
2835
|
+
async function up$6(db) {
|
|
2836
|
+
await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2837
|
+
}
|
|
2838
|
+
//#endregion
|
|
2839
|
+
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
2840
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
|
|
2841
|
+
async function up$5(db) {
|
|
2842
|
+
await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
|
|
2843
|
+
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
2844
|
+
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
2845
|
+
await db.schema.createTable("operation_index_operations").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("opId", "text", (col) => col.notNull()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("index", "integer", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("hash", "text", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).execute();
|
|
2846
|
+
await db.schema.createIndex("idx_operation_index_operations_document").on("operation_index_operations").columns([
|
|
2847
|
+
"documentId",
|
|
2848
|
+
"branch",
|
|
2849
|
+
"scope"
|
|
2850
|
+
]).execute();
|
|
2851
|
+
await db.schema.createIndex("idx_operation_index_operations_ordinal").on("operation_index_operations").column("ordinal").execute();
|
|
2852
|
+
}
|
|
2853
|
+
//#endregion
|
|
2854
|
+
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
2855
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
|
|
2856
|
+
async function up$4(db) {
|
|
2857
|
+
await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2858
|
+
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
2859
|
+
await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2860
|
+
await db.schema.createIndex("idx_sync_cursors_ordinal").on("sync_cursors").column("cursor_ordinal").execute();
|
|
2861
|
+
}
|
|
2862
|
+
//#endregion
|
|
2863
|
+
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
2864
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
|
|
2865
|
+
async function up$3(db) {
|
|
2866
|
+
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
2867
|
+
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
2868
|
+
await db.schema.dropTable("sync_cursors").execute();
|
|
2869
|
+
await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("cursor_type", "text", (col) => col.notNull().defaultTo("inbox")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addPrimaryKeyConstraint("sync_cursors_pk", ["remote_name", "cursor_type"]).execute();
|
|
2870
|
+
await db.schema.createIndex("idx_sync_cursors_ordinal").on("sync_cursors").column("cursor_ordinal").execute();
|
|
2871
|
+
}
|
|
2872
|
+
//#endregion
|
|
2873
|
+
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
2874
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
|
|
2875
|
+
async function up$2(db) {
|
|
2876
|
+
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
2877
|
+
}
|
|
2878
|
+
//#endregion
|
|
2879
|
+
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
2880
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$1 });
|
|
2881
|
+
async function up$1(db) {
|
|
2882
|
+
await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2883
|
+
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
2884
|
+
}
|
|
2885
|
+
//#endregion
|
|
2886
|
+
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
2887
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up });
|
|
2888
|
+
async function up(db) {
|
|
2889
|
+
await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
2890
|
+
}
|
|
2891
|
+
//#endregion
|
|
2892
|
+
//#region src/storage/migrations/migrator.ts
|
|
2893
|
+
const REACTOR_SCHEMA = "reactor";
|
|
2894
|
+
const migrations = {
|
|
2895
|
+
"001_create_operation_table": _001_create_operation_table_exports,
|
|
2896
|
+
"002_create_keyframe_table": _002_create_keyframe_table_exports,
|
|
2897
|
+
"003_create_document_table": _003_create_document_table_exports,
|
|
2898
|
+
"004_create_document_relationship_table": _004_create_document_relationship_table_exports,
|
|
2899
|
+
"005_create_indexer_state_table": _005_create_indexer_state_table_exports,
|
|
2900
|
+
"006_create_document_snapshot_table": _006_create_document_snapshot_table_exports,
|
|
2901
|
+
"007_create_slug_mapping_table": _007_create_slug_mapping_table_exports,
|
|
2902
|
+
"008_create_view_state_table": _008_create_view_state_table_exports,
|
|
2903
|
+
"009_create_operation_index_tables": _009_create_operation_index_tables_exports,
|
|
2904
|
+
"010_create_sync_tables": _010_create_sync_tables_exports,
|
|
2905
|
+
"011_add_cursor_type_column": _011_add_cursor_type_column_exports,
|
|
2906
|
+
"012_add_source_remote_column": _012_add_source_remote_column_exports,
|
|
2907
|
+
"013_create_sync_dead_letters_table": _013_create_sync_dead_letters_table_exports,
|
|
2908
|
+
"014_create_processor_cursor_table": _014_create_processor_cursor_table_exports
|
|
2909
|
+
};
|
|
2910
|
+
var ProgrammaticMigrationProvider = class {
|
|
2911
|
+
getMigrations() {
|
|
2912
|
+
return Promise.resolve(migrations);
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
async function runMigrations(db, schema = REACTOR_SCHEMA) {
|
|
2916
|
+
try {
|
|
2917
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
return {
|
|
2920
|
+
success: false,
|
|
2921
|
+
migrationsExecuted: [],
|
|
2922
|
+
error: error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create schema")
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
const migrator = new Migrator({
|
|
2926
|
+
db: db.withSchema(schema),
|
|
2927
|
+
provider: new ProgrammaticMigrationProvider(),
|
|
2928
|
+
migrationTableSchema: schema
|
|
2929
|
+
});
|
|
2930
|
+
let error;
|
|
2931
|
+
let results;
|
|
2932
|
+
try {
|
|
2933
|
+
const result = await migrator.migrateToLatest();
|
|
2934
|
+
error = result.error;
|
|
2935
|
+
results = result.results;
|
|
2936
|
+
} catch (e) {
|
|
2937
|
+
error = e;
|
|
2938
|
+
results = [];
|
|
2939
|
+
}
|
|
2940
|
+
const migrationsExecuted = results?.map((result) => result.migrationName) ?? [];
|
|
2941
|
+
if (error) return {
|
|
2942
|
+
success: false,
|
|
2943
|
+
migrationsExecuted,
|
|
2944
|
+
error: error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown migration error")
|
|
2945
|
+
};
|
|
2946
|
+
return {
|
|
2947
|
+
success: true,
|
|
2948
|
+
migrationsExecuted
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
2952
|
+
return await new Migrator({
|
|
2953
|
+
db: db.withSchema(schema),
|
|
2954
|
+
provider: new ProgrammaticMigrationProvider(),
|
|
2955
|
+
migrationTableSchema: schema
|
|
2956
|
+
}).getMigrations();
|
|
2957
|
+
}
|
|
2958
|
+
//#endregion
|
|
2959
|
+
//#region src/core/drive-container-types.ts
|
|
2960
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
2961
|
+
//#endregion
|
|
2962
|
+
export { parsePagingOptions as A, DuplicateManifestError as C, DocumentDeletedError as D, ModuleNotFoundError as E, __exportAll as M, DocumentNotFoundError as O, CollectionMembershipCache as S, InvalidModuleError as T, KyselyWriteCache as _, createForwardingPoolInstrumentation as a, createConsistencyToken as b, DuplicateOperationError as c, KyselyKeyframeStore as d, DocumentModelRegistry as f, EventBus as g, KyselyExecutionScope as h, runMigrations as i, throwIfAborted as j, matchesScope as k, OptimisticLockError as l, driveCollectionId as m, REACTOR_SCHEMA as n, instrumentPgPool as o, SimpleJobExecutor as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, RevisionMismatchError as u, KyselyOperationIndex as v, DuplicateModuleError as w, createEmptyConsistencyToken as x, DocumentMetaCache as y };
|
|
2963
|
+
|
|
2964
|
+
//# sourceMappingURL=drive-container-types-BNpMlgT_.js.map
|