@yielded/agent-storage-memory 0.1.0-beta.166 → 0.1.0-beta.168
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/MemoryMessageDeliveryStore.d.mts +7 -3
- package/dist/MemoryMessageDeliveryStore.mjs +427 -64
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -1
- package/dist/MemoryScheduleStore.mjs +8 -8
- package/dist/MemoryScheduleStore.mjs.map +1 -1
- package/dist/MemorySemanticIndex.mjs +7 -9
- package/dist/MemorySemanticIndex.mjs.map +1 -1
- package/dist/MemorySubmissionLedger.d.mts +7 -4
- package/dist/MemorySubmissionLedger.mjs +761 -377
- package/dist/MemorySubmissionLedger.mjs.map +1 -1
- package/dist/MemorySubscriptionStore.mjs +23 -23
- package/dist/MemorySubscriptionStore.mjs.map +1 -1
- package/dist/MemoryThreadStore.d.mts +8 -4
- package/dist/MemoryThreadStore.mjs +1325 -277
- package/dist/MemoryThreadStore.mjs.map +1 -1
- package/dist/MemoryThreadStoreKernel-DYNDjlhO.d.mts +85 -0
- package/dist/WorkSelection-CmqTuws3.mjs +31 -0
- package/dist/WorkSelection-CmqTuws3.mjs.map +1 -0
- package/package.json +1 -1
- package/src/MemoryMessageDeliveryStore.ts +766 -230
- package/src/MemoryScheduleStore.ts +190 -200
- package/src/MemorySemanticIndex.ts +94 -95
- package/src/MemorySubmissionLedger.ts +1955 -1245
- package/src/MemorySubscriptionStore.ts +1122 -1117
- package/src/MemoryThreadStore.ts +2609 -628
- package/src/internal/MemoryThreadStoreKernel.ts +156 -0
- package/src/internal/WorkSelection.ts +29 -0
|
@@ -19,9 +19,35 @@ import {
|
|
|
19
19
|
validateMessageDelivery,
|
|
20
20
|
} from "@yielded/agent/message-delivery";
|
|
21
21
|
import { ScheduleInstant } from "@yielded/agent/schedule";
|
|
22
|
-
import {
|
|
22
|
+
import { validateCanonicalSettlement } from "@yielded/agent/settlement-publisher";
|
|
23
|
+
import {
|
|
24
|
+
Settlement,
|
|
25
|
+
settlementFailureFromRecord,
|
|
26
|
+
submissionSettlementRecordId,
|
|
27
|
+
submissionSettlementId,
|
|
28
|
+
} from "@yielded/agent/submission-ledger";
|
|
29
|
+
import { PreparedInput } from "@yielded/agent/subscription";
|
|
30
|
+
import {
|
|
31
|
+
ThreadDeliveryImportReader,
|
|
32
|
+
ThreadImportRejected,
|
|
33
|
+
invalidThreadArchive,
|
|
34
|
+
} from "@yielded/agent/thread-import";
|
|
35
|
+
import { ThreadStoreError, makeTransferFactCheck } from "@yielded/agent/thread-store";
|
|
36
|
+
import { workId, type ThreadWorkEntry } from "@yielded/agent/thread-work";
|
|
37
|
+
import { Effect, Layer, MutableRef, Ref, Schema } from "effect";
|
|
38
|
+
|
|
39
|
+
import { MemoryThreadStoreKernel } from "./internal/MemoryThreadStoreKernel.ts";
|
|
40
|
+
import { boundedWorkSelection } from "./internal/WorkSelection.ts";
|
|
23
41
|
|
|
24
42
|
const codec = Schema.fromJsonString(MessageDeliveryRecord);
|
|
43
|
+
const deliveryFitsTransfer = makeTransferFactCheck(MessageDeliveryRecord);
|
|
44
|
+
|
|
45
|
+
const checkTransfer = (record: MessageDeliveryRecord) =>
|
|
46
|
+
deliveryFitsTransfer(record.key.ownerThreadId, { ...record, leaseUntilMillis: null })
|
|
47
|
+
? Effect.void
|
|
48
|
+
: Effect.fail(
|
|
49
|
+
MessageDeliveryError.make({ reason: "capacity", operation: "transfer-page-bytes" }),
|
|
50
|
+
);
|
|
25
51
|
|
|
26
52
|
const decode = (text: string) =>
|
|
27
53
|
Schema.decodeEffect(codec)(text).pipe(
|
|
@@ -34,19 +60,80 @@ const Scan = Schema.Struct({
|
|
|
34
60
|
ownerThreadId: Schema.optionalKey(ThreadId),
|
|
35
61
|
});
|
|
36
62
|
|
|
63
|
+
const position = (keys: ReadonlyArray<string>, key: string) => {
|
|
64
|
+
let low = 0;
|
|
65
|
+
let high = keys.length;
|
|
66
|
+
|
|
67
|
+
while (low < high) {
|
|
68
|
+
const mid = Math.floor((low + high) / 2);
|
|
69
|
+
|
|
70
|
+
if (keys[mid] < key) low = mid + 1;
|
|
71
|
+
else high = mid;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return low;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const retainKey = (keys: Array<string>, key: string) => {
|
|
78
|
+
const at = position(keys, key);
|
|
79
|
+
|
|
80
|
+
if (keys[at] !== key) keys.splice(at, 0, key);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const removeKey = (keys: Array<string>, key: string) => {
|
|
84
|
+
const at = position(keys, key);
|
|
85
|
+
|
|
86
|
+
if (keys[at] === key) keys.splice(at, 1);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const backend = () => ({
|
|
90
|
+
records: new Map<string, string>(),
|
|
91
|
+
work: new Map<string, ThreadWorkEntry>(),
|
|
92
|
+
ordered: new Map<string, Array<string>>(),
|
|
93
|
+
pending: new Map<ThreadId, Set<MessageDeliveryKey["messageId"]>>(),
|
|
94
|
+
counts: new Map<ThreadId, { retained: number; ordinary: number; update: number; peer: number }>(),
|
|
95
|
+
metadata: new Map<
|
|
96
|
+
string,
|
|
97
|
+
{
|
|
98
|
+
update: boolean;
|
|
99
|
+
pending: boolean;
|
|
100
|
+
peer: boolean;
|
|
101
|
+
deadlineKey?: string;
|
|
102
|
+
importVisit?: "visiting" | "verified";
|
|
103
|
+
}
|
|
104
|
+
>(),
|
|
105
|
+
deadlines: new Map<string, { readonly key: MessageDeliveryKey; readonly deadline: number }>(),
|
|
106
|
+
deadlineKeys: new Array<string>(),
|
|
107
|
+
ownerDeadlineKeys: new Map<ThreadId, Array<string>>(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const ownerKey = (id: ThreadId) => JSON.stringify([id]);
|
|
111
|
+
|
|
112
|
+
const keysFor = (current: ReturnType<typeof backend>, key: string) => {
|
|
113
|
+
let keys = current.ordered.get(key);
|
|
114
|
+
|
|
115
|
+
if (keys === undefined) current.ordered.set(key, (keys = []));
|
|
116
|
+
|
|
117
|
+
return keys;
|
|
118
|
+
};
|
|
119
|
+
|
|
37
120
|
export interface MemoryMessageDeliveryStoreOptions {
|
|
38
121
|
/** UTF-8 bound on the complete stored record, including a processed Settlement. */
|
|
39
122
|
readonly maxStoredValueBytes?: number;
|
|
40
123
|
}
|
|
41
124
|
|
|
42
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Pair with MemoryThreadStoreLive so export/import sees every retained delivery obligation.
|
|
127
|
+
* Each retained value and live partition is bounded; completed deduplication evidence is never evicted.
|
|
128
|
+
*/
|
|
43
129
|
export const memoryMessageDeliveryStoreLayer = (
|
|
44
130
|
limits: MessageDeliveryStoreLimits = defaultMessageDeliveryStoreLimits,
|
|
45
131
|
options: MemoryMessageDeliveryStoreOptions = {},
|
|
46
|
-
): Layer.Layer<MessageDeliveryStore, MessageDeliveryError> =>
|
|
132
|
+
): Layer.Layer<MessageDeliveryStore, MessageDeliveryError, MemoryThreadStoreKernel> =>
|
|
47
133
|
Layer.effect(
|
|
48
134
|
MessageDeliveryStore,
|
|
49
135
|
Effect.gen(function* () {
|
|
136
|
+
const journal = yield* MemoryThreadStoreKernel;
|
|
50
137
|
const config = yield* validateMessageDelivery(MessageDeliveryStoreLimits, limits, "limits");
|
|
51
138
|
|
|
52
139
|
const maxStoredValueBytes = yield* validateMessageDelivery(
|
|
@@ -55,62 +142,187 @@ export const memoryMessageDeliveryStoreLayer = (
|
|
|
55
142
|
"stored-value-limit",
|
|
56
143
|
);
|
|
57
144
|
|
|
58
|
-
const state = yield* Ref.make(
|
|
59
|
-
records: new Map<string, string>(),
|
|
60
|
-
pending: new Map<ThreadId, ReadonlySet<MessageDeliveryKey["messageId"]>>(),
|
|
61
|
-
workers: new Map<string, ReadonlySet<MessageDeliveryKey["messageId"]>>(),
|
|
62
|
-
});
|
|
145
|
+
const state = yield* Ref.make(backend());
|
|
63
146
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
147
|
+
yield* journal.registerDeliveryLookup((threadId, messageId) =>
|
|
148
|
+
MutableRef.get(state.ref).records.has(
|
|
149
|
+
messageDeliveryKeyString({ ownerThreadId: threadId, messageId }),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
yield* journal.registerWorkOwner("deliveries", {
|
|
153
|
+
threads: (request) =>
|
|
154
|
+
Ref.get(state).pipe(
|
|
155
|
+
Effect.map((current) => {
|
|
156
|
+
const candidates = boundedWorkSelection<ThreadId>(
|
|
157
|
+
request.limit,
|
|
158
|
+
(id) => id,
|
|
159
|
+
request.afterThreadId,
|
|
160
|
+
);
|
|
68
161
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const pending = new Map(current.pending);
|
|
162
|
+
for (const threadId of current.pending.keys()) candidates.add(threadId);
|
|
163
|
+
const threadIds = candidates.values;
|
|
72
164
|
|
|
73
|
-
|
|
74
|
-
|
|
165
|
+
const afterThreadId =
|
|
166
|
+
threadIds.length === request.limit ? threadIds.at(-1) : undefined;
|
|
75
167
|
|
|
76
|
-
|
|
77
|
-
|
|
168
|
+
return {
|
|
169
|
+
threadIds,
|
|
170
|
+
...(afterThreadId === undefined ? {} : { afterThreadId }),
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
page: (threadId, after, limit) =>
|
|
175
|
+
Effect.gen(function* () {
|
|
176
|
+
const current = yield* Ref.get(state);
|
|
177
|
+
|
|
178
|
+
const candidates = boundedWorkSelection<MessageDeliveryKey["messageId"]>(
|
|
179
|
+
limit + 1,
|
|
180
|
+
(id) => id,
|
|
181
|
+
after,
|
|
182
|
+
);
|
|
78
183
|
|
|
79
|
-
|
|
80
|
-
const
|
|
184
|
+
for (const id of current.pending.get(threadId) ?? []) candidates.add(id);
|
|
185
|
+
const ids = candidates.values;
|
|
81
186
|
|
|
82
|
-
const
|
|
83
|
-
record.key.ownerThreadId,
|
|
84
|
-
worker.delegationId,
|
|
85
|
-
worker.targetAgentId,
|
|
86
|
-
]);
|
|
187
|
+
const entries: Array<ThreadWorkEntry> = [];
|
|
87
188
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
189
|
+
for (const messageId of ids.slice(0, limit)) {
|
|
190
|
+
const key = messageDeliveryKeyString({ ownerThreadId: threadId, messageId });
|
|
191
|
+
const entry = current.work.get(key);
|
|
192
|
+
|
|
193
|
+
if (entry === undefined || !current.records.has(key))
|
|
194
|
+
return yield* ThreadStoreError.make({
|
|
195
|
+
operation: "delivery work",
|
|
196
|
+
message: "Pending delivery index is incomplete or corrupt",
|
|
197
|
+
});
|
|
198
|
+
entries.push(entry);
|
|
199
|
+
}
|
|
200
|
+
const next = ids.length > limit ? ids[limit - 1] : undefined;
|
|
201
|
+
|
|
202
|
+
return { entries, ...(next === undefined ? {} : { after: next }) };
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const commitInto = (
|
|
207
|
+
current: ReturnType<typeof backend>,
|
|
208
|
+
record: MessageDeliveryRecord,
|
|
209
|
+
encoded: string,
|
|
210
|
+
publish: boolean,
|
|
211
|
+
) => {
|
|
212
|
+
const key = messageDeliveryKeyString(record.key);
|
|
213
|
+
const owner = record.key.ownerThreadId;
|
|
214
|
+
const old = current.metadata.get(key);
|
|
215
|
+
const update = isWorkerUpdateDelivery(record);
|
|
216
|
+
const pending = messageDeliveryUsesCapacity(record);
|
|
217
|
+
|
|
218
|
+
const peer =
|
|
219
|
+
record.envelope.messageAdmission !== undefined &&
|
|
220
|
+
"peerName" in record.envelope.messageAdmission;
|
|
221
|
+
|
|
222
|
+
let counts = current.counts.get(owner);
|
|
223
|
+
|
|
224
|
+
if (counts === undefined)
|
|
225
|
+
current.counts.set(owner, (counts = { retained: 0, ordinary: 0, update: 0, peer: 0 }));
|
|
226
|
+
if (old === undefined) counts.retained++;
|
|
227
|
+
if (old?.pending) {
|
|
228
|
+
if (old.update) counts.update--;
|
|
229
|
+
else counts.ordinary--;
|
|
230
|
+
if (old.peer) counts.peer--;
|
|
231
|
+
}
|
|
232
|
+
if (pending) {
|
|
233
|
+
if (update) counts.update++;
|
|
234
|
+
else counts.ordinary++;
|
|
235
|
+
if (peer) counts.peer++;
|
|
236
|
+
}
|
|
237
|
+
let owned = current.pending.get(owner);
|
|
238
|
+
|
|
239
|
+
if (owned === undefined) current.pending.set(owner, (owned = new Set()));
|
|
240
|
+
if (pending) owned.add(record.key.messageId);
|
|
241
|
+
else owned.delete(record.key.messageId);
|
|
242
|
+
if (owned.size === 0) current.pending.delete(owner);
|
|
243
|
+
retainKey(keysFor(current, ownerKey(owner)), record.key.messageId);
|
|
244
|
+
const admission = record.envelope.workerAdmission;
|
|
245
|
+
|
|
246
|
+
if (admission !== undefined) {
|
|
247
|
+
const worker = admission.origin.worker;
|
|
248
|
+
|
|
249
|
+
if (record.key.messageId === admission.origin.firstMessageId)
|
|
250
|
+
retainKey(
|
|
251
|
+
keysFor(current, JSON.stringify([owner, worker.delegationId, worker.targetAgentId])),
|
|
252
|
+
record.key.messageId,
|
|
253
|
+
);
|
|
254
|
+
const inputs = keysFor(current, JSON.stringify([owner, worker.threadId]));
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
(record.status === "pending" || record.status === "parked") &&
|
|
258
|
+
record.receipt === null
|
|
259
|
+
)
|
|
260
|
+
retainKey(inputs, record.key.messageId);
|
|
261
|
+
else removeKey(inputs, record.key.messageId);
|
|
262
|
+
}
|
|
263
|
+
const deadline = messageDeliveryDeadline(record);
|
|
264
|
+
let ownerDeadlines = current.ownerDeadlineKeys.get(owner);
|
|
265
|
+
|
|
266
|
+
if (ownerDeadlines === undefined)
|
|
267
|
+
current.ownerDeadlineKeys.set(owner, (ownerDeadlines = []));
|
|
268
|
+
if (old?.deadlineKey !== undefined) {
|
|
269
|
+
current.deadlines.delete(old.deadlineKey);
|
|
270
|
+
removeKey(current.deadlineKeys, old.deadlineKey);
|
|
271
|
+
removeKey(ownerDeadlines, old.deadlineKey);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const deadlineKey =
|
|
275
|
+
deadline === null ? undefined : `${String(deadline).padStart(16, "0")}:${key}`;
|
|
104
276
|
|
|
105
|
-
|
|
277
|
+
if (deadlineKey !== undefined && deadline !== null) {
|
|
278
|
+
current.deadlines.set(deadlineKey, { key: record.key, deadline });
|
|
279
|
+
retainKey(current.deadlineKeys, deadlineKey);
|
|
280
|
+
retainKey(ownerDeadlines, deadlineKey);
|
|
281
|
+
}
|
|
282
|
+
current.metadata.set(key, {
|
|
283
|
+
update,
|
|
284
|
+
pending,
|
|
285
|
+
peer,
|
|
286
|
+
...(deadlineKey === undefined ? {} : { deadlineKey }),
|
|
106
287
|
});
|
|
288
|
+
if (pending)
|
|
289
|
+
current.work.set(key, {
|
|
290
|
+
id: workId("delivery", record.key.messageId),
|
|
291
|
+
owner: { _tag: "Delivery", messageId: record.key.messageId },
|
|
292
|
+
partition: update ? "update" : "ordinary",
|
|
293
|
+
stateReference: {
|
|
294
|
+
_tag: "Delivery",
|
|
295
|
+
messageId: record.key.messageId,
|
|
296
|
+
version: record.version,
|
|
297
|
+
},
|
|
298
|
+
state: record.status === "pending" ? "ready" : "waiting",
|
|
299
|
+
...(record.status === "parked"
|
|
300
|
+
? { wait: "parked" as const }
|
|
301
|
+
: record.receipt !== null
|
|
302
|
+
? { wait: "destination" as const }
|
|
303
|
+
: {}),
|
|
304
|
+
...(deadline === null ? {} : { notBeforeMillis: deadline }),
|
|
305
|
+
...(record.receipt === null
|
|
306
|
+
? {}
|
|
307
|
+
: {
|
|
308
|
+
receiptId: record.receipt.receiptId,
|
|
309
|
+
queueSequence: record.receipt.queueSequence,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
else current.work.delete(key);
|
|
313
|
+
current.records.set(key, encoded);
|
|
314
|
+
if (publish) {
|
|
315
|
+
journal.initializeWork(owner);
|
|
316
|
+
journal.retainDelivery(owner, record.key.messageId);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const commit = (record: MessageDeliveryRecord, encoded: string) =>
|
|
321
|
+
Effect.sync(() => commitInto(MutableRef.get(state.ref), record, encoded, true));
|
|
107
322
|
|
|
108
|
-
const lock = yield* Semaphore.make(1);
|
|
109
323
|
const failpoint = yield* MessageDeliveryFailpoint;
|
|
110
324
|
|
|
111
|
-
const encode = Effect.
|
|
112
|
-
record: MessageDeliveryRecord,
|
|
113
|
-
) {
|
|
325
|
+
const encode = Effect.fnUntraced(function* (record: MessageDeliveryRecord) {
|
|
114
326
|
const text = yield* Schema.encodeEffect(codec)(record).pipe(
|
|
115
327
|
Effect.mapError(() =>
|
|
116
328
|
MessageDeliveryError.make({ reason: "corrupt", operation: "encode" }),
|
|
@@ -127,133 +339,454 @@ export const memoryMessageDeliveryStoreLayer = (
|
|
|
127
339
|
return text;
|
|
128
340
|
});
|
|
129
341
|
|
|
130
|
-
const
|
|
131
|
-
|
|
342
|
+
const insert: MessageDeliveryStore["Service"]["insert"] = Effect.fnUntraced(
|
|
343
|
+
function* (record) {
|
|
344
|
+
const encoded = yield* encode(record);
|
|
345
|
+
const input = yield* decode(encoded);
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
input.version !== 1 ||
|
|
349
|
+
input.status !== "pending" ||
|
|
350
|
+
input.leaseUntilMillis !== null ||
|
|
351
|
+
input.retry.attempts !== 0 ||
|
|
352
|
+
input.retry.generation !== 0 ||
|
|
353
|
+
input.retry.automaticAttempts !== 0 ||
|
|
354
|
+
input.retry.nextAttemptAtMillis !== input.createdAtMillis ||
|
|
355
|
+
input.retry.lastAttemptAtMillis !== null ||
|
|
356
|
+
input.retry.lastFailure !== null ||
|
|
357
|
+
input.deadlineAtMillis !== input.initialDeadlineAtMillis
|
|
358
|
+
) {
|
|
359
|
+
return yield* MessageDeliveryError.make({
|
|
360
|
+
reason: "validation",
|
|
361
|
+
operation: "insert-state",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
new TextEncoder().encode(JSON.stringify(input.envelope)).byteLength >
|
|
366
|
+
config.maxEnvelopeBytes
|
|
367
|
+
) {
|
|
368
|
+
return yield* MessageDeliveryError.make({
|
|
369
|
+
reason: "capacity",
|
|
370
|
+
operation: "envelope-bytes",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
yield* failpoint.hit("message-delivery:insert:before");
|
|
374
|
+
|
|
375
|
+
const inserted = yield* journal.withMutation(
|
|
376
|
+
Effect.uninterruptible(
|
|
377
|
+
Effect.gen(function* () {
|
|
378
|
+
yield* journal.checkThreadCapacity(input.key.ownerThreadId).pipe(
|
|
379
|
+
Effect.mapError(() =>
|
|
380
|
+
MessageDeliveryError.make({
|
|
381
|
+
reason: "capacity",
|
|
382
|
+
operation: "Thread capacity",
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
const { records } = yield* Ref.get(state);
|
|
387
|
+
const key = messageDeliveryKeyString(input.key);
|
|
388
|
+
const existingText = records.get(key);
|
|
389
|
+
|
|
390
|
+
if (existingText !== undefined) {
|
|
391
|
+
const existing = yield* decode(existingText);
|
|
392
|
+
|
|
393
|
+
if (!sameMessageDeliveryIdentity(existing, input))
|
|
394
|
+
return yield* MessageDeliveryError.make({
|
|
395
|
+
reason: "conflict",
|
|
396
|
+
operation: "insert",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
journal.retainDelivery(existing.key.ownerThreadId, existing.key.messageId);
|
|
400
|
+
|
|
401
|
+
return existing;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
yield* checkTransfer(input);
|
|
405
|
+
const update = isWorkerUpdateDelivery(input);
|
|
406
|
+
const counts = MutableRef.get(state.ref).counts.get(input.key.ownerThreadId);
|
|
407
|
+
const capacity = messageDeliveryCapacity(config, update);
|
|
408
|
+
|
|
409
|
+
if ((update ? (counts?.update ?? 0) : (counts?.ordinary ?? 0)) >= capacity.pending)
|
|
410
|
+
return yield* MessageDeliveryError.make({
|
|
411
|
+
reason: "capacity",
|
|
412
|
+
operation: "insert",
|
|
413
|
+
});
|
|
414
|
+
yield* commit(input, encoded);
|
|
415
|
+
|
|
416
|
+
return input;
|
|
417
|
+
}),
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
yield* failpoint.hit("message-delivery:insert:after");
|
|
422
|
+
|
|
423
|
+
return yield* decode(yield* encode(inserted));
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const get: MessageDeliveryStore["Service"]["get"] = Effect.fnUntraced(function* (key) {
|
|
428
|
+
const input = yield* validateMessageDelivery(MessageDeliveryKey, key, "get");
|
|
429
|
+
const text = (yield* Ref.get(state)).records.get(messageDeliveryKeyString(input));
|
|
430
|
+
|
|
431
|
+
return text === undefined ? null : yield* decode(text);
|
|
132
432
|
});
|
|
133
433
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
input.version !== 1 ||
|
|
142
|
-
input.status !== "pending" ||
|
|
143
|
-
input.leaseUntilMillis !== null ||
|
|
144
|
-
input.retry.attempts !== 0 ||
|
|
145
|
-
input.retry.generation !== 0 ||
|
|
146
|
-
input.retry.automaticAttempts !== 0 ||
|
|
147
|
-
input.retry.nextAttemptAtMillis !== input.createdAtMillis ||
|
|
148
|
-
input.retry.lastAttemptAtMillis !== null ||
|
|
149
|
-
input.retry.lastFailure !== null ||
|
|
150
|
-
input.deadlineAtMillis !== input.initialDeadlineAtMillis
|
|
151
|
-
) {
|
|
152
|
-
return yield* MessageDeliveryError.make({
|
|
153
|
-
reason: "validation",
|
|
154
|
-
operation: "insert-state",
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
if (
|
|
158
|
-
new TextEncoder().encode(JSON.stringify(input.envelope)).byteLength >
|
|
159
|
-
config.maxEnvelopeBytes
|
|
160
|
-
) {
|
|
161
|
-
return yield* MessageDeliveryError.make({
|
|
162
|
-
reason: "capacity",
|
|
163
|
-
operation: "envelope-bytes",
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
yield* failpoint.hit("message-delivery:insert:before");
|
|
434
|
+
const change: MessageDeliveryStore["Service"]["change"] = Effect.fnUntraced(
|
|
435
|
+
function* (key, change) {
|
|
436
|
+
const input = yield* validateMessageDelivery(MessageDeliveryChange, change, "change");
|
|
437
|
+
const decodedKey = yield* validateMessageDelivery(MessageDeliveryKey, key, "change");
|
|
438
|
+
const point = `message-delivery:${input._tag.toLowerCase()}`;
|
|
167
439
|
|
|
168
|
-
|
|
169
|
-
Effect.uninterruptible(
|
|
170
|
-
Effect.gen(function* () {
|
|
171
|
-
const { records } = yield* Ref.get(state);
|
|
172
|
-
const key = messageDeliveryKeyString(input.key);
|
|
173
|
-
const existingText = records.get(key);
|
|
440
|
+
yield* failpoint.hit(`${point}:before`);
|
|
174
441
|
|
|
175
|
-
|
|
176
|
-
|
|
442
|
+
const changed = yield* journal.withMutation(
|
|
443
|
+
Effect.uninterruptible(
|
|
444
|
+
Effect.gen(function* () {
|
|
445
|
+
const existing = yield* get(decodedKey);
|
|
177
446
|
|
|
178
|
-
if (
|
|
447
|
+
if (existing === null)
|
|
179
448
|
return yield* MessageDeliveryError.make({
|
|
180
|
-
reason: "
|
|
181
|
-
operation: "
|
|
449
|
+
reason: "not-found",
|
|
450
|
+
operation: "change",
|
|
182
451
|
});
|
|
452
|
+
const next = yield* Effect.fromResult(applyMessageDeliveryChange(existing, input));
|
|
183
453
|
|
|
184
|
-
return existing;
|
|
185
|
-
|
|
454
|
+
if (next === existing) return existing;
|
|
455
|
+
yield* checkTransfer(next);
|
|
456
|
+
const encoded = yield* encode(next);
|
|
186
457
|
|
|
187
|
-
|
|
188
|
-
(record) =>
|
|
189
|
-
record.key.ownerThreadId === input.key.ownerThreadId &&
|
|
190
|
-
isWorkerUpdateDelivery(record) === isWorkerUpdateDelivery(input),
|
|
191
|
-
);
|
|
458
|
+
yield* commit(next, encoded);
|
|
192
459
|
|
|
193
|
-
|
|
460
|
+
return next;
|
|
461
|
+
}),
|
|
462
|
+
),
|
|
463
|
+
);
|
|
194
464
|
|
|
195
|
-
|
|
196
|
-
owned.length >= capacity.retained ||
|
|
197
|
-
owned.filter(messageDeliveryUsesCapacity).length >= capacity.pending
|
|
198
|
-
) {
|
|
199
|
-
return yield* MessageDeliveryError.make({
|
|
200
|
-
reason: "capacity",
|
|
201
|
-
operation: "insert",
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
yield* commit(input, encoded);
|
|
465
|
+
yield* failpoint.hit(`${point}:after`);
|
|
205
466
|
|
|
206
|
-
|
|
467
|
+
return yield* decode(yield* encode(changed));
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const transferDecode = (text: string) =>
|
|
472
|
+
decode(text).pipe(
|
|
473
|
+
Effect.mapError(() =>
|
|
474
|
+
ThreadStoreError.make({
|
|
475
|
+
operation: "delivery transfer",
|
|
476
|
+
message: "Invalid retained delivery wire",
|
|
207
477
|
}),
|
|
208
478
|
),
|
|
209
479
|
);
|
|
210
480
|
|
|
211
|
-
|
|
481
|
+
yield* journal.registerDeliveryTransfer({
|
|
482
|
+
record: (threadId, messageId) => {
|
|
483
|
+
const text = MutableRef.get(state.ref).records.get(
|
|
484
|
+
messageDeliveryKeyString({ ownerThreadId: threadId, messageId }),
|
|
485
|
+
);
|
|
212
486
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"MemoryMessageDeliveryStore.get",
|
|
218
|
-
)(function* (key) {
|
|
219
|
-
const input = yield* validateMessageDelivery(MessageDeliveryKey, key, "get");
|
|
220
|
-
const text = (yield* Ref.get(state)).records.get(messageDeliveryKeyString(input));
|
|
221
|
-
|
|
222
|
-
return text === undefined ? null : yield* decode(text);
|
|
223
|
-
});
|
|
487
|
+
return text === undefined ? Effect.succeed(undefined) : transferDecode(text);
|
|
488
|
+
},
|
|
489
|
+
snapshot: (threadId) => {
|
|
490
|
+
const counts = MutableRef.get(state.ref).counts.get(threadId);
|
|
224
491
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
492
|
+
return {
|
|
493
|
+
idle: (MutableRef.get(state.ref).pending.get(threadId)?.size ?? 0) === 0,
|
|
494
|
+
deliveries: counts?.retained ?? 0,
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
pendingPeerCount: (threadId, limit) =>
|
|
498
|
+
Math.min(MutableRef.get(state.ref).counts.get(threadId)?.peer ?? 0, limit),
|
|
499
|
+
facts: Effect.fnUntraced(function* (threadId, after) {
|
|
500
|
+
const current = MutableRef.get(state.ref);
|
|
501
|
+
const keys = current.ordered.get(ownerKey(threadId)) ?? [];
|
|
502
|
+
|
|
503
|
+
const at =
|
|
504
|
+
after === undefined
|
|
505
|
+
? 0
|
|
506
|
+
: position(keys, after) + Number(keys[position(keys, after)] === after);
|
|
507
|
+
|
|
508
|
+
const messageId = keys[at];
|
|
509
|
+
|
|
510
|
+
const record =
|
|
511
|
+
messageId === undefined
|
|
512
|
+
? undefined
|
|
513
|
+
: yield* transferDecode(
|
|
514
|
+
current.records.get(
|
|
515
|
+
messageDeliveryKeyString({
|
|
516
|
+
ownerThreadId: threadId,
|
|
517
|
+
messageId: Schema.decodeSync(MessageDeliveryKey.fields.messageId)(messageId),
|
|
518
|
+
}),
|
|
519
|
+
) ?? "",
|
|
520
|
+
);
|
|
233
521
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
522
|
+
return {
|
|
523
|
+
facts: {
|
|
524
|
+
admissions: [],
|
|
525
|
+
commands: { aborts: [], approvals: [], resolutions: [] },
|
|
526
|
+
deliveries:
|
|
527
|
+
record === undefined
|
|
528
|
+
? []
|
|
529
|
+
: [MessageDeliveryRecord.make({ ...record, leaseUntilMillis: null })],
|
|
530
|
+
},
|
|
531
|
+
after: messageId ?? after ?? "",
|
|
532
|
+
};
|
|
533
|
+
}),
|
|
534
|
+
startImport: Effect.fnUntraced(function* (threadId) {
|
|
535
|
+
const current = MutableRef.get(state.ref);
|
|
238
536
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
537
|
+
if ((current.counts.get(threadId)?.retained ?? 0) > 0)
|
|
538
|
+
return yield* ThreadImportRejected.make({
|
|
539
|
+
threadId,
|
|
540
|
+
reason: "target-not-empty",
|
|
541
|
+
message: "Destination Thread owns retained deliveries",
|
|
542
|
+
});
|
|
543
|
+
const staged = backend();
|
|
246
544
|
|
|
247
|
-
|
|
545
|
+
return {
|
|
546
|
+
record: (messageId) => {
|
|
547
|
+
const text = staged.records.get(
|
|
548
|
+
messageDeliveryKeyString({ ownerThreadId: threadId, messageId }),
|
|
549
|
+
);
|
|
248
550
|
|
|
249
|
-
return
|
|
551
|
+
return text === undefined ? Effect.succeed(undefined) : transferDecode(text);
|
|
552
|
+
},
|
|
553
|
+
has: (messageId) =>
|
|
554
|
+
staged.records.has(messageDeliveryKeyString({ ownerThreadId: threadId, messageId })),
|
|
555
|
+
stage: Effect.fnUntraced(function* (page) {
|
|
556
|
+
for (const record of page.archive.deliveries) {
|
|
557
|
+
const key = messageDeliveryKeyString(record.key);
|
|
558
|
+
|
|
559
|
+
if (staged.records.has(key) || current.records.has(key))
|
|
560
|
+
return yield* invalidThreadArchive("Duplicate retained delivery", threadId);
|
|
561
|
+
|
|
562
|
+
const text = yield* encode(record).pipe(
|
|
563
|
+
Effect.mapError(() =>
|
|
564
|
+
ThreadImportRejected.make({
|
|
565
|
+
threadId,
|
|
566
|
+
reason: "unsupported-capacity",
|
|
567
|
+
message: "Delivery wire exceeds destination stored-value bound",
|
|
568
|
+
}),
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (
|
|
573
|
+
new TextEncoder().encode(
|
|
574
|
+
JSON.stringify(
|
|
575
|
+
yield* Schema.encodeEffect(PreparedInput)(record.envelope).pipe(
|
|
576
|
+
Effect.mapError(() =>
|
|
577
|
+
invalidThreadArchive("Invalid delivery envelope", threadId),
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
),
|
|
581
|
+
).byteLength > config.maxEnvelopeBytes
|
|
582
|
+
)
|
|
583
|
+
return yield* ThreadImportRejected.make({
|
|
584
|
+
threadId,
|
|
585
|
+
reason: "unsupported-capacity",
|
|
586
|
+
message: "Delivery envelope exceeds destination bound",
|
|
587
|
+
});
|
|
588
|
+
const counts = staged.counts.get(threadId);
|
|
589
|
+
|
|
590
|
+
if (
|
|
591
|
+
messageDeliveryUsesCapacity(record) &&
|
|
592
|
+
(isWorkerUpdateDelivery(record)
|
|
593
|
+
? (counts?.update ?? 0)
|
|
594
|
+
: (counts?.ordinary ?? 0)) >=
|
|
595
|
+
messageDeliveryCapacity(config, isWorkerUpdateDelivery(record)).pending
|
|
596
|
+
)
|
|
597
|
+
return yield* ThreadImportRejected.make({
|
|
598
|
+
threadId,
|
|
599
|
+
reason: "unsupported-capacity",
|
|
600
|
+
message: "Pending delivery capacity exceeded",
|
|
601
|
+
});
|
|
602
|
+
commitInto(staged, record, text, false);
|
|
603
|
+
}
|
|
250
604
|
}),
|
|
251
|
-
|
|
252
|
-
|
|
605
|
+
prepareCommit: Effect.fnUntraced(function* () {
|
|
606
|
+
const reader = yield* ThreadDeliveryImportReader;
|
|
607
|
+
|
|
608
|
+
const read = (messageId: MessageDeliveryKey["messageId"]) => {
|
|
609
|
+
const text = staged.records.get(
|
|
610
|
+
messageDeliveryKeyString({ ownerThreadId: threadId, messageId }),
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
return text === undefined
|
|
614
|
+
? Effect.fail(invalidThreadArchive("Missing delivery predecessor", threadId))
|
|
615
|
+
: transferDecode(text);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const visit = (messageId: MessageDeliveryKey["messageId"]) => {
|
|
619
|
+
const metadata = staged.metadata.get(
|
|
620
|
+
messageDeliveryKeyString({ ownerThreadId: threadId, messageId }),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return metadata === undefined
|
|
624
|
+
? Effect.fail(
|
|
625
|
+
invalidThreadArchive("Missing delivery predecessor metadata", threadId),
|
|
626
|
+
)
|
|
627
|
+
: Effect.succeed(metadata);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const verifyPredecessors = Effect.fnUntraced(function* (
|
|
631
|
+
start: MessageDeliveryKey["messageId"],
|
|
632
|
+
) {
|
|
633
|
+
let cursor: MessageDeliveryKey["messageId"] | undefined = start;
|
|
634
|
+
|
|
635
|
+
while (cursor !== undefined) {
|
|
636
|
+
const metadata = yield* visit(cursor);
|
|
637
|
+
|
|
638
|
+
if (metadata.importVisit === "verified") break;
|
|
639
|
+
if (metadata.importVisit === "visiting")
|
|
640
|
+
return yield* invalidThreadArchive("Delivery predecessor cycle", threadId);
|
|
641
|
+
metadata.importVisit = "visiting";
|
|
642
|
+
const record: MessageDeliveryRecord = yield* read(cursor);
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
record.predecessor !== undefined &&
|
|
646
|
+
(yield* read(record.predecessor)).createdAtMillis > record.createdAtMillis
|
|
647
|
+
)
|
|
648
|
+
return yield* invalidThreadArchive(
|
|
649
|
+
"Delivery predecessor has a later boundary",
|
|
650
|
+
threadId,
|
|
651
|
+
);
|
|
652
|
+
cursor = record.predecessor;
|
|
653
|
+
}
|
|
654
|
+
cursor = start;
|
|
655
|
+
while (cursor !== undefined) {
|
|
656
|
+
const metadata = yield* visit(cursor);
|
|
657
|
+
|
|
658
|
+
if (metadata.importVisit === "verified") break;
|
|
659
|
+
metadata.importVisit = "verified";
|
|
660
|
+
cursor = (yield* read(cursor)).predecessor;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
for (const text of staged.records.values()) {
|
|
665
|
+
const d = yield* transferDecode(text);
|
|
666
|
+
|
|
667
|
+
yield* verifyPredecessors(d.key.messageId);
|
|
668
|
+
if (d.receipt !== null) {
|
|
669
|
+
if (
|
|
670
|
+
d.settlement !== null &&
|
|
671
|
+
d.settlement.settlementId !== submissionSettlementId(d.receipt.submissionId)
|
|
672
|
+
)
|
|
673
|
+
return yield* invalidThreadArchive(
|
|
674
|
+
"Delivery settlement identity is not canonical",
|
|
675
|
+
threadId,
|
|
676
|
+
);
|
|
677
|
+
const foreign = d.receipt.threadId !== threadId;
|
|
678
|
+
|
|
679
|
+
if (foreign && d.status !== "processed")
|
|
680
|
+
return yield* ThreadImportRejected.make({
|
|
681
|
+
threadId,
|
|
682
|
+
reason: "unsupported-obligations",
|
|
683
|
+
message: "Live foreign delivery receipt cannot be restored",
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const destination = yield* reader.admission(
|
|
687
|
+
d.receipt.threadId,
|
|
688
|
+
d.receipt.submissionId,
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// A closed foreign delivery retains frozen evidence, not the receiver's execution authority.
|
|
692
|
+
if (destination === undefined) {
|
|
693
|
+
if (foreign && d.status === "processed") continue;
|
|
694
|
+
|
|
695
|
+
return yield* invalidThreadArchive(
|
|
696
|
+
"Destination delivery receipt evidence is missing",
|
|
697
|
+
threadId,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const expected = PreparedInput.make({
|
|
702
|
+
schemaVersion: 1,
|
|
703
|
+
authorization: d.envelope.authorization,
|
|
704
|
+
threadId: destination.threadId,
|
|
705
|
+
deliveryPrincipal: destination.principal,
|
|
706
|
+
admissionKey: destination.idempotencyKey,
|
|
707
|
+
agentId: destination.agentId,
|
|
708
|
+
definitions: destination.agentDigests,
|
|
709
|
+
input: destination.inputPayload,
|
|
710
|
+
inputDigest: destination.inputDigest,
|
|
711
|
+
...(destination.admissionGroup === undefined
|
|
712
|
+
? {}
|
|
713
|
+
: { admissionGroup: destination.admissionGroup }),
|
|
714
|
+
...(destination.admissionFence === undefined
|
|
715
|
+
? {}
|
|
716
|
+
: { admissionFence: destination.admissionFence }),
|
|
717
|
+
...(destination.workerAdmission === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: { workerAdmission: destination.workerAdmission }),
|
|
720
|
+
...(destination.messageAdmission === undefined
|
|
721
|
+
? {}
|
|
722
|
+
: { messageAdmission: destination.messageAdmission }),
|
|
723
|
+
});
|
|
253
724
|
|
|
254
|
-
|
|
725
|
+
if (
|
|
726
|
+
d.receipt.receiptId !== destination.receiptId ||
|
|
727
|
+
d.receipt.queueSequence !== destination.queueSequence ||
|
|
728
|
+
!Schema.toEquivalence(PreparedInput)(d.envelope, expected)
|
|
729
|
+
)
|
|
730
|
+
return yield* invalidThreadArchive(
|
|
731
|
+
"Delivery envelope differs from destination admission",
|
|
732
|
+
threadId,
|
|
733
|
+
);
|
|
734
|
+
if (d.settlement !== null) {
|
|
735
|
+
const record = yield* reader.record(
|
|
736
|
+
destination.threadId,
|
|
737
|
+
submissionSettlementRecordId(destination.submissionId),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
if (record === undefined) {
|
|
741
|
+
if (foreign) continue;
|
|
742
|
+
|
|
743
|
+
return yield* invalidThreadArchive(
|
|
744
|
+
"Processed delivery has no destination settlement",
|
|
745
|
+
threadId,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const settled = yield* validateCanonicalSettlement(record, destination).pipe(
|
|
750
|
+
Effect.mapError(() =>
|
|
751
|
+
invalidThreadArchive("Invalid destination settlement", threadId),
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
const failure = settlementFailureFromRecord(record);
|
|
756
|
+
|
|
757
|
+
const expected = Settlement.make({
|
|
758
|
+
submissionId: settled.submissionId,
|
|
759
|
+
receiptId: settled.receiptId,
|
|
760
|
+
settlementId: settled.settlementId,
|
|
761
|
+
outcome: settled.outcome,
|
|
762
|
+
settledAt: d.settlement.settledAt,
|
|
763
|
+
...(failure === undefined ? {} : { failure }),
|
|
764
|
+
...(d.settlement.runDisposition === undefined ||
|
|
765
|
+
settled.runDisposition === undefined
|
|
766
|
+
? {}
|
|
767
|
+
: { runDisposition: settled.runDisposition }),
|
|
768
|
+
...(d.settlement.usageSummary === undefined ||
|
|
769
|
+
settled.usageSummary === undefined
|
|
770
|
+
? {}
|
|
771
|
+
: { usageSummary: settled.usageSummary }),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (!Schema.toEquivalence(Settlement)(d.settlement, expected))
|
|
775
|
+
return yield* invalidThreadArchive(
|
|
776
|
+
"Delivery settlement differs from destination evidence",
|
|
777
|
+
threadId,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
255
782
|
|
|
256
|
-
|
|
783
|
+
return () => {
|
|
784
|
+
for (const text of staged.records.values())
|
|
785
|
+
commitInto(current, Schema.decodeSync(codec)(text), text, true);
|
|
786
|
+
};
|
|
787
|
+
}),
|
|
788
|
+
};
|
|
789
|
+
}),
|
|
257
790
|
});
|
|
258
791
|
|
|
259
792
|
return MessageDeliveryStore.of({
|
|
@@ -262,7 +795,7 @@ export const memoryMessageDeliveryStoreLayer = (
|
|
|
262
795
|
insert,
|
|
263
796
|
get,
|
|
264
797
|
change,
|
|
265
|
-
list: Effect.
|
|
798
|
+
list: Effect.fnUntraced(function* (request) {
|
|
266
799
|
const input = yield* validateMessageDelivery(MessageDeliveryPageRequest, request, "list");
|
|
267
800
|
|
|
268
801
|
const current = yield* Ref.get(state);
|
|
@@ -278,94 +811,97 @@ export const memoryMessageDeliveryStoreLayer = (
|
|
|
278
811
|
? JSON.stringify([input.ownerThreadId, input.pendingWorker])
|
|
279
812
|
: undefined;
|
|
280
813
|
|
|
281
|
-
const
|
|
282
|
-
workerKey
|
|
283
|
-
?
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
record.key.ownerThreadId === input.ownerThreadId &&
|
|
305
|
-
(input.after === undefined || record.key.messageId > input.after),
|
|
306
|
-
)
|
|
307
|
-
.sort((left, right) =>
|
|
308
|
-
left.key.messageId < right.key.messageId
|
|
309
|
-
? -1
|
|
310
|
-
: left.key.messageId > right.key.messageId
|
|
311
|
-
? 1
|
|
312
|
-
: 0,
|
|
814
|
+
const keys =
|
|
815
|
+
workerKey === undefined
|
|
816
|
+
? input.pendingOnly
|
|
817
|
+
? Array.from(current.pending.get(input.ownerThreadId) ?? []).sort()
|
|
818
|
+
: (current.ordered.get(ownerKey(input.ownerThreadId)) ?? [])
|
|
819
|
+
: (current.ordered.get(workerKey) ?? []);
|
|
820
|
+
|
|
821
|
+
const start =
|
|
822
|
+
input.after === undefined
|
|
823
|
+
? 0
|
|
824
|
+
: position(keys, input.after) +
|
|
825
|
+
Number(keys[position(keys, input.after)] === input.after);
|
|
826
|
+
|
|
827
|
+
const items: Array<MessageDeliveryRecord> = [];
|
|
828
|
+
let bytes = 0;
|
|
829
|
+
let at = start;
|
|
830
|
+
|
|
831
|
+
while (at < keys.length && items.length < input.limit) {
|
|
832
|
+
const text = current.records.get(
|
|
833
|
+
messageDeliveryKeyString({
|
|
834
|
+
ownerThreadId: input.ownerThreadId,
|
|
835
|
+
messageId: Schema.decodeSync(MessageDeliveryKey.fields.messageId)(keys[at]),
|
|
836
|
+
}),
|
|
313
837
|
);
|
|
314
838
|
|
|
315
|
-
|
|
839
|
+
if (text === undefined)
|
|
840
|
+
return yield* MessageDeliveryError.make({
|
|
841
|
+
reason: "corrupt",
|
|
842
|
+
operation: "list locator",
|
|
843
|
+
});
|
|
844
|
+
const size = new TextEncoder().encode(text).byteLength;
|
|
845
|
+
|
|
846
|
+
if (size > 32 * 1024 * 1024)
|
|
847
|
+
return yield* MessageDeliveryError.make({
|
|
848
|
+
reason: "capacity",
|
|
849
|
+
operation: "list byte bound",
|
|
850
|
+
});
|
|
851
|
+
if (bytes + size > 32 * 1024 * 1024) break;
|
|
852
|
+
items.push(yield* decode(text));
|
|
853
|
+
bytes += size;
|
|
854
|
+
at++;
|
|
855
|
+
}
|
|
316
856
|
|
|
317
857
|
return {
|
|
318
858
|
items,
|
|
319
|
-
next:
|
|
859
|
+
next: at < keys.length ? (items.at(-1)?.key.messageId ?? null) : null,
|
|
320
860
|
};
|
|
321
861
|
}),
|
|
322
|
-
due: Effect.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
.map((record) => record.key);
|
|
350
|
-
},
|
|
351
|
-
),
|
|
352
|
-
nextDeadline: Effect.fn("MemoryMessageDeliveryStore.nextDeadline")(
|
|
353
|
-
function* (ownerThreadId) {
|
|
354
|
-
if (ownerThreadId !== undefined)
|
|
355
|
-
yield* validateMessageDelivery(ThreadId, ownerThreadId, "nextDeadline");
|
|
356
|
-
let next: number | null = null;
|
|
357
|
-
|
|
358
|
-
for (const record of yield* all()) {
|
|
359
|
-
if (ownerThreadId !== undefined && ownerThreadId !== record.key.ownerThreadId)
|
|
360
|
-
continue;
|
|
361
|
-
const deadline = messageDeliveryDeadline(record);
|
|
362
|
-
|
|
363
|
-
if (deadline !== null && (next === null || deadline < next)) next = deadline;
|
|
364
|
-
}
|
|
862
|
+
due: Effect.fnUntraced(function* (nowMillis, limit, ownerThreadId) {
|
|
863
|
+
const input = yield* validateMessageDelivery(
|
|
864
|
+
Scan,
|
|
865
|
+
{ nowMillis, limit, ...(ownerThreadId === undefined ? {} : { ownerThreadId }) },
|
|
866
|
+
"due",
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const current = MutableRef.get(state.ref);
|
|
870
|
+
|
|
871
|
+
const keys =
|
|
872
|
+
input.ownerThreadId === undefined
|
|
873
|
+
? current.deadlineKeys
|
|
874
|
+
: (current.ownerDeadlineKeys.get(input.ownerThreadId) ?? []);
|
|
875
|
+
|
|
876
|
+
const selected: Array<MessageDeliveryKey> = [];
|
|
877
|
+
|
|
878
|
+
for (const key of keys) {
|
|
879
|
+
const row = current.deadlines.get(key);
|
|
880
|
+
|
|
881
|
+
if (row === undefined)
|
|
882
|
+
return yield* MessageDeliveryError.make({
|
|
883
|
+
reason: "corrupt",
|
|
884
|
+
operation: "deadline index",
|
|
885
|
+
});
|
|
886
|
+
if (row.deadline > input.nowMillis || selected.length === input.limit) break;
|
|
887
|
+
selected.push(row.key);
|
|
888
|
+
}
|
|
365
889
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
890
|
+
return selected;
|
|
891
|
+
}),
|
|
892
|
+
nextDeadline: Effect.fnUntraced(function* (ownerThreadId) {
|
|
893
|
+
if (ownerThreadId !== undefined)
|
|
894
|
+
yield* validateMessageDelivery(ThreadId, ownerThreadId, "nextDeadline");
|
|
895
|
+
const current = MutableRef.get(state.ref);
|
|
896
|
+
|
|
897
|
+
const first = (
|
|
898
|
+
ownerThreadId === undefined
|
|
899
|
+
? current.deadlineKeys
|
|
900
|
+
: current.ownerDeadlineKeys.get(ownerThreadId)
|
|
901
|
+
)?.[0];
|
|
902
|
+
|
|
903
|
+
return first === undefined ? null : (current.deadlines.get(first)?.deadline ?? null);
|
|
904
|
+
}),
|
|
369
905
|
});
|
|
370
906
|
}),
|
|
371
907
|
);
|