@farukada/aws-langgraph-dynamodb-ts 0.4.0 → 0.6.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/README.md +3 -3
- package/dist/checkpointer/actions/put-writes.d.ts +4 -4
- package/dist/checkpointer/actions/put-writes.d.ts.map +1 -1
- package/dist/checkpointer/actions/put-writes.js +22 -25
- package/dist/checkpointer/actions/put-writes.js.map +1 -1
- package/dist/checkpointer/internal/item-writer.d.ts +18 -9
- package/dist/checkpointer/internal/item-writer.d.ts.map +1 -1
- package/dist/checkpointer/internal/item-writer.js +28 -11
- package/dist/checkpointer/internal/item-writer.js.map +1 -1
- package/dist/checkpointer/internal/special-write-cleanup.d.ts +18 -0
- package/dist/checkpointer/internal/special-write-cleanup.d.ts.map +1 -0
- package/dist/checkpointer/internal/special-write-cleanup.js +108 -0
- package/dist/checkpointer/internal/special-write-cleanup.js.map +1 -0
- package/dist/factory/factory.d.ts +2 -0
- package/dist/factory/factory.d.ts.map +1 -1
- package/dist/factory/factory.js.map +1 -1
- package/dist/history/actions/clear.js +1 -1
- package/dist/history/actions/clear.js.map +1 -1
- package/dist/history/internal/append-saga.js +24 -21
- package/dist/history/internal/append-saga.js.map +1 -1
- package/dist/history/internal/message-transaction.d.ts +11 -6
- package/dist/history/internal/message-transaction.d.ts.map +1 -1
- package/dist/history/internal/message-transaction.js +40 -13
- package/dist/history/internal/message-transaction.js.map +1 -1
- package/dist/history/internal/query.d.ts +5 -1
- package/dist/history/internal/query.d.ts.map +1 -1
- package/dist/history/internal/query.js +5 -2
- package/dist/history/internal/query.js.map +1 -1
- package/dist/history/internal/session-count.d.ts +20 -0
- package/dist/history/internal/session-count.d.ts.map +1 -0
- package/dist/history/internal/session-count.js +58 -0
- package/dist/history/internal/session-count.js.map +1 -0
- package/dist/history/internal/session-update.d.ts +4 -1
- package/dist/history/internal/session-update.d.ts.map +1 -1
- package/dist/history/internal/session-update.js +22 -2
- package/dist/history/internal/session-update.js.map +1 -1
- package/dist/shared/codec/s3/offloader.js +1 -1
- package/dist/shared/codec/s3/offloader.js.map +1 -1
- package/dist/shared/dynamodb/batch-write.d.ts +11 -1
- package/dist/shared/dynamodb/batch-write.d.ts.map +1 -1
- package/dist/shared/dynamodb/batch-write.js +22 -3
- package/dist/shared/dynamodb/batch-write.js.map +1 -1
- package/dist/shared/dynamodb/cancellation.d.ts +7 -0
- package/dist/shared/dynamodb/cancellation.d.ts.map +1 -0
- package/dist/shared/dynamodb/cancellation.js +8 -0
- package/dist/shared/dynamodb/cancellation.js.map +1 -0
- package/dist/shared/dynamodb/retry-classifier.d.ts.map +1 -1
- package/dist/shared/dynamodb/retry-classifier.js +2 -1
- package/dist/shared/dynamodb/retry-classifier.js.map +1 -1
- package/dist/shared/errors/errors.d.ts +6 -1
- package/dist/shared/errors/errors.d.ts.map +1 -1
- package/dist/shared/errors/errors.js +8 -2
- package/dist/shared/errors/errors.js.map +1 -1
- package/dist/store/actions/list-namespaces.d.ts.map +1 -1
- package/dist/store/actions/list-namespaces.js +10 -2
- package/dist/store/actions/list-namespaces.js.map +1 -1
- package/dist/store/actions/put.d.ts +5 -7
- package/dist/store/actions/put.d.ts.map +1 -1
- package/dist/store/actions/put.js +7 -28
- package/dist/store/actions/put.js.map +1 -1
- package/dist/store/internal/filter.d.ts.map +1 -1
- package/dist/store/internal/filter.js +6 -2
- package/dist/store/internal/filter.js.map +1 -1
- package/dist/store/internal/index-reconcile.d.ts.map +1 -1
- package/dist/store/internal/index-reconcile.js +1 -0
- package/dist/store/internal/index-reconcile.js.map +1 -1
- package/dist/store/internal/persist.d.ts +11 -0
- package/dist/store/internal/persist.d.ts.map +1 -0
- package/dist/store/internal/persist.js +38 -0
- package/dist/store/internal/persist.js.map +1 -0
- package/dist/store/internal/write-verify.d.ts +12 -0
- package/dist/store/internal/write-verify.d.ts.map +1 -0
- package/dist/store/internal/write-verify.js +28 -0
- package/dist/store/internal/write-verify.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ When `keyPrefix` is omitted, each adapter defaults to its own sub-prefix under t
|
|
|
204
204
|
|
|
205
205
|
**S3 offloading** — set `s3: { bucketName }`. Any serialized payload at or above `thresholdBytes` (default 350 KB) is written to S3, with only a reference stored in DynamoDB; reads rehydrate transparently. Requires the optional `@aws-sdk/client-s3` peer. Deleting a checkpoint thread / chat session also best-effort deletes its offloaded objects. When a `ttl` is also configured, call `ensureS3LifecycleRule()` once (e.g. during deployment) to best-effort install a matching S3 lifecycle expiration rule (logged, never fatal) — this is opt-in rather than automatic, since it requires the broader `s3:PutLifecycleConfiguration` bucket-level permission and is not safe to fire on every adapter construction. If you configure `ttl` + `s3` but never call it, nothing reclaims objects that best-effort cleanup misses (a failed delete, or a write that lost the checkpointer's first-write-wins race) — they stay in the bucket until you remove them or add a lifecycle rule yourself. The store has an analogous, smaller-scale race: two concurrent `put`s to the same key that both read the previous descriptor before either commits each upload to their own distinct (nonced) key and each then try to clean up that same previous descriptor — the loser's own upload becomes an orphan (bounded at one per race), the same class of leak, reclaimed the same way.
|
|
206
206
|
|
|
207
|
-
**TTL expiry** — set `ttl: { days }` or `ttl: { seconds }`. The `ttl` attribute is written as a Unix-epoch-seconds timestamp; enable DynamoDB TTL on the `ttl` attribute for automatic deletion. Chat history anchors a single **uniform whole-conversation TTL** on the session's metadata row, shared by every message: normally it's set once, at session creation, via `if_not_exists`; but if the previously-stored anchor is ever found missing or already expired (DynamoDB's own TTL sweep can lag up to ~48h), the next append heals it with a plain overwrite instead of staying stuck. Every message written at any point in time shares whatever the current anchor is; expired messages are also filtered out on read.
|
|
207
|
+
**TTL expiry** — set `ttl: { days }` or `ttl: { seconds }`. The `ttl` attribute is written as a Unix-epoch-seconds timestamp; enable DynamoDB TTL on the `ttl` attribute for automatic deletion. Chat history anchors a single **uniform whole-conversation TTL** on the session's metadata row, shared by every message: normally it's set once, at session creation, via `if_not_exists`; but if the previously-stored anchor is ever found missing or already expired (DynamoDB's own TTL sweep can lag up to ~48h), the next append heals it with a plain overwrite instead of staying stuck. Every message written at any point in time shares whatever the current anchor is; expired messages are also filtered out on read. If the append that triggers a stale-anchor heal is itself later rolled back (a later chunk in the same call failed), the healed ttl is not reverted — the session simply keeps the fresher, never-shorter expiry rather than risk regressing a value a concurrent legitimate extension may have since written; this is a deliberate, self-healing tradeoff, not a bug.
|
|
208
208
|
|
|
209
209
|
**Plain (metadata) search** (store) — a `search()` call with no `query` (or with a `query` but no `index`/`vectorBackend` configured) decodes every row under the `namespacePrefix` — applying `filter` in-process — before slicing to `offset`/`limit`. That full-namespace decode is bounded by `maxScanItems` (default 10,000, the same in-memory cap shared by every other paginated read in the library); exceeding it throws rather than silently returning a partial result. This is a different cap from `maxSearchCandidates` below: `maxScanItems` gates the plain in-memory scan, `maxSearchCandidates` gates the in-DB semantic ranker. Raise `maxScanItems` for a one-off oversized namespace, but for namespaces that routinely exceed the default, prefer a `vectorBackend` or a narrower `namespacePrefix` over raising the cap indefinitely.
|
|
210
210
|
|
|
@@ -230,9 +230,9 @@ try {
|
|
|
230
230
|
}
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
`ErrorCode` values: `VALIDATION`, `CONDITION_CONFLICT`, `RETRY_EXHAUSTED`, `BATCH_WRITE_INCOMPLETE`, `COMPRESSION_LIMIT`, `S3_OFFLOAD_FAILED`, `ABORTED`. Typed subclasses are exported where callers commonly branch: `ValidationError`, `ConflictError`, `RetryExhaustedError`, `BatchWriteIncompleteError`, `BatchWriteAllIncompleteError`, `AbortError`.
|
|
233
|
+
`ErrorCode` values: `VALIDATION`, `CONDITION_CONFLICT`, `RETRY_EXHAUSTED`, `BATCH_WRITE_INCOMPLETE`, `COMPRESSION_LIMIT`, `S3_OFFLOAD_FAILED`, `RESULT_TRUNCATED`, `ABORTED`, `COMPENSATION_FAILED`. Typed subclasses are exported where callers commonly branch: `ValidationError`, `ConflictError`, `RetryExhaustedError`, `BatchWriteIncompleteError`, `BatchWriteAllIncompleteError`, `ResultTruncatedError`, `AbortError`, `CompensationFailedError`.
|
|
234
234
|
|
|
235
|
-
`BatchWriteAllIncompleteError` is thrown by `deleteThread`, `clearSession`, `putWrites
|
|
235
|
+
`BatchWriteAllIncompleteError` is thrown directly by `deleteThread`, `clearSession`, and `putWrites` when a multi-chunk `BatchWriteItem` sequence doesn't fully drain. The chat-history append-rollback path can hit the same underlying failure while deleting a partially-committed batch's rows, but there it's never thrown directly — it surfaces as the `rollbackError` property of a `CompensationFailedError` (the append's original trigger error still needs reporting too), so check `err.rollbackError instanceof BatchWriteAllIncompleteError` there instead of `err instanceof BatchWriteAllIncompleteError`. Where the error may have crossed a package-copy boundary (e.g. a bundler duplicating this package), prefer `err.rollbackError.code === ErrorCode.BATCH_WRITE_INCOMPLETE` or `err.rollbackError.name === 'BatchWriteAllIncompleteError'` over `instanceof` — this library's own code avoids `instanceof` internally for the same reason. It carries `succeededChunks` / `totalChunks` / `failedChunks` / `succeededCount` so a caller can tell how much of the batch actually persisted before the failure, rather than just seeing the first chunk's raw error.
|
|
236
236
|
|
|
237
237
|
## Logging
|
|
238
238
|
|
|
@@ -6,10 +6,10 @@ import type { CheckpointerContext } from '../internal/setup';
|
|
|
6
6
|
* Requires `checkpoint_id` in the config — writes always attach to a checkpoint.
|
|
7
7
|
* Regular writes are first-write-wins (matching the reference checkpointer
|
|
8
8
|
* contract); special negative-index writes always overwrite (see
|
|
9
|
-
* {@link
|
|
10
|
-
* known never to have committed (see
|
|
11
|
-
*
|
|
12
|
-
* never be stranded pointing at a deleted object.
|
|
9
|
+
* {@link writeSpecialItemsWithCleanup}). Regular-write failure cleanup only
|
|
10
|
+
* ever targets items known never to have committed (see
|
|
11
|
+
* {@link RegularWriteOutcome}) — never a lost guard, so an upload can leak
|
|
12
|
+
* but a live row can never be stranded pointing at a deleted object.
|
|
13
13
|
*/
|
|
14
14
|
export declare function putWrites(context: CheckpointerContext, config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
|
|
15
15
|
//# sourceMappingURL=put-writes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"put-writes.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"put-writes.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AASpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAoF7D;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
|
|
@@ -4,18 +4,17 @@ exports.putWrites = putWrites;
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
|
|
6
6
|
const orphans_1 = require("../../shared/codec/s3/orphans");
|
|
7
|
-
const batch_write_1 = require("../../shared/dynamodb/batch-write");
|
|
8
7
|
const retry_1 = require("../../shared/dynamodb/retry");
|
|
9
8
|
const errors_1 = require("../../shared/errors/errors");
|
|
10
9
|
const ttl_1 = require("../../shared/validation/ttl");
|
|
11
10
|
const configurable_1 = require("../internal/configurable");
|
|
12
11
|
const item_writer_1 = require("../internal/item-writer");
|
|
12
|
+
const special_write_cleanup_1 = require("../internal/special-write-cleanup");
|
|
13
13
|
const validation_1 = require("../internal/validation");
|
|
14
14
|
/**
|
|
15
|
-
* True when the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* committed itself and fails identically; the two cases are indistinguishable.
|
|
15
|
+
* True when the guard rejected a write — NOT evidence a competitor won: a
|
|
16
|
+
* `PutCommand` retried after its response was lost can re-hit its own
|
|
17
|
+
* committed row and fail identically; the two cases are indistinguishable.
|
|
19
18
|
*/
|
|
20
19
|
function isConditionalCheckFailed(error) {
|
|
21
20
|
return error.name === 'ConditionalCheckFailedException';
|
|
@@ -26,21 +25,19 @@ async function cleanUpItems(context, items) {
|
|
|
26
25
|
return;
|
|
27
26
|
await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)(items.map((item) => item.value)), 'putWrites', context.logger);
|
|
28
27
|
}
|
|
29
|
-
/** Dedup by sort key (last-write-wins), matching LangGraph's special-write semantics. */
|
|
30
|
-
function dedupeBySortKey(items) {
|
|
31
|
-
return [...new Map(items.map((item) => [item.SK, item])).values()];
|
|
32
|
-
}
|
|
33
28
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* deleting now could strand a row a previous call already committed.
|
|
29
|
+
* Dedup writes to the same special channel (last-write-wins) before upload,
|
|
30
|
+
* so a discarded duplicate is never uploaded. A regular write's index comes
|
|
31
|
+
* from its position here (post-dedup), not the caller's raw array — stable
|
|
32
|
+
* across retries of an identical array, which is what first-write-wins needs.
|
|
39
33
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
function dedupeWritesByIndex(writes) {
|
|
35
|
+
const byIndex = new Map();
|
|
36
|
+
writes.forEach((write, positional) => {
|
|
37
|
+
const index = (0, item_writer_1.resolveWriteIndex)(write[0], positional);
|
|
38
|
+
byIndex.set(index, write);
|
|
39
|
+
});
|
|
40
|
+
return [...byIndex.values()];
|
|
44
41
|
}
|
|
45
42
|
/**
|
|
46
43
|
* Write regular items with a first-write-wins guard. Every `PutCommand` fully
|
|
@@ -73,10 +70,10 @@ async function writeRegularItems(context, items) {
|
|
|
73
70
|
* Requires `checkpoint_id` in the config — writes always attach to a checkpoint.
|
|
74
71
|
* Regular writes are first-write-wins (matching the reference checkpointer
|
|
75
72
|
* contract); special negative-index writes always overwrite (see
|
|
76
|
-
* {@link
|
|
77
|
-
* known never to have committed (see
|
|
78
|
-
*
|
|
79
|
-
* never be stranded pointing at a deleted object.
|
|
73
|
+
* {@link writeSpecialItemsWithCleanup}). Regular-write failure cleanup only
|
|
74
|
+
* ever targets items known never to have committed (see
|
|
75
|
+
* {@link RegularWriteOutcome}) — never a lost guard, so an upload can leak
|
|
76
|
+
* but a live row can never be stranded pointing at a deleted object.
|
|
80
77
|
*/
|
|
81
78
|
async function putWrites(context, config, writes, taskId) {
|
|
82
79
|
(0, validation_1.validateTaskId)(taskId);
|
|
@@ -87,11 +84,11 @@ async function putWrites(context, config, writes, taskId) {
|
|
|
87
84
|
if (writes.length === 0)
|
|
88
85
|
return;
|
|
89
86
|
const ttlTimestamp = context.ttl ? (0, ttl_1.calculateTtlTimestamp)(context.ttl) : undefined;
|
|
90
|
-
const items = await (0, item_writer_1.buildWriteItems)(context, threadId, checkpointNs, checkpointId, taskId, writes, (0, node_crypto_1.randomUUID)(), ttlTimestamp);
|
|
91
|
-
const special =
|
|
87
|
+
const items = await (0, item_writer_1.buildWriteItems)(context, threadId, checkpointNs, checkpointId, taskId, dedupeWritesByIndex(writes), (0, node_crypto_1.randomUUID)(), ttlTimestamp);
|
|
88
|
+
const special = items.filter((item) => item.index < 0);
|
|
92
89
|
const regular = items.filter((item) => item.index >= 0);
|
|
93
90
|
const [specialError, regularOutcome] = await Promise.all([
|
|
94
|
-
|
|
91
|
+
(0, special_write_cleanup_1.writeSpecialItemsWithCleanup)(context, special),
|
|
95
92
|
writeRegularItems(context, regular),
|
|
96
93
|
]);
|
|
97
94
|
const firstError = specialError ?? regularOutcome.error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"put-writes.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":";;;AAAA,6CAAyC;AAKzC,wEAAmE;AACnE,2DAAiE;AACjE,
|
|
1
|
+
{"version":3,"file":"put-writes.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,14 @@ import type { CheckpointMetaItem, CheckpointPayloadItem, CheckpointWriteItem } f
|
|
|
4
4
|
import type { CheckpointerContext } from './setup';
|
|
5
5
|
/** Map a context to the codec collaborators. */
|
|
6
6
|
export declare function codecDeps(context: CheckpointerContext): CodecDeps;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a write's DynamoDB sort-key index: WRITES_IDX_MAP's slot for a
|
|
9
|
+
* known special channel, else its position. `Object.hasOwn` guards against
|
|
10
|
+
* WRITES_IDX_MAP's own `Object.prototype` chain — a channel literally named
|
|
11
|
+
* `constructor`/`toString`/etc. must fall through to `positional`, not
|
|
12
|
+
* resolve to an inherited function reference.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveWriteIndex(channel: string, positional: number): number;
|
|
7
15
|
/** Encode a checkpoint + metadata into its META and PAYLOAD items. */
|
|
8
16
|
export declare function buildCheckpointItems(context: CheckpointerContext, threadId: string, checkpointNs: string, checkpoint: Checkpoint, metadata: CheckpointMetadata, parentCheckpointId?: string, ttlTimestamp?: number): Promise<{
|
|
9
17
|
meta: CheckpointMetaItem;
|
|
@@ -11,15 +19,16 @@ export declare function buildCheckpointItems(context: CheckpointerContext, threa
|
|
|
11
19
|
}>;
|
|
12
20
|
/**
|
|
13
21
|
* Encode a task's pending writes into one item per write. `nonce` must be
|
|
14
|
-
* unique per `putWrites` *call* (not per write) and is appended to
|
|
15
|
-
* offload keyParts
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* same
|
|
22
|
+
* unique per `putWrites` *call* (not per write) and is appended to every
|
|
23
|
+
* write's S3 offload keyParts — regular and special alike — so a repeated
|
|
24
|
+
* write (a retried task re-emitting the same channel, or two calls to the
|
|
25
|
+
* same special channel) never shares an S3 location with any earlier
|
|
26
|
+
* attempt. Special (negative-index) writes still overwrite their DynamoDB
|
|
27
|
+
* row in place (see {@link writeSpecialItemsWithCleanup}), but that
|
|
28
|
+
* overwrite-safety now comes from reading the previous descriptor before
|
|
29
|
+
* writing and only cleaning it up after the new row is confirmed committed —
|
|
30
|
+
* the same pattern store/actions/put.ts uses for the identical "overwrite in
|
|
31
|
+
* place, nonce every upload" shape.
|
|
23
32
|
*/
|
|
24
33
|
export declare function buildWriteItems(context: CheckpointerContext, threadId: string, checkpointNs: string, checkpointId: string, taskId: string, writes: PendingWrite[], nonce: string, ttlTimestamp?: number): Promise<CheckpointWriteItem[]>;
|
|
25
34
|
//# sourceMappingURL=item-writer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"item-writer.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpG,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAEjE;AAOD,sEAAsE;AACtE,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,kBAAkB,EAC5B,kBAAkB,CAAC,EAAE,MAAM,EAC3B,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAC,CAwBvE;AAED
|
|
1
|
+
{"version":3,"file":"item-writer.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpG,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAEjE;AAOD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,kBAAkB,EAC5B,kBAAkB,CAAC,EAAE,MAAM,EAC3B,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAC,CAwBvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EAAE,EACtB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA2BhC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.codecDeps = codecDeps;
|
|
4
|
+
exports.resolveWriteIndex = resolveWriteIndex;
|
|
4
5
|
exports.buildCheckpointItems = buildCheckpointItems;
|
|
5
6
|
exports.buildWriteItems = buildWriteItems;
|
|
6
7
|
const langgraph_checkpoint_1 = require("@langchain/langgraph-checkpoint");
|
|
@@ -15,6 +16,16 @@ function withTtl(item, ttlTimestamp) {
|
|
|
15
16
|
item.ttl = ttlTimestamp;
|
|
16
17
|
return item;
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a write's DynamoDB sort-key index: WRITES_IDX_MAP's slot for a
|
|
21
|
+
* known special channel, else its position. `Object.hasOwn` guards against
|
|
22
|
+
* WRITES_IDX_MAP's own `Object.prototype` chain — a channel literally named
|
|
23
|
+
* `constructor`/`toString`/etc. must fall through to `positional`, not
|
|
24
|
+
* resolve to an inherited function reference.
|
|
25
|
+
*/
|
|
26
|
+
function resolveWriteIndex(channel, positional) {
|
|
27
|
+
return Object.hasOwn(langgraph_checkpoint_1.WRITES_IDX_MAP, channel) ? langgraph_checkpoint_1.WRITES_IDX_MAP[channel] : positional;
|
|
28
|
+
}
|
|
18
29
|
/** Encode a checkpoint + metadata into its META and PAYLOAD items. */
|
|
19
30
|
async function buildCheckpointItems(context, threadId, checkpointNs, checkpoint, metadata, parentCheckpointId, ttlTimestamp) {
|
|
20
31
|
const deps = codecDeps(context);
|
|
@@ -44,15 +55,16 @@ async function buildCheckpointItems(context, threadId, checkpointNs, checkpoint,
|
|
|
44
55
|
}
|
|
45
56
|
/**
|
|
46
57
|
* Encode a task's pending writes into one item per write. `nonce` must be
|
|
47
|
-
* unique per `putWrites` *call* (not per write) and is appended to
|
|
48
|
-
* offload keyParts
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* same
|
|
58
|
+
* unique per `putWrites` *call* (not per write) and is appended to every
|
|
59
|
+
* write's S3 offload keyParts — regular and special alike — so a repeated
|
|
60
|
+
* write (a retried task re-emitting the same channel, or two calls to the
|
|
61
|
+
* same special channel) never shares an S3 location with any earlier
|
|
62
|
+
* attempt. Special (negative-index) writes still overwrite their DynamoDB
|
|
63
|
+
* row in place (see {@link writeSpecialItemsWithCleanup}), but that
|
|
64
|
+
* overwrite-safety now comes from reading the previous descriptor before
|
|
65
|
+
* writing and only cleaning it up after the new row is confirmed committed —
|
|
66
|
+
* the same pattern store/actions/put.ts uses for the identical "overwrite in
|
|
67
|
+
* place, nonce every upload" shape.
|
|
56
68
|
*/
|
|
57
69
|
async function buildWriteItems(context, threadId, checkpointNs, checkpointId, taskId, writes, nonce, ttlTimestamp) {
|
|
58
70
|
const deps = codecDeps(context);
|
|
@@ -60,10 +72,15 @@ async function buildWriteItems(context, threadId, checkpointNs, checkpointId, ta
|
|
|
60
72
|
const items = [];
|
|
61
73
|
for (let positional = 0; positional < writes.length; positional++) {
|
|
62
74
|
const [channel, value] = writes[positional];
|
|
63
|
-
|
|
75
|
+
/**
|
|
76
|
+
* `positional` indexes this (post-dedup) array, not the caller's original
|
|
77
|
+
* one — stable across retries of an identical array, which is all
|
|
78
|
+
* first-write-wins requires (see dedupeWritesByIndex).
|
|
79
|
+
*/
|
|
80
|
+
const index = resolveWriteIndex(channel, positional);
|
|
64
81
|
const baseKeyParts = [threadId, checkpointNs, checkpointId, taskId, `write-${index}`];
|
|
65
82
|
const descriptor = await (0, codec_1.encodePayload)(value, deps, {
|
|
66
|
-
keyParts:
|
|
83
|
+
keyParts: [...baseKeyParts, nonce],
|
|
67
84
|
});
|
|
68
85
|
const item = {
|
|
69
86
|
PK: pk,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"item-writer.js","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"item-writer.js","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CheckpointWriteItem } from '../types';
|
|
2
|
+
import type { CheckpointerContext } from './setup';
|
|
3
|
+
/**
|
|
4
|
+
* Write special (negative-index) items unconditionally — overwrite is
|
|
5
|
+
* correct there, matching every reference checkpointer implementation. When
|
|
6
|
+
* an offloader is configured, reads each item's previous descriptor first,
|
|
7
|
+
* so a settled outcome can clean up the correct side: the previous
|
|
8
|
+
* descriptor on success, each new upload on a confirmed non-commit (batch
|
|
9
|
+
* write failed, or the previous-descriptor read itself failed before the
|
|
10
|
+
* batch write was even attempted), neither when the outcome is genuinely
|
|
11
|
+
* ambiguous (see {@link splitSpecialOutcome}). Never rejects — a failed read
|
|
12
|
+
* is reported the same way a failed write is, via the return value —
|
|
13
|
+
* because the caller runs this concurrently with writeRegularItems via
|
|
14
|
+
* `Promise.all`, whose own regular-write cleanup depends on every branch of
|
|
15
|
+
* that `Promise.all` resolving rather than short-circuiting on a reject.
|
|
16
|
+
*/
|
|
17
|
+
export declare function writeSpecialItemsWithCleanup(context: CheckpointerContext, items: CheckpointWriteItem[]): Promise<Error | undefined>;
|
|
18
|
+
//# sourceMappingURL=special-write-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"special-write-cleanup.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/special-write-cleanup.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAiFnD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,mBAAmB,EAAE,GAC3B,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAoC5B"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeSpecialItemsWithCleanup = writeSpecialItemsWithCleanup;
|
|
4
|
+
const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
|
|
5
|
+
const orphans_1 = require("../../shared/codec/s3/orphans");
|
|
6
|
+
const batch_write_1 = require("../../shared/dynamodb/batch-write");
|
|
7
|
+
const retry_1 = require("../../shared/dynamodb/retry");
|
|
8
|
+
/**
|
|
9
|
+
* Read each special item's stored descriptor before it's overwritten —
|
|
10
|
+
* mirrors store/actions/put.ts's readExisting. Keyed by SK (unique per
|
|
11
|
+
* special channel per call).
|
|
12
|
+
*/
|
|
13
|
+
async function readPreviousDescriptors(context, items) {
|
|
14
|
+
const previous = new Map();
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
const result = await (0, retry_1.withDynamoDBRetry)(() => context.client.get({
|
|
17
|
+
TableName: context.tableName,
|
|
18
|
+
Key: { PK: item.PK, SK: item.SK },
|
|
19
|
+
ConsistentRead: true,
|
|
20
|
+
ProjectionExpression: '#v',
|
|
21
|
+
ExpressionAttributeNames: { '#v': 'value' },
|
|
22
|
+
}));
|
|
23
|
+
previous.set(item.SK, result.Item?.value);
|
|
24
|
+
}
|
|
25
|
+
return previous;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Split `items` by outcome after a failed batch write (always a
|
|
29
|
+
* BatchWriteAllIncompleteError — batchWriteAll's only throw): never-committed
|
|
30
|
+
* items are safe to clean up their own upload, every other item its previous
|
|
31
|
+
* one — unless a failed chunk's shape is unknown, in which case neither side
|
|
32
|
+
* is touched (a leak beats stranding a live row). Uses `.name`, not
|
|
33
|
+
* `instanceof` (banned repo-wide, see base-error.ts).
|
|
34
|
+
*/
|
|
35
|
+
function splitSpecialOutcome(items, error) {
|
|
36
|
+
const neverCommittedSks = new Set();
|
|
37
|
+
let everyChunkTracked = true;
|
|
38
|
+
for (const failedChunk of error.failedChunks) {
|
|
39
|
+
if (failedChunk.name !== 'BatchWriteIncompleteError') {
|
|
40
|
+
everyChunkTracked = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const request of failedChunk.unprocessed) {
|
|
44
|
+
/** Always a PutRequest echoing this submission — special writes never send DeleteRequests. */
|
|
45
|
+
const { SK } = request.PutRequest.Item;
|
|
46
|
+
neverCommittedSks.add(SK);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!everyChunkTracked)
|
|
50
|
+
return { committed: [], neverCommitted: [] };
|
|
51
|
+
return {
|
|
52
|
+
committed: items.filter((item) => !neverCommittedSks.has(item.SK)),
|
|
53
|
+
neverCommitted: items.filter((item) => neverCommittedSks.has(item.SK)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Best-effort delete S3 objects backing `descriptors`, if an offloader is configured. */
|
|
57
|
+
async function deleteDescriptors(context, descriptors, label) {
|
|
58
|
+
if (!context.offloader)
|
|
59
|
+
return;
|
|
60
|
+
await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)(descriptors), label, context.logger);
|
|
61
|
+
}
|
|
62
|
+
/** Best-effort delete the previous descriptors of `items` (now safely superseded). */
|
|
63
|
+
async function cleanUpPrevious(context, items, previous) {
|
|
64
|
+
const descriptors = items
|
|
65
|
+
.map((item) => previous.get(item.SK))
|
|
66
|
+
.filter((descriptor) => descriptor !== undefined);
|
|
67
|
+
await deleteDescriptors(context, descriptors, 'putWrites.special.previous');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write special (negative-index) items unconditionally — overwrite is
|
|
71
|
+
* correct there, matching every reference checkpointer implementation. When
|
|
72
|
+
* an offloader is configured, reads each item's previous descriptor first,
|
|
73
|
+
* so a settled outcome can clean up the correct side: the previous
|
|
74
|
+
* descriptor on success, each new upload on a confirmed non-commit (batch
|
|
75
|
+
* write failed, or the previous-descriptor read itself failed before the
|
|
76
|
+
* batch write was even attempted), neither when the outcome is genuinely
|
|
77
|
+
* ambiguous (see {@link splitSpecialOutcome}). Never rejects — a failed read
|
|
78
|
+
* is reported the same way a failed write is, via the return value —
|
|
79
|
+
* because the caller runs this concurrently with writeRegularItems via
|
|
80
|
+
* `Promise.all`, whose own regular-write cleanup depends on every branch of
|
|
81
|
+
* that `Promise.all` resolving rather than short-circuiting on a reject.
|
|
82
|
+
*/
|
|
83
|
+
async function writeSpecialItemsWithCleanup(context, items) {
|
|
84
|
+
if (items.length === 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
let previous = new Map();
|
|
87
|
+
if (context.offloader) {
|
|
88
|
+
try {
|
|
89
|
+
previous = await readPreviousDescriptors(context, items);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
await deleteDescriptors(context, items.map((item) => item.value), 'putWrites.special.newUpload');
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await (0, batch_write_1.batchWriteAll)(context.client, context.tableName, items.map((item) => ({ PutRequest: { Item: item } })));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const { committed, neverCommitted } = splitSpecialOutcome(items, error);
|
|
101
|
+
await deleteDescriptors(context, neverCommitted.map((item) => item.value), 'putWrites.special.newUpload');
|
|
102
|
+
await cleanUpPrevious(context, committed, previous);
|
|
103
|
+
return error;
|
|
104
|
+
}
|
|
105
|
+
await cleanUpPrevious(context, items, previous);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=special-write-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"special-write-cleanup.js","sourceRoot":"","sources":["../../../src/checkpointer/internal/special-write-cleanup.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DynamoDBClient, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import type { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
|
|
2
3
|
import { DynamoDBSaver } from '../checkpointer/saver';
|
|
3
4
|
import type { DynamoDBSaverOptions } from '../checkpointer/types';
|
|
4
5
|
import { DynamoDBChatMessageHistory } from '../history/chat-message-history';
|
|
@@ -8,6 +9,7 @@ import { DynamoDBStore } from '../store/store';
|
|
|
8
9
|
import type { DynamoDBStoreOptions } from '../store/types';
|
|
9
10
|
/** Shared client/logger defaults applied to every adapter the factory builds. */
|
|
10
11
|
export interface FactoryBaseOptions {
|
|
12
|
+
client?: DynamoDBDocument;
|
|
11
13
|
clientConfig?: DynamoDBClientConfig;
|
|
12
14
|
createClient?: (config: DynamoDBClientConfig) => DynamoDBClient;
|
|
13
15
|
logger?: Logger;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/factory/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/factory/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,kBAAkB,CAAC;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,cAAc,CAAC;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,cAAc,GAAG,cAAc,CAAC,CAAC;IAC9E,KAAK,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,cAAc,GAAG,cAAc,CAAC,CAAC;IAC9E,OAAO,EAAE,IAAI,CAAC,iCAAiC,EAAE,QAAQ,GAAG,cAAc,GAAG,cAAc,CAAC,CAAC;CAC9F;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,0BAA0B,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,GAAE,kBAAuB,EAAI;IAE9D,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAExD;IAED,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAExD;IAED,wBAAwB,CAAC,OAAO,EAAE,iCAAiC,GAAG,0BAA0B,CAE/F;IAED,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAiBpD;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/factory/factory.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/factory/factory.ts"],"names":[],"mappings":";;;AAGA,iDAAsD;AAEtD,0EAA6E;AAE7E,sDAAkE;AAElE,0CAA+C;AA0B/C;;;;GAIG;AACH;IAC+B,IAAI;IAAjC,YAA6B,IAAI,GAAuB,EAAE;oBAA7B,IAAI;IAA4B,CAAC;IAE9D,WAAW,CAAC,OAA6B;QACvC,OAAO,IAAI,qBAAa,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,WAAW,CAAC,OAA6B;QACvC,OAAO,IAAI,qBAAa,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,wBAAwB,CAAC,OAA0C;QACjE,OAAO,IAAI,iDAA0B,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,SAAS,CAAC,OAAyB;QACjC,MAAM,QAAQ,GAAG,IAAA,8BAAqB,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,qBAAa,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,MAAM,KAAK,GAAG,IAAI,qBAAa,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,MAAM,OAAO,GAAG,IAAI,iDAA0B,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7F,OAAO;YACL,KAAK;YACL,KAAK;YACL,OAAO;YACP,OAAO,EAAE,GAAG,EAAE;gBACZ,KAAK,CAAC,OAAO,EAAE,CAAC;gBAChB,KAAK,CAAC,OAAO,EAAE,CAAC;gBAChB,OAAO,CAAC,OAAO,EAAE,CAAC;gBAClB,QAAQ,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC;YAChC,CAAC;SACF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -29,7 +29,7 @@ async function clearSession(context, sessionId) {
|
|
|
29
29
|
const buffer = { keys: [], descriptors: [] };
|
|
30
30
|
const pages = (0, paginate_1.paginateQuery)({
|
|
31
31
|
client: context.client,
|
|
32
|
-
params: (0, query_1.sessionItemsQuery)(context.tableName, sessionId),
|
|
32
|
+
params: (0, query_1.sessionItemsQuery)(context.tableName, sessionId, { consistent: true }),
|
|
33
33
|
maxItems: Number.POSITIVE_INFINITY,
|
|
34
34
|
maxIterations: Number.POSITIVE_INFINITY,
|
|
35
35
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clear.js","sourceRoot":"","sources":["../../../src/history/actions/clear.ts"],"names":[],"mappings":";;;AACA,wEAAmE;AACnE,2DAAiE;AACjE,sDAAyD;AACzD,mEAAkE;AAClE,6DAA+D;AAC/D,6CAAsD;AAStD,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,MAAoB;IACtE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACrC,MAAM,IAAA,2BAAa,EACjB,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,SAAS,EACjB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CACvD,CAAC;IACF,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAA,0BAAgB,EACpB,OAAO,CAAC,SAAS,EACjB,IAAA,+BAAa,EAAC,MAAM,CAAC,WAAW,CAAC,EACjC,eAAe,EACf,OAAO,CAAC,MAAM,CACf,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,MAAM,CAAC,WAAW,GAAG,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,uBAAuB,OAAuB,EAAE,SAAiB;IAC3E,MAAM,MAAM,GAAiB,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAC3D,MAAM,KAAK,GAAG,IAAA,wBAAa,EAAC;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,IAAA,yBAAiB,EAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"clear.js","sourceRoot":"","sources":["../../../src/history/actions/clear.ts"],"names":[],"mappings":";;;AACA,wEAAmE;AACnE,2DAAiE;AACjE,sDAAyD;AACzD,mEAAkE;AAClE,6DAA+D;AAC/D,6CAAsD;AAStD,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,MAAoB;IACtE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACrC,MAAM,IAAA,2BAAa,EACjB,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,SAAS,EACjB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CACvD,CAAC;IACF,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAA,0BAAgB,EACpB,OAAO,CAAC,SAAS,EACjB,IAAA,+BAAa,EAAC,MAAM,CAAC,WAAW,CAAC,EACjC,eAAe,EACf,OAAO,CAAC,MAAM,CACf,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,MAAM,CAAC,WAAW,GAAG,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,uBAAuB,OAAuB,EAAE,SAAiB;IAC3E,MAAM,MAAM,GAAiB,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAC3D,MAAM,KAAK,GAAG,IAAA,wBAAa,EAAC;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,IAAA,yBAAiB,EAAC,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QAC7E,QAAQ,EAAE,MAAM,CAAC,iBAAiB;QAClC,aAAa,EAAE,MAAM,CAAC,iBAAiB;KACxC,CAAC,CAAC;IACH,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,GAAsB,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/C,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,2BAAe;YAAE,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AACrC,CAAC"}
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.appendChunks = appendChunks;
|
|
4
|
-
const node_crypto_1 = require("node:crypto");
|
|
5
4
|
const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
|
|
6
5
|
const orphans_1 = require("../../shared/codec/s3/orphans");
|
|
7
6
|
const batch_write_1 = require("../../shared/dynamodb/batch-write");
|
|
8
|
-
const retry_1 = require("../../shared/dynamodb/retry");
|
|
9
7
|
const errors_1 = require("../../shared/errors/errors");
|
|
10
8
|
const wrap_error_1 = require("../../shared/errors/wrap-error");
|
|
11
|
-
const keys_1 = require("./keys");
|
|
12
9
|
const message_transaction_1 = require("./message-transaction");
|
|
13
|
-
|
|
14
|
-
async function revertSessionCount(context, sessionId, delta) {
|
|
15
|
-
if (delta === 0)
|
|
16
|
-
return;
|
|
17
|
-
const update = {
|
|
18
|
-
TableName: context.tableName,
|
|
19
|
-
Key: { PK: (0, keys_1.sessionPartition)(sessionId), SK: keys_1.SESSION_SORT_KEY },
|
|
20
|
-
UpdateExpression: 'ADD #count :neg',
|
|
21
|
-
ExpressionAttributeNames: { '#count': 'messageCount' },
|
|
22
|
-
ExpressionAttributeValues: { ':neg': -delta },
|
|
23
|
-
};
|
|
24
|
-
const input = { TransactItems: [{ Update: update }], ClientRequestToken: (0, node_crypto_1.randomUUID)() };
|
|
25
|
-
await (0, retry_1.withDynamoDBRetry)(() => context.client.transactWrite(input));
|
|
26
|
-
}
|
|
10
|
+
const session_count_1 = require("./session-count");
|
|
27
11
|
/**
|
|
28
12
|
* Best-effort delete the offloaded S3 objects of the `chunks` slice given.
|
|
29
13
|
* {@link compensate} calls it once per commit status, never for the whole
|
|
@@ -35,14 +19,33 @@ async function cleanBatchS3(context, chunks) {
|
|
|
35
19
|
const descriptors = chunks.flat().map((item) => item.message);
|
|
36
20
|
await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)(descriptors), 'history.addMessages', context.logger);
|
|
37
21
|
}
|
|
38
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Delete every committed chunk's items, then revert the counted total by
|
|
24
|
+
* however many rows the delete actually persisted before failing (not
|
|
25
|
+
* necessarily every committed row — see the catch branch below).
|
|
26
|
+
*/
|
|
39
27
|
async function rollbackCommitted(context, sessionId, committed) {
|
|
40
28
|
const keys = committed.flatMap((chunk) => chunk.keys);
|
|
41
|
-
|
|
29
|
+
const total = committed.reduce((sum, chunk) => sum + chunk.count, 0);
|
|
30
|
+
if (keys.length === 0) {
|
|
31
|
+
await (0, session_count_1.revertSessionCount)(context, sessionId, total);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
42
35
|
await (0, batch_write_1.batchWriteAll)(context.client, context.tableName, keys.map((Key) => ({ DeleteRequest: { Key } })));
|
|
43
36
|
}
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
catch (error) {
|
|
38
|
+
/**
|
|
39
|
+
* batchWriteAll has exactly one throw site and it is always a
|
|
40
|
+
* BatchWriteAllIncompleteError (see Task 9) — asserted, not instanceof-
|
|
41
|
+
* checked, since the false case is unreachable and this project
|
|
42
|
+
* enforces 100% branch coverage with no exceptions.
|
|
43
|
+
*/
|
|
44
|
+
const deleted = error.succeededCount;
|
|
45
|
+
await (0, session_count_1.revertSessionCount)(context, sessionId, deleted);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
await (0, session_count_1.revertSessionCount)(context, sessionId, total);
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* Undo a failed batch. Always throws. S3 cleanup is split by commit status so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"append-saga.js","sourceRoot":"","sources":["../../../src/history/internal/append-saga.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"append-saga.js","sourceRoot":"","sources":["../../../src/history/internal/append-saga.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,12 +10,17 @@ export interface ChunkRetryOptions {
|
|
|
10
10
|
* Atomically write a chunk of message items together with the session-metadata
|
|
11
11
|
* count update in one {@link https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html | TransactWriteItems}
|
|
12
12
|
* call, so `messageCount` can never disagree with the messages that landed. A
|
|
13
|
-
* single `ClientRequestToken` is
|
|
14
|
-
* after a lost response) is idempotent and never double-applies the count
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* single `ClientRequestToken` is used per attempt so a re-sent commit (e.g.
|
|
14
|
+
* after a lost response) is idempotent and never double-applies the count
|
|
15
|
+
* `ADD`. When `fields.forceTtlRefresh` is set, the session update carries a
|
|
16
|
+
* monotonic ConditionExpression (see session-update.ts); if — and only if —
|
|
17
|
+
* that specific condition loses a race against a concurrent caller who just
|
|
18
|
+
* healed the same anchor, this retries the identical chunk once with
|
|
19
|
+
* `forceTtlRefresh: false` (safe: `if_not_exists` then converges to whatever
|
|
20
|
+
* already won) rather than losing the message writes to a benign ttl race. A
|
|
21
|
+
* cancellation caused by any other item (a genuine message-row conflict) is
|
|
22
|
+
* not retried here — it propagates for the normal transient-conflict retry
|
|
23
|
+
* budget inside `withDynamoDBRetry` to handle, or to the caller otherwise.
|
|
19
24
|
*/
|
|
20
25
|
export declare function writeMessageChunk(context: HistoryContext, items: ChatMessageItem[], fields: SessionUpdateFields, retry?: ChunkRetryOptions): Promise<void>;
|
|
21
26
|
//# sourceMappingURL=message-transaction.d.ts.map
|