@effect-agent/storage-memory 0.1.0-beta.41 → 0.1.0-beta.44

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.
@@ -59,7 +59,9 @@ const makeScheduleStore = Effect.gen(function* () {
59
59
  (record, ownerLimit) =>
60
60
  Effect.gen(function* () {
61
61
  const encoded = yield* encodeRecord("insert", record);
62
+
62
63
  yield* failpoint.hit("schedule:insert:before");
64
+
63
65
  const decision = yield* Effect.uninterruptible(
64
66
  Ref.modify(
65
67
  state,
@@ -74,17 +76,20 @@ const makeScheduleStore = Effect.gen(function* () {
74
76
  ] => {
75
77
  const key = scheduleKeyString(record);
76
78
  const existingText = current.records.get(key);
79
+
77
80
  if (existingText !== undefined) {
78
81
  const decoded = Result.try({
79
82
  try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(existingText),
80
83
  catch: () => storageError("insert", "corrupt"),
81
84
  });
85
+
82
86
  if (Result.isFailure(decoded)) {
83
87
  return [Result.fail(decoded.failure), current];
84
88
  }
85
89
  if (decoded.success.creationFingerprint === record.creationFingerprint) {
86
90
  return [Result.succeed(decoded.success), current];
87
91
  }
92
+
88
93
  return [
89
94
  Result.fail(
90
95
  ScheduleConflict.make({ reason: "creation", key: scheduleKeyOf(record) }),
@@ -94,11 +99,13 @@ const makeScheduleStore = Effect.gen(function* () {
94
99
  }
95
100
 
96
101
  let ownerCount = 0;
102
+
97
103
  for (const text of current.records.values()) {
98
104
  const decoded = Result.try({
99
105
  try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(text),
100
106
  catch: () => storageError("insert", "corrupt"),
101
107
  });
108
+
102
109
  if (Result.isFailure(decoded)) {
103
110
  return [Result.fail(decoded.failure), current];
104
111
  }
@@ -112,14 +119,19 @@ const makeScheduleStore = Effect.gen(function* () {
112
119
  return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
113
120
  }
114
121
  const records = new Map(current.records);
122
+
115
123
  records.set(key, encoded);
116
124
  const next = { records };
125
+
117
126
  return [Result.succeed(record), next];
118
127
  },
119
128
  ),
120
129
  );
130
+
121
131
  const inserted = yield* Effect.fromResult(decision);
132
+
122
133
  yield* failpoint.hit("schedule:insert:after");
134
+
123
135
  return yield* decodeRecord("insert", yield* encodeRecord("insert", inserted));
124
136
  }),
125
137
  );
@@ -128,6 +140,7 @@ const makeScheduleStore = Effect.gen(function* () {
128
140
  function* (key) {
129
141
  const decodedKey = yield* decodeInput("get", ScheduleKey, key);
130
142
  const text = (yield* Ref.get(state)).records.get(scheduleKeyString(decodedKey));
143
+
131
144
  return text === undefined ? null : yield* decodeRecord("get", text);
132
145
  },
133
146
  );
@@ -136,8 +149,10 @@ const makeScheduleStore = Effect.gen(function* () {
136
149
  function* (request) {
137
150
  const decodedRequest = yield* decodeInput("list", SchedulePageRequest, request);
138
151
  const records: Array<ScheduleRecord> = [];
152
+
139
153
  for (const text of (yield* Ref.get(state)).records.values()) {
140
154
  const record = yield* decodeRecord("list", text);
155
+
141
156
  if (
142
157
  sameOwner(record, decodedRequest.owner) &&
143
158
  (decodedRequest.after === undefined ||
@@ -149,6 +164,7 @@ const makeScheduleStore = Effect.gen(function* () {
149
164
  records.sort((left, right) => compareScheduleNames(left.scheduleId, right.scheduleId));
150
165
  const hasNext = records.length > decodedRequest.limit;
151
166
  const items = records.slice(0, decodedRequest.limit);
167
+
152
168
  return {
153
169
  items,
154
170
  next: hasNext ? (items.at(-1)?.scheduleId ?? null) : null,
@@ -161,7 +177,9 @@ const makeScheduleStore = Effect.gen(function* () {
161
177
  Effect.gen(function* () {
162
178
  const decodedKey = yield* decodeInput("change", ScheduleKey, key);
163
179
  const decodedCommand = yield* decodeInput("change", ScheduleChange, command);
180
+
164
181
  yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:before`);
182
+
165
183
  const decision = yield* Effect.uninterruptible(
166
184
  Ref.modify(
167
185
  state,
@@ -176,17 +194,21 @@ const makeScheduleStore = Effect.gen(function* () {
176
194
  ] => {
177
195
  const storageKey = scheduleKeyString(decodedKey);
178
196
  const text = current.records.get(storageKey);
197
+
179
198
  if (text === undefined) {
180
199
  return [Result.fail(ScheduleNotFound.make({ key: decodedKey })), current];
181
200
  }
201
+
182
202
  const decoded = Result.try({
183
203
  try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(text),
184
204
  catch: () => storageError("change", "corrupt"),
185
205
  });
206
+
186
207
  if (Result.isFailure(decoded)) {
187
208
  return [Result.fail(decoded.failure), current];
188
209
  }
189
210
  const applied = applyScheduleChange(decoded.success, decodedCommand);
211
+
190
212
  if (Result.isFailure(applied)) {
191
213
  return [Result.fail(applied.failure), current];
192
214
  }
@@ -195,10 +217,12 @@ const makeScheduleStore = Effect.gen(function* () {
195
217
  }
196
218
  if (!scheduleUsesCapacity(decoded.success) && scheduleUsesCapacity(applied.success)) {
197
219
  let count = 0;
220
+
198
221
  for (const text of current.records.values()) {
199
222
  const candidate = Schema.decodeUnknownResult(
200
223
  Schema.fromJsonString(ScheduleRecord),
201
224
  )(text);
225
+
202
226
  if (Result.isFailure(candidate))
203
227
  return [Result.fail(storageError("change", "corrupt")), current];
204
228
  if (
@@ -210,23 +234,30 @@ const makeScheduleStore = Effect.gen(function* () {
210
234
  if (count >= ownerLimit)
211
235
  return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
212
236
  }
237
+
213
238
  const encoded = Result.try({
214
239
  try: () =>
215
240
  Schema.encodeSync(Schema.fromJsonString(ScheduleRecord))(applied.success),
216
241
  catch: () => storageError("change", "corrupt"),
217
242
  });
243
+
218
244
  if (Result.isFailure(encoded)) {
219
245
  return [Result.fail(encoded.failure), current];
220
246
  }
221
247
  const records = new Map(current.records);
248
+
222
249
  records.set(storageKey, encoded.success);
223
250
  const next = { records };
251
+
224
252
  return [Result.succeed(applied.success), next];
225
253
  },
226
254
  ),
227
255
  );
256
+
228
257
  const changed = yield* Effect.fromResult(decision);
258
+
229
259
  yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:after`);
260
+
230
261
  return yield* decodeRecord("change", yield* encodeRecord("change", changed));
231
262
  }),
232
263
  );
@@ -235,12 +266,16 @@ const makeScheduleStore = Effect.gen(function* () {
235
266
  function* (nowMillis, limit, owner, after) {
236
267
  const decodedOwner =
237
268
  owner === undefined ? undefined : yield* decodeInput("due", ScheduleOwner, owner);
269
+
238
270
  const cursor =
239
271
  after === undefined ? undefined : yield* decodeInput("due", ScheduleDueCursor, after);
272
+
240
273
  const records: Array<ScheduleDueCursor> = [];
274
+
241
275
  for (const text of (yield* Ref.get(state)).records.values()) {
242
276
  const record = yield* decodeRecord("due", text);
243
277
  const deadline = scheduleDeadline(record);
278
+
244
279
  if (
245
280
  deadline !== null &&
246
281
  deadline <= nowMillis &&
@@ -254,8 +289,10 @@ const makeScheduleStore = Effect.gen(function* () {
254
289
  }
255
290
  records.sort((left, right) => {
256
291
  const byDeadline = left.deadlineAtMillis - right.deadlineAtMillis;
292
+
257
293
  return byDeadline !== 0 ? byDeadline : compareScheduleKeys(left, right);
258
294
  });
295
+
259
296
  return records.slice(0, limit);
260
297
  },
261
298
  );
@@ -265,13 +302,18 @@ const makeScheduleStore = Effect.gen(function* () {
265
302
  )(function* (owner) {
266
303
  const decodedOwner =
267
304
  owner === undefined ? undefined : yield* decodeInput("nextDeadline", ScheduleOwner, owner);
305
+
268
306
  let earliest: number | null = null;
307
+
269
308
  for (const text of (yield* Ref.get(state)).records.values()) {
270
309
  const record = yield* decodeRecord("nextDeadline", text);
310
+
271
311
  if (decodedOwner !== undefined && !sameOwner(record, decodedOwner)) continue;
272
312
  const deadline = scheduleDeadline(record);
313
+
273
314
  if (deadline !== null && (earliest === null || deadline < earliest)) earliest = deadline;
274
315
  }
316
+
275
317
  return earliest;
276
318
  });
277
319
 
@@ -104,10 +104,12 @@ const offsetSequence = Effect.fn("MemoryThreadStore.offsetSequence")((
104
104
  if (offset === undefined) return Effect.succeed(ZERO_CANONICAL_SEQUENCE);
105
105
  const prefix = `memory:v1:${Encoding.encodeBase64(threadId)}:`;
106
106
  const encodedSequence = offset.startsWith(prefix) ? offset.slice(prefix.length) : "";
107
+
107
108
  if (!/^\d+$/.test(encodedSequence)) {
108
109
  return Effect.fail(storeError("observe", "Malformed observation offset"));
109
110
  }
110
111
  const sequence = Number(encodedSequence);
112
+
111
113
  return Number.isSafeInteger(sequence)
112
114
  ? Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(
113
115
  Effect.mapError(() => storeError("observe", "Malformed observation offset")),
@@ -123,6 +125,7 @@ const findThread = Effect.fn("MemoryThreadStore.findThread")((
123
125
  threadId: ThreadId,
124
126
  ): Effect.Effect<StoredThread, ThreadNotMaterialized> => {
125
127
  const thread = state.threads.get(threadId);
128
+
126
129
  return thread === undefined
127
130
  ? Effect.fail(ThreadNotMaterialized.make({ threadId }))
128
131
  : Effect.succeed(thread);
@@ -140,6 +143,7 @@ const validateCheckpointVersion = Effect.fn("MemoryThreadStore.validateCheckpoin
140
143
  const envelope = yield* Schema.decodeUnknownEffect(CheckpointVersionEnvelope)(value).pipe(
141
144
  Effect.mapError(() => storeError("saveCheckpoint", "Invalid saveCheckpoint request")),
142
145
  );
146
+
143
147
  if (envelope.checkpoint.schemaVersion !== 1) {
144
148
  return yield* CheckpointRejected.make({
145
149
  threadId: envelope.checkpoint.threadId,
@@ -153,6 +157,7 @@ const makeThreadStore = Effect.gen(function* () {
153
157
  const crypto = yield* Crypto.Crypto;
154
158
  const state = yield* Ref.make<MemoryState>({ threads: new Map() });
155
159
  const updates = yield* PubSub.sliding<void>(1);
160
+
156
161
  yield* Effect.addFinalizer(() => PubSub.shutdown(updates));
157
162
 
158
163
  const materialize: ThreadStore["Service"]["materialize"] = Effect.fn(
@@ -160,10 +165,12 @@ const makeThreadStore = Effect.gen(function* () {
160
165
  )((unvalidated) =>
161
166
  Effect.gen(function* () {
162
167
  const request = yield* validate(ThreadMaterialization, "materialize", unvalidated);
168
+
163
169
  const decision = yield* Ref.modify(
164
170
  state,
165
171
  (current): readonly [MaterializeDecision, MemoryState] => {
166
172
  const existing = current.threads.get(request.threadId);
173
+
167
174
  if (existing !== undefined) {
168
175
  if (request.producerEpoch < existing.producerEpoch) {
169
176
  return [
@@ -182,10 +189,12 @@ const makeThreadStore = Effect.gen(function* () {
182
189
  return [{ _tag: "success" }, current];
183
190
  }
184
191
  const threads = new Map(current.threads);
192
+
185
193
  threads.set(request.threadId, {
186
194
  ...existing,
187
195
  producerEpoch: request.producerEpoch,
188
196
  });
197
+
189
198
  return [{ _tag: "success" }, { threads }];
190
199
  }
191
200
  if (current.threads.size >= MAX_THREADS) {
@@ -198,6 +207,7 @@ const makeThreadStore = Effect.gen(function* () {
198
207
  ];
199
208
  }
200
209
  const threads = new Map(current.threads);
210
+
201
211
  threads.set(request.threadId, {
202
212
  producerEpoch: request.producerEpoch,
203
213
  tailSequence: ZERO_CANONICAL_SEQUENCE,
@@ -208,9 +218,11 @@ const makeThreadStore = Effect.gen(function* () {
208
218
  tailDigests: new Map([[ZERO_CANONICAL_SEQUENCE, EMPTY_TAIL_DIGEST]]),
209
219
  checkpoints: new Map(),
210
220
  });
221
+
211
222
  return [{ _tag: "success" }, { threads }];
212
223
  },
213
224
  );
225
+
214
226
  if (decision._tag === "failure") return yield* decision.error;
215
227
  }),
216
228
  );
@@ -219,6 +231,7 @@ const makeThreadStore = Effect.gen(function* () {
219
231
  (unvalidated) =>
220
232
  Effect.gen(function* () {
221
233
  const request = yield* validate(FencedAppendRequest, "append", unvalidated);
234
+
222
235
  const digest = yield* digestCanonicalBatch(request.expectedTailDigest, request.batch).pipe(
223
236
  Effect.provideService(Crypto.Crypto, crypto),
224
237
  Effect.mapError((error) => storeError("append", error.message, error)),
@@ -227,6 +240,7 @@ const makeThreadStore = Effect.gen(function* () {
227
240
  const decision = yield* Effect.uninterruptible(
228
241
  Ref.modify(state, (current): readonly [AppendDecision, MemoryState] => {
229
242
  const thread = current.threads.get(request.threadId);
243
+
230
244
  if (thread === undefined) {
231
245
  return [
232
246
  {
@@ -252,6 +266,7 @@ const makeThreadStore = Effect.gen(function* () {
252
266
  ];
253
267
  }
254
268
  const previous = thread.batches.get(request.batch.batchId);
269
+
255
270
  if (previous !== undefined) {
256
271
  if (previous.digest !== digest) {
257
272
  return [
@@ -266,6 +281,7 @@ const makeThreadStore = Effect.gen(function* () {
266
281
  current,
267
282
  ];
268
283
  }
284
+
269
285
  return [
270
286
  {
271
287
  _tag: "success",
@@ -312,6 +328,7 @@ const makeThreadStore = Effect.gen(function* () {
312
328
  }
313
329
 
314
330
  const batchRecordIds = new Set<RecordId>();
331
+
315
332
  for (const record of request.batch.records) {
316
333
  if (thread.recordIds.has(record.recordId) || batchRecordIds.has(record.recordId)) {
317
334
  return [
@@ -331,6 +348,7 @@ const makeThreadStore = Effect.gen(function* () {
331
348
 
332
349
  const records = request.batch.records.map((record, index) => {
333
350
  const sequence = decodeCanonicalSequence(thread.tailSequence + index + 1);
351
+
334
352
  return CanonicalRecordEnvelope.make({
335
353
  threadId: request.threadId,
336
354
  batchId: request.batch.batchId,
@@ -339,20 +357,27 @@ const makeThreadStore = Effect.gen(function* () {
339
357
  record,
340
358
  });
341
359
  });
360
+
342
361
  const lastSequence = decodeCanonicalSequence(thread.tailSequence + records.length);
362
+
343
363
  const result = AppendResult.make({
344
364
  firstSequence: decodeCanonicalSequence(thread.tailSequence + 1),
345
365
  lastSequence,
346
366
  tailDigest: digest,
347
367
  replayed: false,
348
368
  });
369
+
349
370
  const batches = new Map(thread.batches);
371
+
350
372
  batches.set(request.batch.batchId, { digest, result });
351
373
  const recordIds = new Set(thread.recordIds);
374
+
352
375
  for (const recordId of batchRecordIds) recordIds.add(recordId);
353
376
  const tailDigests = new Map(thread.tailDigests);
377
+
354
378
  tailDigests.set(lastSequence, digest);
355
379
  const threads = new Map(current.threads);
380
+
356
381
  threads.set(request.threadId, {
357
382
  ...thread,
358
383
  tailSequence: lastSequence,
@@ -362,6 +387,7 @@ const makeThreadStore = Effect.gen(function* () {
362
387
  batches,
363
388
  tailDigests,
364
389
  });
390
+
365
391
  return [{ _tag: "success", result, records }, { threads }];
366
392
  }).pipe(
367
393
  Effect.tap((decision) =>
@@ -371,7 +397,9 @@ const makeThreadStore = Effect.gen(function* () {
371
397
  ),
372
398
  ),
373
399
  );
400
+
374
401
  if (decision._tag === "failure") return yield* decision.error;
402
+
375
403
  return decision.result;
376
404
  }),
377
405
  );
@@ -393,6 +421,7 @@ const makeThreadStore = Effect.gen(function* () {
393
421
  Effect.gen(function* () {
394
422
  const request = yield* validate(ThreadRead, "read", unvalidated);
395
423
  const records = yield* readSnapshot(request.threadId, request.afterSequence, request.limit);
424
+
396
425
  return Stream.fromIterable(records);
397
426
  }),
398
427
  );
@@ -402,16 +431,20 @@ const makeThreadStore = Effect.gen(function* () {
402
431
  Effect.gen(function* () {
403
432
  const request = yield* validate(ThreadObservation, "observe", unvalidated);
404
433
  const afterSequence = yield* offsetSequence(request.threadId, request.afterOffset);
434
+
405
435
  return Stream.unwrap(
406
436
  Effect.gen(function* () {
407
437
  const subscription = yield* PubSub.subscribe(updates);
438
+
408
439
  const initial = yield* readSnapshot(
409
440
  request.threadId,
410
441
  afterSequence,
411
442
  MAX_RECORDS_PER_THREAD,
412
443
  );
444
+
413
445
  const highWater =
414
446
  initial.length === 0 ? afterSequence : (initial.at(-1)?.sequence ?? afterSequence);
447
+
415
448
  const live = Stream.fromEffectRepeat(PubSub.take(subscription)).pipe(
416
449
  Stream.mapAccumEffect(
417
450
  () => highWater,
@@ -423,6 +456,7 @@ const makeThreadStore = Effect.gen(function* () {
423
456
  ),
424
457
  ),
425
458
  );
459
+
426
460
  return Stream.fromIterable(initial).pipe(Stream.concat(live));
427
461
  }),
428
462
  );
@@ -433,9 +467,11 @@ const makeThreadStore = Effect.gen(function* () {
433
467
  (unvalidated) =>
434
468
  Effect.gen(function* () {
435
469
  const request = yield* validate(ThreadExportRequest, "export", unvalidated);
470
+
436
471
  const thread = yield* Ref.get(state).pipe(
437
472
  Effect.flatMap((current) => findThread(current, request.threadId)),
438
473
  );
474
+
439
475
  return ThreadExport.make({
440
476
  format: "effect-agent/thread@1",
441
477
  threadId: request.threadId,
@@ -451,9 +487,11 @@ const makeThreadStore = Effect.gen(function* () {
451
487
  )((unvalidated) =>
452
488
  Effect.gen(function* () {
453
489
  const request = yield* validate(ThreadTailRequest, "inspectTail", unvalidated);
490
+
454
491
  const thread = yield* Ref.get(state).pipe(
455
492
  Effect.flatMap((current) => findThread(current, request.threadId)),
456
493
  );
494
+
457
495
  return ThreadTail.make({
458
496
  threadId: request.threadId,
459
497
  tailSequence: thread.tailSequence,
@@ -468,11 +506,13 @@ const makeThreadStore = Effect.gen(function* () {
468
506
  Effect.gen(function* () {
469
507
  yield* validateCheckpointVersion(unvalidated);
470
508
  const request = yield* validate(SaveCheckpointRequest, "saveCheckpoint", unvalidated);
509
+
471
510
  const decision = yield* Ref.modify(
472
511
  state,
473
512
  (current): readonly [CheckpointDecision, MemoryState] => {
474
513
  const checkpoint = request.checkpoint;
475
514
  const thread = current.threads.get(checkpoint.threadId);
515
+
476
516
  if (thread === undefined) {
477
517
  return [
478
518
  {
@@ -524,12 +564,16 @@ const makeThreadStore = Effect.gen(function* () {
524
564
  ];
525
565
  }
526
566
  const checkpoints = new Map(thread.checkpoints);
567
+
527
568
  checkpoints.set(checkpoint.throughSequence, checkpoint);
528
569
  const threads = new Map(current.threads);
570
+
529
571
  threads.set(checkpoint.threadId, { ...thread, checkpoints });
572
+
530
573
  return [{ _tag: "success" }, { threads }];
531
574
  },
532
575
  );
576
+
533
577
  if (decision._tag === "failure") return yield* decision.error;
534
578
  }),
535
579
  );
@@ -538,11 +582,14 @@ const makeThreadStore = Effect.gen(function* () {
538
582
  (unvalidated) =>
539
583
  Effect.gen(function* () {
540
584
  const request = yield* validate(LoadCheckpointRequest, "loadCheckpoint", unvalidated);
585
+
541
586
  const thread = yield* Ref.get(state).pipe(
542
587
  Effect.flatMap((current) => findThread(current, request.threadId)),
543
588
  );
589
+
544
590
  const maximum = request.atOrBeforeSequence ?? thread.tailSequence;
545
591
  let selected: ThreadCheckpoint | undefined;
592
+
546
593
  for (const [sequence, checkpoint] of thread.checkpoints) {
547
594
  if (
548
595
  sequence <= maximum &&
@@ -560,6 +607,7 @@ const makeThreadStore = Effect.gen(function* () {
560
607
  reason: "digest-mismatch",
561
608
  });
562
609
  }
610
+
563
611
  return Option.fromNullishOr(selected);
564
612
  }),
565
613
  );