@x12i/memorix-service 3.13.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/host.d.ts +2 -2
- package/dist/analytics/host.d.ts.map +1 -1
- package/dist/analytics/host.js.map +1 -1
- package/dist/analytics/read-row.d.ts +33 -0
- package/dist/analytics/read-row.d.ts.map +1 -0
- package/dist/analytics/read-row.js +76 -0
- package/dist/analytics/read-row.js.map +1 -0
- package/dist/analytics/tracker.d.ts +14 -0
- package/dist/analytics/tracker.d.ts.map +1 -1
- package/dist/analytics/tracker.js +55 -17
- package/dist/analytics/tracker.js.map +1 -1
- package/dist/catalog-bridge.d.ts +4 -4
- package/dist/catalog-bridge.d.ts.map +1 -1
- package/dist/catalog-bridge.js +63 -34
- package/dist/catalog-bridge.js.map +1 -1
- package/dist/metadata-view.d.ts +30 -0
- package/dist/metadata-view.d.ts.map +1 -0
- package/dist/metadata-view.js +67 -0
- package/dist/metadata-view.js.map +1 -0
- package/dist/nexum-analysis/enqueue.d.ts +1 -1
- package/dist/nexum-analysis/enqueue.d.ts.map +1 -1
- package/dist/nexum-analysis/enqueue.js +2 -1
- package/dist/nexum-analysis/enqueue.js.map +1 -1
- package/dist/openapi/openapi.json +115 -11
- package/dist/platform.d.ts +7 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +68 -29
- package/dist/platform.js.map +1 -1
- package/dist/routes/data.d.ts.map +1 -1
- package/dist/routes/data.js +123 -12
- package/dist/routes/data.js.map +1 -1
- package/dist/writes/content-write.d.ts +38 -1
- package/dist/writes/content-write.d.ts.map +1 -1
- package/dist/writes/content-write.js +67 -7
- package/dist/writes/content-write.js.map +1 -1
- package/dist/writes/error-shape.d.ts.map +1 -1
- package/dist/writes/error-shape.js +8 -1
- package/dist/writes/error-shape.js.map +1 -1
- package/dist/writes/queued-write.d.ts +38 -0
- package/dist/writes/queued-write.d.ts.map +1 -0
- package/dist/writes/queued-write.js +288 -0
- package/dist/writes/queued-write.js.map +1 -0
- package/dist/writes/receipt-store.d.ts +52 -2
- package/dist/writes/receipt-store.d.ts.map +1 -1
- package/dist/writes/receipt-store.js +191 -2
- package/dist/writes/receipt-store.js.map +1 -1
- package/dist/writes/worker.d.ts +21 -0
- package/dist/writes/worker.d.ts.map +1 -1
- package/dist/writes/worker.js +81 -1
- package/dist/writes/worker.js.map +1 -1
- package/openapi/openapi.json +115 -11
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queued content write (docs/async-content-write-FR.md).
|
|
3
|
+
*
|
|
4
|
+
* `queue: true` validates a row, stores one receipt in the operational database, and answers 202.
|
|
5
|
+
* The document is written later by the drain worker, in accept order for each storage key.
|
|
6
|
+
*
|
|
7
|
+
* The idempotency identity is storage key + run id + step id. Memorix derives it, keeps it in the
|
|
8
|
+
* receipt's `writeId`, and the existing unique `(orgId, caller, writeId)` index enforces it.
|
|
9
|
+
* "The same row" is the canonical content hash a synchronous write computes over the whole body:
|
|
10
|
+
* `data`, `_metadata`, `_audit` and the rest. The same tuple with any difference is `409 QUEUE_KEY_REUSED`.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { adoptRecordMetadata, canonicalJson, resolveContentInstanceFromDraft, resolveContentInstanceSelector, WriterError, } from "@x12i/memorix-data";
|
|
14
|
+
import { ServiceError } from "../errors.js";
|
|
15
|
+
import { normalizeWritePrecondition } from "../write-precondition.js";
|
|
16
|
+
import { asServiceError, batchError, CONTENT_BATCH_LIMIT, prepareWrite, queuedBody, storageKey, } from "./content-write.js";
|
|
17
|
+
import { newReceiptId, } from "./receipt-store.js";
|
|
18
|
+
/**
|
|
19
|
+
* A new row is not applied for this long. A second accept for the same key may hold a lower accept
|
|
20
|
+
* number and still be on its way to the database. The wait lets it land first.
|
|
21
|
+
*/
|
|
22
|
+
export const QUEUE_SETTLE_MS = 500;
|
|
23
|
+
const MAX_ID_LENGTH = 200;
|
|
24
|
+
/** `queue` is absent, or a boolean. */
|
|
25
|
+
export function readQueueFlag(value) {
|
|
26
|
+
if (value === undefined || value === null || value === false)
|
|
27
|
+
return false;
|
|
28
|
+
if (value === true)
|
|
29
|
+
return true;
|
|
30
|
+
throw new ServiceError("VALIDATION", "queue must be a boolean", 400, { field: "queue", retryable: false });
|
|
31
|
+
}
|
|
32
|
+
function validation(field, message) {
|
|
33
|
+
return new ServiceError("VALIDATION", message, 400, { field, retryable: false });
|
|
34
|
+
}
|
|
35
|
+
/** ACCEPT-03: what a queued write may not carry. */
|
|
36
|
+
function assertQueueShape(body) {
|
|
37
|
+
if (body.writeId !== undefined && body.writeId !== null && body.writeId !== "") {
|
|
38
|
+
throw validation("writeId", "writeId is not used with queue: true. Memorix derives the idempotency key from the storage key, run id and step id.");
|
|
39
|
+
}
|
|
40
|
+
if (body.op !== undefined && body.op !== "upsert") {
|
|
41
|
+
throw validation("op", "a queued write is an upsert. patch, replace, add and remove use the synchronous write.");
|
|
42
|
+
}
|
|
43
|
+
if (body.precondition !== undefined && body.precondition !== null) {
|
|
44
|
+
const form = normalizeWritePrecondition(body.precondition, body.expectedRevision).form;
|
|
45
|
+
if (form !== "none") {
|
|
46
|
+
throw validation("precondition", `a queued write uses precondition none, not ${form}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (body.expectedRevision !== undefined && body.expectedRevision !== null) {
|
|
50
|
+
throw validation("expectedRevision", "a queued write has no caller revision. Memorix assigns it.");
|
|
51
|
+
}
|
|
52
|
+
if (!body.record || typeof body.record !== "object" || Array.isArray(body.record)) {
|
|
53
|
+
throw validation("record", "record required");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readExecutionId(execution, name) {
|
|
57
|
+
const field = `_metadata.execution.${name}`;
|
|
58
|
+
const value = execution[name];
|
|
59
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
60
|
+
throw validation(field, `${field} is required with queue: true`);
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (trimmed.length > MAX_ID_LENGTH) {
|
|
64
|
+
throw validation(field, `${field} must be at most ${MAX_ID_LENGTH} characters`);
|
|
65
|
+
}
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
68
|
+
/** Run id and step id, read after the fold a synchronous write applies to `properties` / `metadata` / `_metadata`. */
|
|
69
|
+
function executionIds(record) {
|
|
70
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data)
|
|
71
|
+
? record.data
|
|
72
|
+
: {};
|
|
73
|
+
const folded = adoptRecordMetadata({ ...record, data });
|
|
74
|
+
const execution = folded._metadata?.execution;
|
|
75
|
+
if (!execution || typeof execution !== "object" || Array.isArray(execution)) {
|
|
76
|
+
throw validation("_metadata.execution", "_metadata.execution.runId and _metadata.execution.stepId are required with queue: true");
|
|
77
|
+
}
|
|
78
|
+
const bag = execution;
|
|
79
|
+
return { runId: readExecutionId(bag, "runId"), stepId: readExecutionId(bag, "stepId") };
|
|
80
|
+
}
|
|
81
|
+
function keyIdOf(objectType, contentType, address) {
|
|
82
|
+
return `${objectType}\u0000${contentType}\u0000${address}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The queue key of one stored-document address, or null when the address cannot be resolved
|
|
86
|
+
* (for example a discriminated content type read without its discriminator).
|
|
87
|
+
*/
|
|
88
|
+
export function queueKeyIdFor(catalog, args) {
|
|
89
|
+
try {
|
|
90
|
+
const address = resolveContentInstanceSelector({
|
|
91
|
+
recordId: args.recordId,
|
|
92
|
+
contentType: args.contentType,
|
|
93
|
+
discriminatorValue: args.discriminatorValue,
|
|
94
|
+
catalog,
|
|
95
|
+
});
|
|
96
|
+
return keyIdOf(args.objectType, args.contentType, address.storageKey);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The record a queued row will write, shaped like a stored read, minus the `revision` it does not have yet.
|
|
104
|
+
*/
|
|
105
|
+
export function pendingRecord(row) {
|
|
106
|
+
const record = structuredClone(row.request.record ?? {});
|
|
107
|
+
delete record.revision;
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
110
|
+
function addMs(iso, ms) {
|
|
111
|
+
return new Date(Date.parse(iso) + ms).toISOString();
|
|
112
|
+
}
|
|
113
|
+
function nowOf(deps) {
|
|
114
|
+
return (deps.now ?? (() => new Date().toISOString()))();
|
|
115
|
+
}
|
|
116
|
+
/** Validate the row and derive its identity. Nothing is stored. */
|
|
117
|
+
function prepareQueued(deps, body, now) {
|
|
118
|
+
assertQueueShape(body);
|
|
119
|
+
const { queue: _queue, ...rest } = body;
|
|
120
|
+
void _queue;
|
|
121
|
+
const prepared = prepareWrite(deps, { ...rest, op: "upsert", precondition: "none" }, now);
|
|
122
|
+
const request = prepared.request;
|
|
123
|
+
const catalog = deps.ports.catalog;
|
|
124
|
+
const objectType = catalog.getObjectType(request.objectType);
|
|
125
|
+
if (!objectType) {
|
|
126
|
+
throw new WriterError("ROUTE_UNKNOWN_OBJECT_TYPE", `Unknown objectType ${request.objectType}`);
|
|
127
|
+
}
|
|
128
|
+
if (!objectType.contentTypes.includes(request.contentType)) {
|
|
129
|
+
throw new WriterError("ROUTE_UNKNOWN_CONTENT_TYPE", `contentType ${request.contentType} not allowed for ${request.objectType}`, { objectType: request.objectType, contentType: request.contentType, allowed: objectType.contentTypes });
|
|
130
|
+
}
|
|
131
|
+
if (catalog.getContentProfile && !catalog.getContentProfile(request.contentType)) {
|
|
132
|
+
throw new WriterError("METADATA_DESCRIPTOR_MISSING", `content type ${request.contentType} on ${request.objectType} has no descriptor; ensure the pack at startup before writing`, { objectType: request.objectType, contentType: request.contentType });
|
|
133
|
+
}
|
|
134
|
+
const resolved = resolveContentInstanceFromDraft(request.record, catalog);
|
|
135
|
+
const { runId, stepId } = executionIds(body.record);
|
|
136
|
+
const recordId = resolved.address.recordId;
|
|
137
|
+
const key = storageKey(resolved.address.kind === "discriminated"
|
|
138
|
+
? { ...request, discriminatorValue: resolved.address.discriminatorValue }
|
|
139
|
+
: request, recordId);
|
|
140
|
+
const keyId = keyIdOf(request.objectType, request.contentType, resolved.address.storageKey);
|
|
141
|
+
const writeId = `queue:${createHash("sha256").update(canonicalJson({ keyId, runId, stepId })).digest("hex")}`;
|
|
142
|
+
return { prepared, identity: { keyId, runId, stepId, storageKey: key, writeId } };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Accept one row. 202 `queued`, or the answer for a repeat, or a rejection that stored nothing.
|
|
146
|
+
*/
|
|
147
|
+
export async function runQueuedWrite(deps, body) {
|
|
148
|
+
const now = nowOf(deps);
|
|
149
|
+
let prepared;
|
|
150
|
+
let identity;
|
|
151
|
+
try {
|
|
152
|
+
({ prepared, identity } = prepareQueued(deps, body, now));
|
|
153
|
+
await deps.checkWrite?.(prepared.request);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
return { kind: "rejected", error: asServiceError(err) };
|
|
157
|
+
}
|
|
158
|
+
const seq = await deps.receipts.reserveSeq(deps.scope.orgId, 1);
|
|
159
|
+
const row = {
|
|
160
|
+
receiptId: newReceiptId(),
|
|
161
|
+
orgId: deps.scope.orgId,
|
|
162
|
+
caller: deps.caller,
|
|
163
|
+
writeId: identity.writeId,
|
|
164
|
+
contentHash: prepared.contentHash,
|
|
165
|
+
status: "queued",
|
|
166
|
+
request: prepared.request,
|
|
167
|
+
queue: {
|
|
168
|
+
seq,
|
|
169
|
+
keyId: identity.keyId,
|
|
170
|
+
runId: identity.runId,
|
|
171
|
+
stepId: identity.stepId,
|
|
172
|
+
storageKey: identity.storageKey,
|
|
173
|
+
},
|
|
174
|
+
attempts: 0,
|
|
175
|
+
nextAttemptAt: addMs(now, QUEUE_SETTLE_MS),
|
|
176
|
+
createdAt: now,
|
|
177
|
+
updatedAt: now,
|
|
178
|
+
};
|
|
179
|
+
const inserted = await deps.receipts.insert(row);
|
|
180
|
+
if (inserted === "inserted")
|
|
181
|
+
return { kind: "queued", httpStatus: 202, body: queuedBody(row) };
|
|
182
|
+
const existing = await deps.receipts.findByWriteId(deps.scope.orgId, deps.caller, identity.writeId);
|
|
183
|
+
if (existing)
|
|
184
|
+
return answerRepeat(existing, prepared.contentHash);
|
|
185
|
+
return {
|
|
186
|
+
kind: "rejected",
|
|
187
|
+
error: new ServiceError("WRITE_UNAVAILABLE", "the write queue could not record this row; send it again", 503, {
|
|
188
|
+
retryable: true,
|
|
189
|
+
retryAfterMs: 1_000,
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** The same tuple again: the original receipt, or 409 when the row differs. */
|
|
194
|
+
function answerRepeat(existing, contentHash) {
|
|
195
|
+
if (existing.contentHash !== contentHash) {
|
|
196
|
+
return {
|
|
197
|
+
kind: "rejected",
|
|
198
|
+
error: new ServiceError("QUEUE_KEY_REUSED", "this storage key, run id and step id was already accepted with different content. Send a correction under a new step id.", 409, { field: "stepId", retryable: false, receiptId: existing.receiptId }),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (existing.status === "stored") {
|
|
202
|
+
return { kind: "stored", httpStatus: 200, body: publicQueuedReceipt(existing) };
|
|
203
|
+
}
|
|
204
|
+
if (existing.status === "queued" || existing.status === "pending") {
|
|
205
|
+
return { kind: "queued", httpStatus: 202, body: queuedBody(existing) };
|
|
206
|
+
}
|
|
207
|
+
const error = existing.error;
|
|
208
|
+
return {
|
|
209
|
+
kind: "rejected",
|
|
210
|
+
error: new ServiceError(error?.code ?? "VALIDATION", error?.message ?? "write was rejected", error?.status ?? 400, {
|
|
211
|
+
field: error?.field,
|
|
212
|
+
retryable: false,
|
|
213
|
+
receiptId: existing.receiptId,
|
|
214
|
+
}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Accept up to 500 rows in array order. Each row is stored and acknowledged before its result is recorded.
|
|
219
|
+
* One bad row does not reject the batch or drop rows already stored.
|
|
220
|
+
*/
|
|
221
|
+
export async function runQueuedBatch(deps, body, datasourceFor) {
|
|
222
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.writes)) {
|
|
223
|
+
throw new ServiceError("VALIDATION", "writes must be an array", 400, { field: "writes", retryable: false });
|
|
224
|
+
}
|
|
225
|
+
const writes = body.writes;
|
|
226
|
+
if (writes.length > CONTENT_BATCH_LIMIT) {
|
|
227
|
+
throw new ServiceError("VALIDATION", `writes accepts at most ${CONTENT_BATCH_LIMIT} rows`, 400, {
|
|
228
|
+
field: "writes",
|
|
229
|
+
retryable: false,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const results = [];
|
|
233
|
+
for (const item of writes) {
|
|
234
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
235
|
+
results.push({
|
|
236
|
+
error: { code: "VALIDATION", field: "writes", message: "each write must be an object", retryable: false },
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const row = item;
|
|
241
|
+
if (row.queue !== undefined) {
|
|
242
|
+
results.push({
|
|
243
|
+
error: {
|
|
244
|
+
code: "VALIDATION",
|
|
245
|
+
field: "queue",
|
|
246
|
+
message: "queue is set on the batch body, not on a row",
|
|
247
|
+
retryable: false,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const datasource = datasourceFor(row);
|
|
254
|
+
const outcome = await runQueuedWrite({ ...deps, datasource }, row);
|
|
255
|
+
if (outcome.kind === "rejected")
|
|
256
|
+
results.push(batchError(outcome.error));
|
|
257
|
+
else
|
|
258
|
+
results.push(outcome.body);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
results.push(batchError(err));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { results };
|
|
265
|
+
}
|
|
266
|
+
/** What `GET /api/data/receipts/:id`, the run list, and a repeat of a stored row return for a queued write. */
|
|
267
|
+
export function publicQueuedReceipt(row) {
|
|
268
|
+
const queue = row.queue;
|
|
269
|
+
return {
|
|
270
|
+
receiptId: row.receiptId,
|
|
271
|
+
status: row.status,
|
|
272
|
+
...(queue ? { storageKey: queue.storageKey, runId: queue.runId, stepId: queue.stepId } : {}),
|
|
273
|
+
createdAt: row.createdAt,
|
|
274
|
+
updatedAt: row.updatedAt,
|
|
275
|
+
...(row.status === "stored" && row.receipt ? { receipt: row.receipt } : {}),
|
|
276
|
+
...(row.status === "rejected" && row.error
|
|
277
|
+
? {
|
|
278
|
+
error: {
|
|
279
|
+
code: row.error.code,
|
|
280
|
+
field: row.error.field,
|
|
281
|
+
message: row.error.message,
|
|
282
|
+
retryable: row.error.retryable,
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
: {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=queued-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queued-write.js","sourceRoot":"","sources":["../../src/writes/queued-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Db } from "mongodb";
|
|
2
2
|
export declare const WRITE_RECEIPTS_COLLECTION = "write-receipts";
|
|
3
|
+
export declare const RECEIPT_SEQ_COLLECTION = "write-receipt-seq";
|
|
3
4
|
export declare const REJECTED_RECEIPT_TTL_MS: number;
|
|
4
5
|
export declare const WRITE_ATTEMPT_LEASE_MS = 30000;
|
|
5
6
|
export type WriteOperation = "created" | "updated" | "removed" | "unchanged";
|
|
@@ -44,16 +45,39 @@ export type ReceiptError = {
|
|
|
44
45
|
retryAfterMs?: number;
|
|
45
46
|
status: number;
|
|
46
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* What a queued row adds to its receipt. A synchronous receipt has none of this.
|
|
50
|
+
*/
|
|
51
|
+
export type QueuedWriteInfo = {
|
|
52
|
+
/** Accept number, monotonic per org. Rows for one key apply in this order. */
|
|
53
|
+
seq: number;
|
|
54
|
+
/** The storage key the row lands on. Rows with the same `keyId` apply one at a time. */
|
|
55
|
+
keyId: string;
|
|
56
|
+
runId: string;
|
|
57
|
+
stepId: string;
|
|
58
|
+
storageKey: WriteReceipt["storageKey"];
|
|
59
|
+
};
|
|
60
|
+
export type RunReceiptStatus = "queued" | "stored" | "rejected";
|
|
61
|
+
export type RunSummary = {
|
|
62
|
+
queued: number;
|
|
63
|
+
stored: number;
|
|
64
|
+
rejected: number;
|
|
65
|
+
};
|
|
47
66
|
export type WriteReceiptRow = {
|
|
48
67
|
receiptId: string;
|
|
49
68
|
orgId: string;
|
|
50
69
|
caller: string;
|
|
51
70
|
writeId?: string;
|
|
52
71
|
contentHash: string;
|
|
53
|
-
|
|
72
|
+
/**
|
|
73
|
+
* `pending` is a synchronous write that failed in a way a retry can fix.
|
|
74
|
+
* `queued` is a row that was accepted with `queue: true` and is not applied yet.
|
|
75
|
+
*/
|
|
76
|
+
status: "pending" | "queued" | "stored" | "rejected";
|
|
54
77
|
receipt?: WriteReceipt;
|
|
55
78
|
error?: ReceiptError;
|
|
56
79
|
request: StoredContentWrite;
|
|
80
|
+
queue?: QueuedWriteInfo;
|
|
57
81
|
attempts: number;
|
|
58
82
|
nextAttemptAt: string;
|
|
59
83
|
createdAt: string;
|
|
@@ -69,10 +93,36 @@ export type WriteReceiptStore = {
|
|
|
69
93
|
list(args: {
|
|
70
94
|
orgId: string;
|
|
71
95
|
caller: string;
|
|
72
|
-
status?: "pending" | "stored" | "rejected";
|
|
96
|
+
status?: "pending" | "queued" | "stored" | "rejected";
|
|
73
97
|
limit: number;
|
|
74
98
|
}): Promise<WriteReceiptRow[]>;
|
|
75
99
|
claimDue(orgId: string, now: string, limit: number, leaseMs: number): Promise<WriteReceiptRow[]>;
|
|
100
|
+
/** The first of `count` consecutive accept numbers for this org. */
|
|
101
|
+
reserveSeq(orgId: string, count: number): Promise<number>;
|
|
102
|
+
/**
|
|
103
|
+
* Queued rows that are due and have no earlier open row on the same key.
|
|
104
|
+
* Each returned row is leased: no other claim returns it until the lease ends.
|
|
105
|
+
*/
|
|
106
|
+
claimQueued(orgId: string, now: string, limit: number, leaseMs: number): Promise<WriteReceiptRow[]>;
|
|
107
|
+
/** Queued-write receipts of one run for one caller, in accept order, after the cursor. */
|
|
108
|
+
listByRun(args: {
|
|
109
|
+
orgId: string;
|
|
110
|
+
caller: string;
|
|
111
|
+
runId: string;
|
|
112
|
+
stepId?: string;
|
|
113
|
+
status?: RunReceiptStatus;
|
|
114
|
+
/** Return rows whose accept number is greater than this. */
|
|
115
|
+
after?: number;
|
|
116
|
+
limit: number;
|
|
117
|
+
}): Promise<WriteReceiptRow[]>;
|
|
118
|
+
summarizeRun(args: {
|
|
119
|
+
orgId: string;
|
|
120
|
+
caller: string;
|
|
121
|
+
runId: string;
|
|
122
|
+
stepId?: string;
|
|
123
|
+
}): Promise<RunSummary>;
|
|
124
|
+
/** The newest accepted row for a key that has not been applied. */
|
|
125
|
+
latestQueuedForKey(orgId: string, keyId: string): Promise<WriteReceiptRow | null>;
|
|
76
126
|
};
|
|
77
127
|
export declare function newReceiptId(): string;
|
|
78
128
|
export declare function rejectedExpiry(nowIso: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receipt-store.d.ts","sourceRoot":"","sources":["../../src/writes/receipt-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,eAAO,MAAM,yBAAyB,mBAAmB,CAAC;AAC1D,eAAO,MAAM,uBAAuB,QAA0B,CAAC;AAC/D,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7E,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,cAAc,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,QAAQ,GAAG,gBAAgB,GAAG,MAAM,CAAC;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"receipt-store.d.ts","sourceRoot":"","sources":["../../src/writes/receipt-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
export const WRITE_RECEIPTS_COLLECTION = "write-receipts";
|
|
6
|
+
export const RECEIPT_SEQ_COLLECTION = "write-receipt-seq";
|
|
6
7
|
export const REJECTED_RECEIPT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7
8
|
export const WRITE_ATTEMPT_LEASE_MS = 30_000;
|
|
8
9
|
export function newReceiptId() {
|
|
@@ -11,12 +12,17 @@ export function newReceiptId() {
|
|
|
11
12
|
export function rejectedExpiry(nowIso) {
|
|
12
13
|
return new Date(Date.parse(nowIso) + REJECTED_RECEIPT_TTL_MS).toISOString();
|
|
13
14
|
}
|
|
15
|
+
function matchesRun(row, args) {
|
|
16
|
+
return (row.caller === args.caller &&
|
|
17
|
+
row.queue?.runId === args.runId &&
|
|
18
|
+
(args.stepId === undefined || row.queue.stepId === args.stepId));
|
|
19
|
+
}
|
|
14
20
|
export function createMemoryWriteReceiptStore() {
|
|
15
21
|
const buckets = new Map();
|
|
16
22
|
function bucket(orgId) {
|
|
17
23
|
let found = buckets.get(orgId);
|
|
18
24
|
if (!found) {
|
|
19
|
-
found = { byId: new Map(), byWriteId: new Map() };
|
|
25
|
+
found = { byId: new Map(), byWriteId: new Map(), seq: 0 };
|
|
20
26
|
buckets.set(orgId, found);
|
|
21
27
|
}
|
|
22
28
|
return found;
|
|
@@ -78,6 +84,67 @@ export function createMemoryWriteReceiptStore() {
|
|
|
78
84
|
}
|
|
79
85
|
return claimed;
|
|
80
86
|
},
|
|
87
|
+
async reserveSeq(orgId, count) {
|
|
88
|
+
const box = bucket(orgId);
|
|
89
|
+
const first = box.seq + 1;
|
|
90
|
+
box.seq += count;
|
|
91
|
+
return first;
|
|
92
|
+
},
|
|
93
|
+
async claimQueued(orgId, now, limit, leaseMs) {
|
|
94
|
+
const until = new Date(Date.parse(now) + leaseMs).toISOString();
|
|
95
|
+
const open = [...bucket(orgId).byId.values()].filter((row) => row.status === "queued" && row.queue);
|
|
96
|
+
const earliestOpen = new Map();
|
|
97
|
+
for (const row of open) {
|
|
98
|
+
const seen = earliestOpen.get(row.queue.keyId);
|
|
99
|
+
if (seen === undefined || row.queue.seq < seen)
|
|
100
|
+
earliestOpen.set(row.queue.keyId, row.queue.seq);
|
|
101
|
+
}
|
|
102
|
+
const claimed = [];
|
|
103
|
+
const due = open
|
|
104
|
+
.filter((row) => row.nextAttemptAt <= now)
|
|
105
|
+
.sort((a, b) => a.queue.seq - b.queue.seq);
|
|
106
|
+
for (const row of due) {
|
|
107
|
+
if (claimed.length >= limit)
|
|
108
|
+
break;
|
|
109
|
+
if (earliestOpen.get(row.queue.keyId) !== row.queue.seq)
|
|
110
|
+
continue;
|
|
111
|
+
row.nextAttemptAt = until;
|
|
112
|
+
row.attempts += 1;
|
|
113
|
+
row.updatedAt = now;
|
|
114
|
+
claimed.push(structuredClone(row));
|
|
115
|
+
}
|
|
116
|
+
return claimed;
|
|
117
|
+
},
|
|
118
|
+
async listByRun(args) {
|
|
119
|
+
return [...bucket(args.orgId).byId.values()]
|
|
120
|
+
.filter((row) => matchesRun(row, args))
|
|
121
|
+
.filter((row) => (args.status ? row.status === args.status : true))
|
|
122
|
+
.filter((row) => (args.after === undefined ? true : row.queue.seq > args.after))
|
|
123
|
+
.sort((a, b) => a.queue.seq - b.queue.seq)
|
|
124
|
+
.slice(0, args.limit)
|
|
125
|
+
.map((row) => structuredClone(row));
|
|
126
|
+
},
|
|
127
|
+
async summarizeRun(args) {
|
|
128
|
+
const summary = { queued: 0, stored: 0, rejected: 0 };
|
|
129
|
+
for (const row of bucket(args.orgId).byId.values()) {
|
|
130
|
+
if (!matchesRun(row, args))
|
|
131
|
+
continue;
|
|
132
|
+
if (row.status === "queued" || row.status === "stored" || row.status === "rejected") {
|
|
133
|
+
summary[row.status] += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return summary;
|
|
137
|
+
},
|
|
138
|
+
async latestQueuedForKey(orgId, keyId) {
|
|
139
|
+
let latest = null;
|
|
140
|
+
for (const row of bucket(orgId).byId.values()) {
|
|
141
|
+
if (row.status !== "queued" || row.queue?.keyId !== keyId)
|
|
142
|
+
continue;
|
|
143
|
+
if (!latest || row.queue.seq > latest.queue.seq)
|
|
144
|
+
latest = row;
|
|
145
|
+
}
|
|
146
|
+
return latest ? structuredClone(latest) : null;
|
|
147
|
+
},
|
|
81
148
|
};
|
|
82
149
|
}
|
|
83
150
|
export function createMongoWriteReceiptStore(db) {
|
|
@@ -96,6 +163,29 @@ export function createMongoWriteReceiptStore(db) {
|
|
|
96
163
|
]).then(() => undefined);
|
|
97
164
|
return ready;
|
|
98
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Indexes for queued writes only. They are separate from `ensure()` so a synchronous write never
|
|
168
|
+
* waits for them: on a large `write-receipts` collection the first build scans every row once.
|
|
169
|
+
* Partial filters keep synchronous receipts out of these indexes, and each query on one of them
|
|
170
|
+
* states the field its filter names, so the planner can use it.
|
|
171
|
+
* A failed build is tried again on the next queue call.
|
|
172
|
+
*/
|
|
173
|
+
let queueReady = null;
|
|
174
|
+
function ensureQueue() {
|
|
175
|
+
queueReady ??= Promise.all([
|
|
176
|
+
ensure(),
|
|
177
|
+
coll.createIndex({ orgId: 1, status: 1, "queue.seq": 1 }, { name: "queue_seq", partialFilterExpression: { "queue.seq": { $exists: true } } }),
|
|
178
|
+
coll.createIndex({ orgId: 1, "queue.keyId": 1, status: 1, "queue.seq": 1 }, { name: "queue_key", partialFilterExpression: { "queue.keyId": { $exists: true } } }),
|
|
179
|
+
coll.createIndex({ orgId: 1, caller: 1, "queue.runId": 1, status: 1, "queue.seq": 1 }, { name: "queue_run_status", partialFilterExpression: { "queue.runId": { $exists: true } } }),
|
|
180
|
+
coll.createIndex({ orgId: 1, caller: 1, "queue.runId": 1, "queue.seq": 1 }, { name: "queue_run", partialFilterExpression: { "queue.runId": { $exists: true } } }),
|
|
181
|
+
]).then(() => undefined, (err) => {
|
|
182
|
+
queueReady = null;
|
|
183
|
+
throw err;
|
|
184
|
+
});
|
|
185
|
+
return queueReady;
|
|
186
|
+
}
|
|
187
|
+
/** One counter document per org. `$inc` is atomic, so two processes never share a number. */
|
|
188
|
+
const counters = db.collection(RECEIPT_SEQ_COLLECTION);
|
|
99
189
|
return {
|
|
100
190
|
async findByWriteId(orgId, caller, writeId) {
|
|
101
191
|
await ensure();
|
|
@@ -108,7 +198,8 @@ export function createMongoWriteReceiptStore(db) {
|
|
|
108
198
|
return doc ? fromDoc(doc) : null;
|
|
109
199
|
},
|
|
110
200
|
async insert(row) {
|
|
111
|
-
|
|
201
|
+
// A synchronous receipt never waits for the queue indexes.
|
|
202
|
+
await (row.queue ? ensureQueue() : ensure());
|
|
112
203
|
try {
|
|
113
204
|
await coll.insertOne(toDoc(row));
|
|
114
205
|
return "inserted";
|
|
@@ -154,6 +245,104 @@ export function createMongoWriteReceiptStore(db) {
|
|
|
154
245
|
}
|
|
155
246
|
return claimed;
|
|
156
247
|
},
|
|
248
|
+
async reserveSeq(orgId, count) {
|
|
249
|
+
const after = await counters.findOneAndUpdate({ _id: orgId }, { $inc: { seq: count } }, { upsert: true, returnDocument: "after" });
|
|
250
|
+
if (!after)
|
|
251
|
+
throw new Error("receipt sequence counter did not return a document");
|
|
252
|
+
return after.seq - count + 1;
|
|
253
|
+
},
|
|
254
|
+
async claimQueued(orgId, now, limit, leaseMs) {
|
|
255
|
+
await ensureQueue();
|
|
256
|
+
const until = new Date(Date.parse(now) + leaseMs).toISOString();
|
|
257
|
+
const claimed = [];
|
|
258
|
+
const taken = new Set();
|
|
259
|
+
const pageSize = Math.max(limit * 4, 16);
|
|
260
|
+
// Rows waiting behind an earlier row on the same key fill the front of the order.
|
|
261
|
+
// Keep paging past them, up to a cap, so other keys are not starved.
|
|
262
|
+
const SCAN_CAP = 4_000;
|
|
263
|
+
let scanned = 0;
|
|
264
|
+
let after = 0;
|
|
265
|
+
while (claimed.length < limit && scanned < SCAN_CAP) {
|
|
266
|
+
// Only what the claim needs. The full row, with its request body, is read once it is claimed.
|
|
267
|
+
const page = await coll
|
|
268
|
+
.find({ orgId, status: "queued", "queue.seq": { $gt: after }, nextAttemptAt: { $lte: now } }, { projection: { _id: 1, "queue.seq": 1, "queue.keyId": 1, nextAttemptAt: 1 } })
|
|
269
|
+
.sort({ "queue.seq": 1 })
|
|
270
|
+
.limit(pageSize)
|
|
271
|
+
.toArray();
|
|
272
|
+
if (page.length === 0)
|
|
273
|
+
break;
|
|
274
|
+
for (const doc of page) {
|
|
275
|
+
scanned += 1;
|
|
276
|
+
after = doc.queue.seq;
|
|
277
|
+
if (claimed.length >= limit)
|
|
278
|
+
break;
|
|
279
|
+
const keyId = doc.queue.keyId;
|
|
280
|
+
if (taken.has(keyId))
|
|
281
|
+
continue;
|
|
282
|
+
const ahead = await coll.findOne({
|
|
283
|
+
orgId,
|
|
284
|
+
status: "queued",
|
|
285
|
+
"queue.keyId": keyId,
|
|
286
|
+
"queue.seq": { $lt: doc.queue.seq },
|
|
287
|
+
}, { projection: { _id: 1 } });
|
|
288
|
+
if (ahead)
|
|
289
|
+
continue;
|
|
290
|
+
const updated = await coll.updateOne({ _id: doc._id, status: "queued", nextAttemptAt: doc.nextAttemptAt }, { $set: { nextAttemptAt: until, updatedAt: now }, $inc: { attempts: 1 } });
|
|
291
|
+
if (updated.modifiedCount !== 1)
|
|
292
|
+
continue;
|
|
293
|
+
taken.add(keyId);
|
|
294
|
+
const fresh = await coll.findOne({ _id: doc._id });
|
|
295
|
+
if (fresh)
|
|
296
|
+
claimed.push(fromDoc(fresh));
|
|
297
|
+
}
|
|
298
|
+
if (page.length < pageSize)
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
return claimed;
|
|
302
|
+
},
|
|
303
|
+
async listByRun(args) {
|
|
304
|
+
await ensureQueue();
|
|
305
|
+
const filter = {
|
|
306
|
+
orgId: args.orgId,
|
|
307
|
+
caller: args.caller,
|
|
308
|
+
"queue.runId": args.runId,
|
|
309
|
+
"queue.seq": { $gt: args.after ?? 0 },
|
|
310
|
+
};
|
|
311
|
+
if (args.stepId !== undefined)
|
|
312
|
+
filter["queue.stepId"] = args.stepId;
|
|
313
|
+
if (args.status)
|
|
314
|
+
filter.status = args.status;
|
|
315
|
+
const docs = await coll.find(filter).sort({ "queue.seq": 1 }).limit(args.limit).toArray();
|
|
316
|
+
return docs.map(fromDoc);
|
|
317
|
+
},
|
|
318
|
+
async summarizeRun(args) {
|
|
319
|
+
await ensureQueue();
|
|
320
|
+
const match = {
|
|
321
|
+
orgId: args.orgId,
|
|
322
|
+
caller: args.caller,
|
|
323
|
+
"queue.runId": args.runId,
|
|
324
|
+
};
|
|
325
|
+
if (args.stepId !== undefined)
|
|
326
|
+
match["queue.stepId"] = args.stepId;
|
|
327
|
+
const groups = await coll
|
|
328
|
+
.aggregate([
|
|
329
|
+
{ $match: match },
|
|
330
|
+
{ $group: { _id: "$status", n: { $sum: 1 } } },
|
|
331
|
+
])
|
|
332
|
+
.toArray();
|
|
333
|
+
const summary = { queued: 0, stored: 0, rejected: 0 };
|
|
334
|
+
for (const group of groups) {
|
|
335
|
+
if (group._id === "queued" || group._id === "stored" || group._id === "rejected") {
|
|
336
|
+
summary[group._id] = group.n;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return summary;
|
|
340
|
+
},
|
|
341
|
+
async latestQueuedForKey(orgId, keyId) {
|
|
342
|
+
await ensureQueue();
|
|
343
|
+
const doc = await coll.findOne({ orgId, status: "queued", "queue.keyId": keyId, "queue.seq": { $gt: 0 } }, { sort: { "queue.seq": -1 } });
|
|
344
|
+
return doc ? fromDoc(doc) : null;
|
|
345
|
+
},
|
|
157
346
|
};
|
|
158
347
|
}
|
|
159
348
|
function toDoc(row) {
|