@farukada/aws-langgraph-dynamodb-ts 0.7.0 → 0.9.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 +52 -9
- package/dist/checkpointer/actions/delete-thread.d.ts +3 -4
- package/dist/checkpointer/actions/delete-thread.d.ts.map +1 -1
- package/dist/checkpointer/actions/delete-thread.js +21 -41
- package/dist/checkpointer/actions/delete-thread.js.map +1 -1
- package/dist/checkpointer/actions/list.d.ts.map +1 -1
- package/dist/checkpointer/actions/list.js +38 -4
- package/dist/checkpointer/actions/list.js.map +1 -1
- package/dist/checkpointer/actions/put-writes.d.ts.map +1 -1
- package/dist/checkpointer/actions/put-writes.js +15 -24
- package/dist/checkpointer/actions/put-writes.js.map +1 -1
- package/dist/checkpointer/internal/item-reader.d.ts +45 -0
- package/dist/checkpointer/internal/item-reader.d.ts.map +1 -1
- package/dist/checkpointer/internal/item-reader.js +63 -1
- package/dist/checkpointer/internal/item-reader.js.map +1 -1
- package/dist/checkpointer/internal/item-writer.d.ts +0 -8
- package/dist/checkpointer/internal/item-writer.d.ts.map +1 -1
- package/dist/checkpointer/internal/item-writer.js +16 -21
- package/dist/checkpointer/internal/item-writer.js.map +1 -1
- package/dist/checkpointer/internal/keys.d.ts +19 -3
- package/dist/checkpointer/internal/keys.d.ts.map +1 -1
- package/dist/checkpointer/internal/keys.js +41 -6
- package/dist/checkpointer/internal/keys.js.map +1 -1
- package/dist/checkpointer/internal/special-write-cas.d.ts +30 -0
- package/dist/checkpointer/internal/special-write-cas.d.ts.map +1 -0
- package/dist/checkpointer/internal/special-write-cas.js +104 -0
- package/dist/checkpointer/internal/special-write-cas.js.map +1 -0
- package/dist/checkpointer/internal/special-write-cleanup.d.ts +18 -12
- package/dist/checkpointer/internal/special-write-cleanup.d.ts.map +1 -1
- package/dist/checkpointer/internal/special-write-cleanup.js +31 -86
- package/dist/checkpointer/internal/special-write-cleanup.js.map +1 -1
- package/dist/checkpointer/internal/special-write-verify.d.ts +54 -0
- package/dist/checkpointer/internal/special-write-verify.d.ts.map +1 -0
- package/dist/checkpointer/internal/special-write-verify.js +65 -0
- package/dist/checkpointer/internal/special-write-verify.js.map +1 -0
- package/dist/checkpointer/internal/validation.d.ts +8 -4
- package/dist/checkpointer/internal/validation.d.ts.map +1 -1
- package/dist/checkpointer/internal/validation.js +12 -10
- package/dist/checkpointer/internal/validation.js.map +1 -1
- package/dist/checkpointer/internal/write-guard.d.ts +13 -0
- package/dist/checkpointer/internal/write-guard.d.ts.map +1 -0
- package/dist/checkpointer/internal/write-guard.js +39 -0
- package/dist/checkpointer/internal/write-guard.js.map +1 -0
- package/dist/checkpointer/internal/write-index.d.ts +37 -0
- package/dist/checkpointer/internal/write-index.d.ts.map +1 -0
- package/dist/checkpointer/internal/write-index.js +42 -0
- package/dist/checkpointer/internal/write-index.js.map +1 -0
- package/dist/checkpointer/types.d.ts +8 -0
- package/dist/checkpointer/types.d.ts.map +1 -1
- package/dist/history/actions/add-messages.d.ts.map +1 -1
- package/dist/history/actions/add-messages.js +24 -4
- package/dist/history/actions/add-messages.js.map +1 -1
- package/dist/history/actions/clear.d.ts +4 -6
- package/dist/history/actions/clear.d.ts.map +1 -1
- package/dist/history/actions/clear.js +18 -33
- package/dist/history/actions/clear.js.map +1 -1
- package/dist/history/actions/get-messages.d.ts +2 -1
- package/dist/history/actions/get-messages.d.ts.map +1 -1
- package/dist/history/actions/get-messages.js +28 -2
- package/dist/history/actions/get-messages.js.map +1 -1
- package/dist/history/actions/list-sessions.d.ts +2 -1
- package/dist/history/actions/list-sessions.d.ts.map +1 -1
- package/dist/history/actions/list-sessions.js +21 -2
- package/dist/history/actions/list-sessions.js.map +1 -1
- package/dist/history/actions/reconcile-count.d.ts.map +1 -1
- package/dist/history/actions/reconcile-count.js +2 -2
- package/dist/history/actions/reconcile-count.js.map +1 -1
- package/dist/history/internal/append-saga.d.ts.map +1 -1
- package/dist/history/internal/append-saga.js +2 -78
- package/dist/history/internal/append-saga.js.map +1 -1
- package/dist/history/internal/compensation.d.ts +21 -0
- package/dist/history/internal/compensation.d.ts.map +1 -0
- package/dist/history/internal/compensation.js +84 -0
- package/dist/history/internal/compensation.js.map +1 -0
- package/dist/history/internal/keys.d.ts +9 -1
- package/dist/history/internal/keys.d.ts.map +1 -1
- package/dist/history/internal/keys.js +21 -4
- package/dist/history/internal/keys.js.map +1 -1
- package/dist/history/internal/message-chunker.d.ts.map +1 -1
- package/dist/history/internal/message-chunker.js +13 -5
- package/dist/history/internal/message-chunker.js.map +1 -1
- package/dist/history/internal/session-count.d.ts +21 -0
- package/dist/history/internal/session-count.d.ts.map +1 -1
- package/dist/history/internal/session-count.js +51 -0
- package/dist/history/internal/session-count.js.map +1 -1
- package/dist/history/internal/session-title.d.ts +20 -0
- package/dist/history/internal/session-title.d.ts.map +1 -0
- package/dist/history/internal/session-title.js +44 -0
- package/dist/history/internal/session-title.js.map +1 -0
- package/dist/history/internal/setup.d.ts +2 -1
- package/dist/history/internal/setup.d.ts.map +1 -1
- package/dist/history/internal/setup.js +1 -0
- package/dist/history/internal/setup.js.map +1 -1
- package/dist/history/internal/title-generator.d.ts +8 -2
- package/dist/history/internal/title-generator.d.ts.map +1 -1
- package/dist/history/internal/title-generator.js +12 -3
- package/dist/history/internal/title-generator.js.map +1 -1
- package/dist/history/internal/validation.d.ts +9 -0
- package/dist/history/internal/validation.d.ts.map +1 -0
- package/dist/history/internal/validation.js +16 -0
- package/dist/history/internal/validation.js.map +1 -0
- package/dist/history/types.d.ts +12 -0
- package/dist/history/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/shared/codec/s3/offloader.d.ts +7 -1
- package/dist/shared/codec/s3/offloader.d.ts.map +1 -1
- package/dist/shared/codec/s3/offloader.js +14 -1
- package/dist/shared/codec/s3/offloader.js.map +1 -1
- package/dist/shared/constants.d.ts +15 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +16 -1
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/dynamodb/conditional-put.d.ts +51 -0
- package/dist/shared/dynamodb/conditional-put.d.ts.map +1 -0
- package/dist/shared/dynamodb/conditional-put.js +59 -0
- package/dist/shared/dynamodb/conditional-put.js.map +1 -0
- package/dist/shared/dynamodb/partition-delete.d.ts +33 -0
- package/dist/shared/dynamodb/partition-delete.d.ts.map +1 -0
- package/dist/shared/dynamodb/partition-delete.js +70 -0
- package/dist/shared/dynamodb/partition-delete.js.map +1 -0
- package/dist/shared/dynamodb/retry-classifier.d.ts.map +1 -1
- package/dist/shared/dynamodb/retry-classifier.js +9 -1
- package/dist/shared/dynamodb/retry-classifier.js.map +1 -1
- package/dist/shared/logging/redaction-walk.d.ts +23 -0
- package/dist/shared/logging/redaction-walk.d.ts.map +1 -0
- package/dist/shared/logging/redaction-walk.js +92 -0
- package/dist/shared/logging/redaction-walk.js.map +1 -0
- package/dist/shared/logging/redaction.d.ts +26 -17
- package/dist/shared/logging/redaction.d.ts.map +1 -1
- package/dist/shared/logging/redaction.js +24 -48
- package/dist/shared/logging/redaction.js.map +1 -1
- package/dist/shared/logging/secret-patterns.d.ts +74 -0
- package/dist/shared/logging/secret-patterns.d.ts.map +1 -0
- package/dist/shared/logging/secret-patterns.js +107 -0
- package/dist/shared/logging/secret-patterns.js.map +1 -0
- package/dist/shared/validation/primitives.d.ts +9 -1
- package/dist/shared/validation/primitives.d.ts.map +1 -1
- package/dist/shared/validation/primitives.js +13 -0
- package/dist/shared/validation/primitives.js.map +1 -1
- package/dist/store/actions/get.d.ts.map +1 -1
- package/dist/store/actions/get.js +15 -1
- package/dist/store/actions/get.js.map +1 -1
- package/dist/store/actions/list-namespaces.d.ts.map +1 -1
- package/dist/store/actions/list-namespaces.js +1 -0
- package/dist/store/actions/list-namespaces.js.map +1 -1
- package/dist/store/actions/put.d.ts.map +1 -1
- package/dist/store/actions/put.js +24 -18
- package/dist/store/actions/put.js.map +1 -1
- package/dist/store/actions/search.d.ts.map +1 -1
- package/dist/store/actions/search.js +4 -35
- package/dist/store/actions/search.js.map +1 -1
- package/dist/store/internal/backend-search.d.ts +5 -0
- package/dist/store/internal/backend-search.d.ts.map +1 -0
- package/dist/store/internal/backend-search.js +68 -0
- package/dist/store/internal/backend-search.js.map +1 -0
- package/dist/store/internal/filter.d.ts.map +1 -1
- package/dist/store/internal/filter.js +46 -5
- 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 +41 -5
- package/dist/store/internal/index-reconcile.js.map +1 -1
- package/dist/store/internal/item-mapper.d.ts +1 -0
- package/dist/store/internal/item-mapper.d.ts.map +1 -1
- package/dist/store/internal/item-mapper.js +2 -0
- package/dist/store/internal/item-mapper.js.map +1 -1
- package/dist/store/internal/keys.d.ts +1 -1
- package/dist/store/internal/keys.d.ts.map +1 -1
- package/dist/store/internal/keys.js +8 -2
- package/dist/store/internal/keys.js.map +1 -1
- package/dist/store/internal/overwrite-swap.d.ts +33 -0
- package/dist/store/internal/overwrite-swap.d.ts.map +1 -0
- package/dist/store/internal/overwrite-swap.js +62 -0
- package/dist/store/internal/overwrite-swap.js.map +1 -0
- package/dist/store/internal/persist.d.ts +22 -6
- package/dist/store/internal/persist.d.ts.map +1 -1
- package/dist/store/internal/persist.js +41 -20
- package/dist/store/internal/persist.js.map +1 -1
- package/dist/store/internal/read-existing.d.ts +19 -0
- package/dist/store/internal/read-existing.d.ts.map +1 -0
- package/dist/store/internal/read-existing.js +29 -0
- package/dist/store/internal/read-existing.js.map +1 -0
- package/dist/store/internal/score-direction.d.ts +32 -0
- package/dist/store/internal/score-direction.d.ts.map +1 -0
- package/dist/store/internal/score-direction.js +39 -0
- package/dist/store/internal/score-direction.js.map +1 -0
- package/dist/store/internal/search-filter.d.ts +4 -0
- package/dist/store/internal/search-filter.d.ts.map +1 -0
- package/dist/store/internal/search-filter.js +11 -0
- package/dist/store/internal/search-filter.js.map +1 -0
- package/dist/store/internal/setup.d.ts +13 -1
- package/dist/store/internal/setup.d.ts.map +1 -1
- package/dist/store/internal/setup.js +57 -1
- package/dist/store/internal/setup.js.map +1 -1
- package/dist/store/internal/validation.d.ts +8 -2
- package/dist/store/internal/validation.d.ts.map +1 -1
- package/dist/store/internal/validation.js +15 -6
- package/dist/store/internal/validation.js.map +1 -1
- package/dist/store/internal/write-verify.d.ts +31 -6
- package/dist/store/internal/write-verify.d.ts.map +1 -1
- package/dist/store/internal/write-verify.js +48 -8
- package/dist/store/internal/write-verify.js.map +1 -1
- package/dist/store/types.d.ts +16 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/vector-backend.d.ts +17 -0
- package/dist/store/vector-backend.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StoreItemRecord } from '../types';
|
|
2
|
+
import { type ExistingRecordMeta } from './read-existing';
|
|
3
|
+
import type { StoreContext } from './setup';
|
|
4
|
+
/**
|
|
5
|
+
* Commit `record`, re-reading and retrying while another writer holds the row,
|
|
6
|
+
* and return the state this write actually superseded — the only descriptor
|
|
7
|
+
* safe to delete afterwards.
|
|
8
|
+
*
|
|
9
|
+
* Without the swap both racers read the same previous descriptor, both commit,
|
|
10
|
+
* and both delete it, orphaning the loser's own upload. Retrying against the
|
|
11
|
+
* *re-read* state is what makes each writer supersede exactly one payload.
|
|
12
|
+
*
|
|
13
|
+
* A rejection is not proof a competitor won: `withDynamoDBRetry` retries
|
|
14
|
+
* transient errors, so an attempt can commit server-side, its response can be
|
|
15
|
+
* lost, and the retried put can hit the row it just wrote and fail the same
|
|
16
|
+
* guard — indistinguishable from a competitor's win by the rejection alone.
|
|
17
|
+
* Each attempt's pinned observation is captured in `attempted` before the
|
|
18
|
+
* put, so that when a re-read finds the row already holding *this call's
|
|
19
|
+
* own* `rev`, the swap returns whatever `attempted` held — never this
|
|
20
|
+
* record's own just-committed value, which would strand the live row
|
|
21
|
+
* pointing at a deleted object. That comparison is guarded on `rev` being
|
|
22
|
+
* set: `rev` is optional on the record type, and an unnonced record against a
|
|
23
|
+
* pre-0.9.0 revision-less row would otherwise match `undefined === undefined`
|
|
24
|
+
* and claim a race it never entered.
|
|
25
|
+
*
|
|
26
|
+
* On exhaustion the write proceeds unconditionally and warns. That is
|
|
27
|
+
* deliberate: the fallback is precisely the pre-0.9.0 behaviour — one possible
|
|
28
|
+
* orphan, reclaimed by a lifecycle rule — so pathological contention degrades
|
|
29
|
+
* instead of turning a working put into an error. `createdAt` is refreshed from
|
|
30
|
+
* each re-read so a row created by whoever won keeps its true creation time.
|
|
31
|
+
*/
|
|
32
|
+
export declare function putWithRevisionSwap(context: StoreContext, record: StoreItemRecord, existing: ExistingRecordMeta): Promise<ExistingRecordMeta>;
|
|
33
|
+
//# sourceMappingURL=overwrite-swap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overwrite-swap.d.ts","sourceRoot":"","sources":["../../../src/store/internal/overwrite-swap.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,KAAK,kBAAkB,EAAgB,MAAM,iBAAiB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAc5C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAqB7B"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.putWithRevisionSwap = putWithRevisionSwap;
|
|
4
|
+
const conditional_put_1 = require("../../shared/dynamodb/conditional-put");
|
|
5
|
+
const retry_1 = require("../../shared/dynamodb/retry");
|
|
6
|
+
const read_existing_1 = require("./read-existing");
|
|
7
|
+
/** Put the record, optionally pinned to the revision the caller observed. */
|
|
8
|
+
async function put(context, record, observed) {
|
|
9
|
+
const guard = observed ? (0, conditional_put_1.revisionGuard)(conditional_put_1.REVISION_ATTRIBUTE, observed) : {};
|
|
10
|
+
await (0, retry_1.withDynamoDBRetry)(() => context.client.put({ TableName: context.tableName, Item: record, ...guard }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Commit `record`, re-reading and retrying while another writer holds the row,
|
|
14
|
+
* and return the state this write actually superseded — the only descriptor
|
|
15
|
+
* safe to delete afterwards.
|
|
16
|
+
*
|
|
17
|
+
* Without the swap both racers read the same previous descriptor, both commit,
|
|
18
|
+
* and both delete it, orphaning the loser's own upload. Retrying against the
|
|
19
|
+
* *re-read* state is what makes each writer supersede exactly one payload.
|
|
20
|
+
*
|
|
21
|
+
* A rejection is not proof a competitor won: `withDynamoDBRetry` retries
|
|
22
|
+
* transient errors, so an attempt can commit server-side, its response can be
|
|
23
|
+
* lost, and the retried put can hit the row it just wrote and fail the same
|
|
24
|
+
* guard — indistinguishable from a competitor's win by the rejection alone.
|
|
25
|
+
* Each attempt's pinned observation is captured in `attempted` before the
|
|
26
|
+
* put, so that when a re-read finds the row already holding *this call's
|
|
27
|
+
* own* `rev`, the swap returns whatever `attempted` held — never this
|
|
28
|
+
* record's own just-committed value, which would strand the live row
|
|
29
|
+
* pointing at a deleted object. That comparison is guarded on `rev` being
|
|
30
|
+
* set: `rev` is optional on the record type, and an unnonced record against a
|
|
31
|
+
* pre-0.9.0 revision-less row would otherwise match `undefined === undefined`
|
|
32
|
+
* and claim a race it never entered.
|
|
33
|
+
*
|
|
34
|
+
* On exhaustion the write proceeds unconditionally and warns. That is
|
|
35
|
+
* deliberate: the fallback is precisely the pre-0.9.0 behaviour — one possible
|
|
36
|
+
* orphan, reclaimed by a lifecycle rule — so pathological contention degrades
|
|
37
|
+
* instead of turning a working put into an error. `createdAt` is refreshed from
|
|
38
|
+
* each re-read so a row created by whoever won keeps its true creation time.
|
|
39
|
+
*/
|
|
40
|
+
async function putWithRevisionSwap(context, record, existing) {
|
|
41
|
+
let observed = existing;
|
|
42
|
+
for (let attempt = 1; attempt <= conditional_put_1.OVERWRITE_CAS_MAX_ATTEMPTS; attempt++) {
|
|
43
|
+
const attempted = observed;
|
|
44
|
+
try {
|
|
45
|
+
await put(context, record, attempted);
|
|
46
|
+
return attempted;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!(0, conditional_put_1.isConditionalCheckFailed)(error))
|
|
50
|
+
throw error;
|
|
51
|
+
observed = await (0, read_existing_1.readExisting)(context, record.PK, record.SK);
|
|
52
|
+
if (record.rev !== undefined && observed.revision === record.rev)
|
|
53
|
+
return attempted;
|
|
54
|
+
record.createdAt = observed.createdAt ?? record.createdAt;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
context.logger.warn('store.put: compare-and-swap exhausted; overwriting unconditionally, which can orphan one ' +
|
|
58
|
+
'S3 object under a concurrent put (reclaimed by ensureS3LifecycleRule)', { namespace: record.namespace, key: record.key, attempts: conditional_put_1.OVERWRITE_CAS_MAX_ATTEMPTS });
|
|
59
|
+
await put(context, record);
|
|
60
|
+
return observed;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=overwrite-swap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overwrite-swap.js","sourceRoot":"","sources":["../../../src/store/internal/overwrite-swap.ts"],"names":[],"mappings":";;;AAAA,2EAK+C;AAC/C,uDAAgE;AAEhE,mDAAwE;AAGxE,6EAA6E;AAC7E,KAAK,UAAU,GAAG,CAChB,OAAqB,EACrB,MAAuB,EACvB,QAA6B;IAE7B,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAA,+BAAa,EAAC,oCAAkB,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,MAAM,IAAA,yBAAiB,EAAC,GAAG,EAAE,CAC3B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC,CAC7E,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACI,KAAK,8BACV,OAAqB,EACrB,MAAuB,EACvB,QAA4B;IAE5B,IAAI,QAAQ,GAAG,QAAQ,CAAC;IACxB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,4CAA0B,EAAE,OAAO,EAAE,EAAE,CAAC;QACvE,MAAM,SAAS,GAAG,QAAQ,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;YACtC,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAA,0CAAwB,EAAC,KAA0B,CAAC;gBAAE,MAAM,KAAK,CAAC;YACvE,QAAQ,GAAG,MAAM,IAAA,4BAAY,EAAC,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;YAC7D,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,CAAC,GAAG;gBAAE,OAAO,SAAS,CAAC;YACnF,MAAM,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,IAAI,CACjB,2FAA2F;QACzF,uEAAuE,EACzE,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,4CAA0B,EAAE,CACvF,CAAC;IACF,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import { type PayloadDescriptor } from '../../shared/codec/codec';
|
|
2
1
|
import type { StoreItemRecord } from '../types';
|
|
2
|
+
import type { ExistingRecordMeta } from './read-existing';
|
|
3
3
|
import type { StoreContext } from './setup';
|
|
4
4
|
/**
|
|
5
|
-
* Put the record
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Put the record and clean up whichever side is now dead.
|
|
6
|
+
*
|
|
7
|
+
* The compare-and-swap path runs **only when an offloader is configured**:
|
|
8
|
+
* without one there is no S3 object to orphan, so a plain last-write-wins put
|
|
9
|
+
* stays correct and costs no extra write capacity (DynamoDB charges for a
|
|
10
|
+
* failed conditional write too). With one, the swap is what lets this call
|
|
11
|
+
* delete exactly the payload it superseded rather than a descriptor a racer may
|
|
12
|
+
* already have replaced.
|
|
13
|
+
*
|
|
14
|
+
* Every failure reaching the catch arrives after at least one put was issued —
|
|
15
|
+
* `putWithRevisionSwap` only re-reads from inside its own catch — so none of
|
|
16
|
+
* them proves a non-commit on its own: a put can commit server-side and lose
|
|
17
|
+
* its response, and a `ConditionalCheckFailedException` is as consistent with
|
|
18
|
+
* hitting the row this call just wrote as with a competitor's win. The row is
|
|
19
|
+
* therefore read back (`verifyWriteLanded`) before anything is deleted. Only a
|
|
20
|
+
* confirmed `'not-landed'` deletes this record's own nonced object; a confirmed
|
|
21
|
+
* `'landed'` cleans up the previous object like the success path and swallows
|
|
22
|
+
* the error, and an `'unverified'` read deletes nothing and rethrows — leaking
|
|
23
|
+
* one object at worst rather than stranding a live row pointing at a deleted
|
|
24
|
+
* one. An inline payload has no object to leak, so it needs no read.
|
|
9
25
|
*/
|
|
10
|
-
export declare function persistRecord(context: StoreContext, record: StoreItemRecord,
|
|
26
|
+
export declare function persistRecord(context: StoreContext, record: StoreItemRecord, existing: ExistingRecordMeta): Promise<void>;
|
|
11
27
|
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../../src/store/internal/persist.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../../src/store/internal/persist.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAa5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|
|
@@ -5,34 +5,55 @@ const codec_1 = require("../../shared/codec/codec");
|
|
|
5
5
|
const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
|
|
6
6
|
const orphans_1 = require("../../shared/codec/s3/orphans");
|
|
7
7
|
const retry_1 = require("../../shared/dynamodb/retry");
|
|
8
|
+
const overwrite_swap_1 = require("./overwrite-swap");
|
|
8
9
|
const write_verify_1 = require("./write-verify");
|
|
9
|
-
/**
|
|
10
|
-
function
|
|
11
|
-
|
|
10
|
+
/** Best-effort delete of one descriptor's S3 object. */
|
|
11
|
+
async function cleanUp(context, descriptor, label) {
|
|
12
|
+
if (!context.offloader || !descriptor)
|
|
13
|
+
return;
|
|
14
|
+
await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)([descriptor]), label, context.logger);
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
|
-
* Put the record
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Put the record and clean up whichever side is now dead.
|
|
18
|
+
*
|
|
19
|
+
* The compare-and-swap path runs **only when an offloader is configured**:
|
|
20
|
+
* without one there is no S3 object to orphan, so a plain last-write-wins put
|
|
21
|
+
* stays correct and costs no extra write capacity (DynamoDB charges for a
|
|
22
|
+
* failed conditional write too). With one, the swap is what lets this call
|
|
23
|
+
* delete exactly the payload it superseded rather than a descriptor a racer may
|
|
24
|
+
* already have replaced.
|
|
25
|
+
*
|
|
26
|
+
* Every failure reaching the catch arrives after at least one put was issued —
|
|
27
|
+
* `putWithRevisionSwap` only re-reads from inside its own catch — so none of
|
|
28
|
+
* them proves a non-commit on its own: a put can commit server-side and lose
|
|
29
|
+
* its response, and a `ConditionalCheckFailedException` is as consistent with
|
|
30
|
+
* hitting the row this call just wrote as with a competitor's win. The row is
|
|
31
|
+
* therefore read back (`verifyWriteLanded`) before anything is deleted. Only a
|
|
32
|
+
* confirmed `'not-landed'` deletes this record's own nonced object; a confirmed
|
|
33
|
+
* `'landed'` cleans up the previous object like the success path and swallows
|
|
34
|
+
* the error, and an `'unverified'` read deletes nothing and rethrows — leaking
|
|
35
|
+
* one object at worst rather than stranding a live row pointing at a deleted
|
|
36
|
+
* one. An inline payload has no object to leak, so it needs no read.
|
|
18
37
|
*/
|
|
19
|
-
async function persistRecord(context, record,
|
|
38
|
+
async function persistRecord(context, record, existing) {
|
|
39
|
+
let superseded = existing;
|
|
20
40
|
try {
|
|
21
|
-
|
|
41
|
+
if (context.offloader) {
|
|
42
|
+
superseded = await (0, overwrite_swap_1.putWithRevisionSwap)(context, record, existing);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await (0, retry_1.withDynamoDBRetry)(() => context.client.put({ TableName: context.tableName, Item: record }));
|
|
46
|
+
}
|
|
22
47
|
}
|
|
23
48
|
catch (error) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
49
|
+
const verdict = record.value.location === codec_1.PayloadLocation.S3
|
|
50
|
+
? await (0, write_verify_1.verifyWriteLanded)(context, record, record.value.s3Key)
|
|
51
|
+
: 'not-landed';
|
|
52
|
+
if (verdict === 'not-landed')
|
|
53
|
+
await cleanUp(context, record.value, 'store.put');
|
|
54
|
+
if (verdict !== 'landed')
|
|
31
55
|
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (context.offloader && previousValue) {
|
|
35
|
-
await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)([previousValue]), 'store.put.overwrite', context.logger);
|
|
36
56
|
}
|
|
57
|
+
await cleanUp(context, superseded.value, 'store.put.overwrite');
|
|
37
58
|
}
|
|
38
59
|
//# sourceMappingURL=persist.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persist.js","sourceRoot":"","sources":["../../../src/store/internal/persist.ts"],"names":[],"mappings":";;;AAAA,oDAAmF;AACnF,wEAAmE;AACnE,2DAAiE;AACjE,uDAAgE;
|
|
1
|
+
{"version":3,"file":"persist.js","sourceRoot":"","sources":["../../../src/store/internal/persist.ts"],"names":[],"mappings":";;;AAAA,oDAAmF;AACnF,wEAAmE;AACnE,2DAAiE;AACjE,uDAAgE;AAEhE,qDAAuD;AAGvD,iDAAsE;AAEtE,wDAAwD;AACxD,KAAK,UAAU,OAAO,CACpB,OAAqB,EACrB,UAAyC,EACzC,KAAa;IAEb,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,UAAU;QAAE,OAAO;IAC9C,MAAM,IAAA,0BAAgB,EAAC,OAAO,CAAC,SAAS,EAAE,IAAA,+BAAa,EAAC,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,KAAK,wBACV,OAAqB,EACrB,MAAuB,EACvB,QAA4B;IAE5B,IAAI,UAAU,GAAG,QAAQ,CAAC;IAC1B,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,UAAU,GAAG,MAAM,IAAA,oCAAmB,EAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QACpE,CAAC;aAAM,CAAC;YACN,MAAM,IAAA,yBAAiB,EAAC,GAAG,EAAE,CAC3B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CACnE,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GACX,MAAM,CAAC,KAAK,CAAC,QAAQ,KAAK,uBAAe,CAAC,EAAE;YAC1C,CAAC,CAAC,MAAM,IAAA,gCAAiB,EAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;YAC9D,CAAC,CAAC,YAAY,CAAC;QACnB,IAAI,OAAO,KAAK,YAAY;YAAE,MAAM,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAChF,IAAI,OAAO,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;IACxC,CAAC;IACD,MAAM,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PayloadDescriptor } from '../../shared/codec/codec';
|
|
2
|
+
import type { StoreContext } from './setup';
|
|
3
|
+
/** The previous row's createdAt, payload descriptor and revision. */
|
|
4
|
+
export interface ExistingRecordMeta {
|
|
5
|
+
exists: boolean;
|
|
6
|
+
createdAt?: string;
|
|
7
|
+
value?: PayloadDescriptor;
|
|
8
|
+
revision?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read the fields a put needs from the row it is about to replace, in one
|
|
12
|
+
* strongly-consistent projection: `createdAt` to preserve, `value` to clean up
|
|
13
|
+
* afterwards, and the revision the compare-and-swap pins.
|
|
14
|
+
*
|
|
15
|
+
* Lives apart from `actions/put.ts` so `persist.ts` can re-read on a lost swap
|
|
16
|
+
* without importing its own caller.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readExisting(context: StoreContext, pk: string, sk: string): Promise<ExistingRecordMeta>;
|
|
19
|
+
//# sourceMappingURL=read-existing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-existing.d.ts","sourceRoot":"","sources":["../../../src/store/internal/read-existing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,kBAAkB,CAAC,CAgB7B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readExisting = readExisting;
|
|
4
|
+
const conditional_put_1 = require("../../shared/dynamodb/conditional-put");
|
|
5
|
+
const retry_1 = require("../../shared/dynamodb/retry");
|
|
6
|
+
/**
|
|
7
|
+
* Read the fields a put needs from the row it is about to replace, in one
|
|
8
|
+
* strongly-consistent projection: `createdAt` to preserve, `value` to clean up
|
|
9
|
+
* afterwards, and the revision the compare-and-swap pins.
|
|
10
|
+
*
|
|
11
|
+
* Lives apart from `actions/put.ts` so `persist.ts` can re-read on a lost swap
|
|
12
|
+
* without importing its own caller.
|
|
13
|
+
*/
|
|
14
|
+
async function readExisting(context, pk, sk) {
|
|
15
|
+
const existing = await (0, retry_1.withDynamoDBRetry)(() => context.client.get({
|
|
16
|
+
TableName: context.tableName,
|
|
17
|
+
Key: { PK: pk, SK: sk },
|
|
18
|
+
ConsistentRead: true,
|
|
19
|
+
ProjectionExpression: '#c, #v, #r',
|
|
20
|
+
ExpressionAttributeNames: { '#c': 'createdAt', '#v': 'value', '#r': conditional_put_1.REVISION_ATTRIBUTE },
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
exists: existing.Item !== undefined,
|
|
24
|
+
createdAt: existing.Item?.createdAt,
|
|
25
|
+
value: existing.Item?.value,
|
|
26
|
+
revision: existing.Item?.[conditional_put_1.REVISION_ATTRIBUTE],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=read-existing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-existing.js","sourceRoot":"","sources":["../../../src/store/internal/read-existing.ts"],"names":[],"mappings":";;;AACA,2EAA2E;AAC3E,uDAAgE;AAWhE;;;;;;;GAOG;AACI,KAAK,uBACV,OAAqB,EACrB,EAAU,EACV,EAAU;IAEV,MAAM,QAAQ,GAAG,MAAM,IAAA,yBAAiB,EAAC,GAAG,EAAE,CAC5C,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;QACjB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QACvB,cAAc,EAAE,IAAI;QACpB,oBAAoB,EAAE,YAAY;QAClC,wBAAwB,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,oCAAkB,EAAE;KACzF,CAAC,CACH,CAAC;IACF,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,IAAI,KAAK,SAAS;QACnC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,SAA+B;QACzD,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAsC;QAC5D,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,oCAAkB,CAAuB;KACpE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { VectorMatch } from '../vector-backend';
|
|
2
|
+
/** Which direction a {@link VectorBackend} reports its score in. */
|
|
3
|
+
export type VectorScoreDirection = 'relevance' | 'distance';
|
|
4
|
+
/** Every direction {@link toRelevanceScores} recognises, for validating input. */
|
|
5
|
+
export declare const VECTOR_SCORE_DIRECTIONS: readonly VectorScoreDirection[];
|
|
6
|
+
/**
|
|
7
|
+
* Normalise a backend's matches to the relevance direction upstream
|
|
8
|
+
* `SearchItem.score` documents — "higher scores indicate better matches",
|
|
9
|
+
* typically a cosine similarity between -1 and 1.
|
|
10
|
+
*
|
|
11
|
+
* A distance is converted by **negation** rather than `1 / (1 + d)`: negation
|
|
12
|
+
* is monotone over the whole real line, needs no non-negativity precondition,
|
|
13
|
+
* and is exactly invertible, so the original distance is simply `-score`. A
|
|
14
|
+
* reciprocal would silently imply a different `(0, 1]` scale and is undefined
|
|
15
|
+
* at `d === -1`. Negative scores are already in range for this contract, so
|
|
16
|
+
* nothing is lost by producing them.
|
|
17
|
+
*
|
|
18
|
+
* Results are re-sorted after conversion so a backend that returns its matches
|
|
19
|
+
* in some other order still ranks correctly. Anything that is not `'distance'`
|
|
20
|
+
* is returned untouched — its own order stays authoritative, exactly as before.
|
|
21
|
+
*
|
|
22
|
+
* The test is for `'distance'` rather than against `'relevance'` on purpose:
|
|
23
|
+
* converting is the destructive branch, so only the exact value that asks for
|
|
24
|
+
* it may reach it. Testing the other way round made every unrecognised
|
|
25
|
+
* string — `'Distance'`, a typo, a value read from a config file — reverse the
|
|
26
|
+
* ranking silently, with no error and no warning (the ascending-score warning
|
|
27
|
+
* lives downstream of this conversion, so it could never fire). `setUpStore`
|
|
28
|
+
* rejects such a value outright; this keeps the failure harmless for any
|
|
29
|
+
* caller that reaches the function some other way.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toRelevanceScores(matches: VectorMatch[], direction: VectorScoreDirection): VectorMatch[];
|
|
32
|
+
//# sourceMappingURL=score-direction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-direction.d.ts","sourceRoot":"","sources":["../../../src/store/internal/score-direction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,oEAAoE;AACpE,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,UAAU,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAA8B,CAAC;AAElG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,oBAAoB,GAC9B,WAAW,EAAE,CAKf"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VECTOR_SCORE_DIRECTIONS = void 0;
|
|
4
|
+
exports.toRelevanceScores = toRelevanceScores;
|
|
5
|
+
/** Every direction {@link toRelevanceScores} recognises, for validating input. */
|
|
6
|
+
exports.VECTOR_SCORE_DIRECTIONS = ['relevance', 'distance'];
|
|
7
|
+
/**
|
|
8
|
+
* Normalise a backend's matches to the relevance direction upstream
|
|
9
|
+
* `SearchItem.score` documents — "higher scores indicate better matches",
|
|
10
|
+
* typically a cosine similarity between -1 and 1.
|
|
11
|
+
*
|
|
12
|
+
* A distance is converted by **negation** rather than `1 / (1 + d)`: negation
|
|
13
|
+
* is monotone over the whole real line, needs no non-negativity precondition,
|
|
14
|
+
* and is exactly invertible, so the original distance is simply `-score`. A
|
|
15
|
+
* reciprocal would silently imply a different `(0, 1]` scale and is undefined
|
|
16
|
+
* at `d === -1`. Negative scores are already in range for this contract, so
|
|
17
|
+
* nothing is lost by producing them.
|
|
18
|
+
*
|
|
19
|
+
* Results are re-sorted after conversion so a backend that returns its matches
|
|
20
|
+
* in some other order still ranks correctly. Anything that is not `'distance'`
|
|
21
|
+
* is returned untouched — its own order stays authoritative, exactly as before.
|
|
22
|
+
*
|
|
23
|
+
* The test is for `'distance'` rather than against `'relevance'` on purpose:
|
|
24
|
+
* converting is the destructive branch, so only the exact value that asks for
|
|
25
|
+
* it may reach it. Testing the other way round made every unrecognised
|
|
26
|
+
* string — `'Distance'`, a typo, a value read from a config file — reverse the
|
|
27
|
+
* ranking silently, with no error and no warning (the ascending-score warning
|
|
28
|
+
* lives downstream of this conversion, so it could never fire). `setUpStore`
|
|
29
|
+
* rejects such a value outright; this keeps the failure harmless for any
|
|
30
|
+
* caller that reaches the function some other way.
|
|
31
|
+
*/
|
|
32
|
+
function toRelevanceScores(matches, direction) {
|
|
33
|
+
if (direction !== 'distance')
|
|
34
|
+
return matches;
|
|
35
|
+
return matches
|
|
36
|
+
.map((match) => ({ ...match, score: -match.score }))
|
|
37
|
+
.sort((left, right) => right.score - left.score);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=score-direction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-direction.js","sourceRoot":"","sources":["../../../src/store/internal/score-direction.ts"],"names":[],"mappings":";;;;AAKA,kFAAkF;AACrE,QAAA,uBAAuB,GAAoC,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAElG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,2BACE,OAAsB,EACtB,SAA+B;IAE/B,IAAI,SAAS,KAAK,UAAU;QAAE,OAAO,OAAO,CAAC;IAC7C,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;SACnD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Item, SearchOperation } from '@langchain/langgraph-checkpoint';
|
|
2
|
+
/** True when `item` satisfies the search operation's optional metadata filter. */
|
|
3
|
+
export declare function passesFilter(item: Item, op: SearchOperation): boolean;
|
|
4
|
+
//# sourceMappingURL=search-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-filter.d.ts","sourceRoot":"","sources":["../../../src/store/internal/search-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAI7E,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,eAAe,GAAG,OAAO,CAMrE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.passesFilter = passesFilter;
|
|
4
|
+
const filter_1 = require("./filter");
|
|
5
|
+
/** True when `item` satisfies the search operation's optional metadata filter. */
|
|
6
|
+
function passesFilter(item, op) {
|
|
7
|
+
if (!op.filter)
|
|
8
|
+
return true;
|
|
9
|
+
return (0, filter_1.matchesStoreFilter)(item.value, op.filter);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=search-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-filter.js","sourceRoot":"","sources":["../../../src/store/internal/search-filter.ts"],"names":[],"mappings":";;;AAEA,qCAA8D;AAE9D,kFAAkF;AAClF,sBAA6B,IAAU,EAAE,EAAmB;IAC1D,IAAI,CAAC,EAAE,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,IAAA,2BAAkB,EACvB,IAAI,CAAC,KAAkC,EACvC,EAAE,CAAC,MAAmC,CACvC,CAAC;AACJ,CAAC"}
|
|
@@ -7,6 +7,7 @@ import { type Logger } from '../../shared/logging/logger';
|
|
|
7
7
|
import type { TtlOption } from '../../shared/validation/ttl';
|
|
8
8
|
import type { DynamoDBStoreOptions } from '../types';
|
|
9
9
|
import type { VectorBackend } from '../vector-backend';
|
|
10
|
+
import { type VectorScoreDirection } from './score-direction';
|
|
10
11
|
/** Resolved collaborators shared by every store action. */
|
|
11
12
|
export interface StoreContext {
|
|
12
13
|
client: DynamoDBDocument;
|
|
@@ -18,6 +19,7 @@ export interface StoreContext {
|
|
|
18
19
|
logger: Logger;
|
|
19
20
|
index?: IndexConfig;
|
|
20
21
|
vectorBackend?: VectorBackend;
|
|
22
|
+
vectorScoreDirection: VectorScoreDirection;
|
|
21
23
|
maxSearchCandidates: number;
|
|
22
24
|
maxScanItems: number;
|
|
23
25
|
}
|
|
@@ -27,6 +29,16 @@ export interface StoreSetup {
|
|
|
27
29
|
ddbClient: DynamoDBClient | undefined;
|
|
28
30
|
ownsClient: boolean;
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the client, optional S3 offloader, serializer, and index config.
|
|
34
|
+
*
|
|
35
|
+
* A `vectorBackend` without an `index` is rejected outright rather than
|
|
36
|
+
* silently degrading: with no embeddings configured, every `put` would compute
|
|
37
|
+
* no vector and instruct the backend to *delete* the item's entry instead of
|
|
38
|
+
* indexing it, and `search()` would fall through to an unranked scan-order
|
|
39
|
+
* listing with no `.score` field and no error — a semantic query returning a
|
|
40
|
+
* normal-looking but meaningless response. `reconcileVectorIndex` already
|
|
41
|
+
* refused this exact misconfiguration.
|
|
42
|
+
*/
|
|
31
43
|
export declare function setUpStore(options: DynamoDBStoreOptions): StoreSetup;
|
|
32
44
|
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/store/internal/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/store/internal/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAEvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAQ9D,OAAO,EAAE,KAAK,MAAM,EAAiB,MAAM,6BAA6B,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAA2B,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE,cAAc,GAAG,SAAS,CAAC;IACtC,UAAU,EAAE,OAAO,CAAC;CACrB;AA4CD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,oBAAoB,GAAG,UAAU,CAoCpE"}
|
|
@@ -6,9 +6,64 @@ const config_1 = require("../../shared/codec/s3/config");
|
|
|
6
6
|
const offloader_1 = require("../../shared/codec/s3/offloader");
|
|
7
7
|
const constants_1 = require("../../shared/constants");
|
|
8
8
|
const client_1 = require("../../shared/dynamodb/client");
|
|
9
|
+
const errors_1 = require("../../shared/errors/errors");
|
|
9
10
|
const logger_1 = require("../../shared/logging/logger");
|
|
10
|
-
|
|
11
|
+
const score_direction_1 = require("./score-direction");
|
|
12
|
+
/**
|
|
13
|
+
* Reject an `index` that cannot actually embed. `IndexConfig` mandates
|
|
14
|
+
* `embeddings`, but a JavaScript caller can omit it or pass the wrong shape,
|
|
15
|
+
* and the failure then surfaced as a raw `TypeError` deep inside the first
|
|
16
|
+
* `put()`/`search()` rather than this library's typed error at construction.
|
|
17
|
+
*
|
|
18
|
+
* Only `embeddings` is checked. `dims` is part of the upstream type but is
|
|
19
|
+
* never read anywhere in this package, so rejecting a configuration over it
|
|
20
|
+
* would break working callers for no benefit.
|
|
21
|
+
*/
|
|
22
|
+
function assertUsableIndex(index) {
|
|
23
|
+
if (!index)
|
|
24
|
+
return;
|
|
25
|
+
const embeddings = index.embeddings;
|
|
26
|
+
if (typeof embeddings?.embedQuery !== 'function') {
|
|
27
|
+
throw new errors_1.ValidationError('`index.embeddings` must be an Embeddings implementation exposing embedQuery(); ' +
|
|
28
|
+
'without one no embedding can be computed for put() or search()', 'index');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reject a `vectorScoreDirection` outside the declared union.
|
|
33
|
+
*
|
|
34
|
+
* {@link toRelevanceScores} treats anything it does not recognise as a no-op —
|
|
35
|
+
* the only safe default, since guessing would invert a ranking — so a mistyped
|
|
36
|
+
* or config-file-sourced value would otherwise leave a distance backend ranked
|
|
37
|
+
* backwards with no error and no warning anywhere. Same premise as
|
|
38
|
+
* {@link assertUsableIndex}: a JavaScript caller can pass a string the type
|
|
39
|
+
* never admits.
|
|
40
|
+
*/
|
|
41
|
+
function assertScoreDirection(direction) {
|
|
42
|
+
if (direction === undefined || score_direction_1.VECTOR_SCORE_DIRECTIONS.includes(direction))
|
|
43
|
+
return;
|
|
44
|
+
throw new errors_1.ValidationError(`vectorScoreDirection must be one of ${score_direction_1.VECTOR_SCORE_DIRECTIONS.join(' | ')}; received ` +
|
|
45
|
+
`${JSON.stringify(direction)}, which would be left in the backend's own direction and ` +
|
|
46
|
+
'could rank a distance backend backwards', 'vectorScoreDirection');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the client, optional S3 offloader, serializer, and index config.
|
|
50
|
+
*
|
|
51
|
+
* A `vectorBackend` without an `index` is rejected outright rather than
|
|
52
|
+
* silently degrading: with no embeddings configured, every `put` would compute
|
|
53
|
+
* no vector and instruct the backend to *delete* the item's entry instead of
|
|
54
|
+
* indexing it, and `search()` would fall through to an unranked scan-order
|
|
55
|
+
* listing with no `.score` field and no error — a semantic query returning a
|
|
56
|
+
* normal-looking but meaningless response. `reconcileVectorIndex` already
|
|
57
|
+
* refused this exact misconfiguration.
|
|
58
|
+
*/
|
|
11
59
|
function setUpStore(options) {
|
|
60
|
+
if (options.vectorBackend && !options.index) {
|
|
61
|
+
throw new errors_1.ValidationError('vectorBackend requires a configured `index` (embeddings); without one no embedding ' +
|
|
62
|
+
'is computed, every put would clear the item vector, and search would silently return ' +
|
|
63
|
+
'unranked, score-less results', 'vectorBackend');
|
|
64
|
+
}
|
|
65
|
+
assertUsableIndex(options.index);
|
|
66
|
+
assertScoreDirection(options.vectorScoreDirection);
|
|
12
67
|
const resolved = (0, client_1.resolveDynamoDBClient)(options);
|
|
13
68
|
return {
|
|
14
69
|
context: {
|
|
@@ -26,6 +81,7 @@ function setUpStore(options) {
|
|
|
26
81
|
logger: (0, logger_1.resolveLogger)(options.logger),
|
|
27
82
|
index: options.index,
|
|
28
83
|
vectorBackend: options.vectorBackend,
|
|
84
|
+
vectorScoreDirection: options.vectorScoreDirection ?? 'relevance',
|
|
29
85
|
maxSearchCandidates: options.maxSearchCandidates ?? constants_1.DEFAULT_MAX_SEARCH_CANDIDATES,
|
|
30
86
|
maxScanItems: options.maxScanItems ?? constants_1.MAX_TOTAL_ITEMS_IN_MEMORY,
|
|
31
87
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/store/internal/setup.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/store/internal/setup.ts"],"names":[],"mappings":";;;AAMA,8DAA2D;AAC3D,yDAAuE;AACvE,+DAA8D;AAC9D,sDAIgC;AAChC,yDAAqE;AACrE,uDAA6D;AAC7D,wDAAyE;AAIzE,uDAAuF;AAyBvF;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,KAAmB;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,MAAM,UAAU,GAAoC,KAAK,CAAC,UAAU,CAAC;IACrE,IAAI,OAAO,UAAU,EAAE,UAAU,KAAK,UAAU,EAAE,CAAC;QACjD,MAAM,IAAI,wBAAe,CACvB,iFAAiF;YAC/E,gEAAgE,EAClE,OAAO,CACR,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,oBAAoB,CAAC,SAAgC;IAC5D,IAAI,SAAS,KAAK,SAAS,IAAI,yCAAuB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO;IACnF,MAAM,IAAI,wBAAe,CACvB,uCAAuC,yCAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa;QACrF,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,2DAA2D;QACvF,yCAAyC,EAC3C,sBAAsB,CACvB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,oBAA2B,OAA6B;IACtD,IAAI,OAAO,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5C,MAAM,IAAI,wBAAe,CACvB,qFAAqF;YACnF,uFAAuF;YACvF,8BAA8B,EAChC,eAAe,CAChB,CAAC;IACJ,CAAC;IACD,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,oBAAoB,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAA,8BAAqB,EAAC,OAAO,CAAC,CAAC;IAChD,OAAO;QACL,OAAO,EAAE;YACP,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,uBAAU;YAClC,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,SAAS,EAAE,OAAO,CAAC,EAAE;gBACnB,CAAC,CAAC,IAAI,uBAAW,CAAC;oBACd,GAAG,OAAO,CAAC,EAAE;oBACb,SAAS,EACP,OAAO,CAAC,EAAE,CAAC,SAAS,IAAI,IAAA,gCAAuB,EAAC,iCAAqB,EAAE,OAAO,CAAC;iBAClF,CAAC;gBACJ,CAAC,CAAC,SAAS;YACb,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,MAAM,EAAE,IAAA,sBAAa,EAAC,OAAO,CAAC,MAAM,CAAC;YACrC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,IAAI,WAAW;YACjE,mBAAmB,EAAE,OAAO,CAAC,mBAAmB,IAAI,yCAA6B;YACjF,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,qCAAyB;SAChE;QACD,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,UAAU,EAAE,QAAQ,CAAC,UAAU;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** Validate search/listNamespaces paging is non-negative integers. */
|
|
2
2
|
export declare function validatePaging(offset: number, limit: number): void;
|
|
3
|
-
/** Validate the namespace is non-empty and each element is
|
|
3
|
+
/** Validate the namespace is non-empty and each element is a valid identifier. */
|
|
4
4
|
export declare function validateNamespace(namespace: string[]): void;
|
|
5
|
-
/** Validate an item key is a non-empty, separator-free string. */
|
|
5
|
+
/** Validate an item key is a non-empty, separator- and control-char-free string. */
|
|
6
6
|
export declare function validateKey(key: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Validate an optional namespace depth cap. Left unchecked, a negative value
|
|
9
|
+
* silently inverted truncation via `Array.prototype.slice(0, -n)`, which drops
|
|
10
|
+
* the *last* n elements rather than erroring.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateMaxDepth(maxDepth?: number): void;
|
|
7
13
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/store/internal/validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/store/internal/validation.ts"],"names":[],"mappings":"AAOA,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGlE;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAK3D;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAGxD"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validatePaging = validatePaging;
|
|
4
4
|
exports.validateNamespace = validateNamespace;
|
|
5
5
|
exports.validateKey = validateKey;
|
|
6
|
+
exports.validateMaxDepth = validateMaxDepth;
|
|
6
7
|
const primitives_1 = require("../../shared/validation/primitives");
|
|
7
8
|
const keys_1 = require("./keys");
|
|
8
9
|
/** Validate search/listNamespaces paging is non-negative integers. */
|
|
@@ -10,17 +11,25 @@ function validatePaging(offset, limit) {
|
|
|
10
11
|
(0, primitives_1.validateInteger)(offset, 'offset', { min: 0 });
|
|
11
12
|
(0, primitives_1.validateInteger)(limit, 'limit', { min: 0 });
|
|
12
13
|
}
|
|
13
|
-
/** Validate the namespace is non-empty and each element is
|
|
14
|
+
/** Validate the namespace is non-empty and each element is a valid identifier. */
|
|
14
15
|
function validateNamespace(namespace) {
|
|
15
16
|
(0, primitives_1.validateNonEmptyArray)(namespace, 'namespace');
|
|
16
17
|
for (const element of namespace) {
|
|
17
|
-
(0, primitives_1.
|
|
18
|
-
(0, primitives_1.assertNoSeparator)(element, keys_1.NAMESPACE_SEPARATOR, 'namespace element');
|
|
18
|
+
(0, primitives_1.validateIdentifier)(element, keys_1.NAMESPACE_SEPARATOR, 'namespace element');
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
/** Validate an item key is a non-empty, separator-free string. */
|
|
21
|
+
/** Validate an item key is a non-empty, separator- and control-char-free string. */
|
|
22
22
|
function validateKey(key) {
|
|
23
|
-
(0, primitives_1.
|
|
24
|
-
|
|
23
|
+
(0, primitives_1.validateIdentifier)(key, keys_1.NAMESPACE_SEPARATOR, 'key');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate an optional namespace depth cap. Left unchecked, a negative value
|
|
27
|
+
* silently inverted truncation via `Array.prototype.slice(0, -n)`, which drops
|
|
28
|
+
* the *last* n elements rather than erroring.
|
|
29
|
+
*/
|
|
30
|
+
function validateMaxDepth(maxDepth) {
|
|
31
|
+
if (maxDepth === undefined)
|
|
32
|
+
return;
|
|
33
|
+
(0, primitives_1.validateInteger)(maxDepth, 'maxDepth', { min: 1 });
|
|
25
34
|
}
|
|
26
35
|
//# sourceMappingURL=validation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/store/internal/validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/store/internal/validation.ts"],"names":[],"mappings":";;;;;;AAAA,mEAI4C;AAC5C,iCAA6C;AAE7C,sEAAsE;AACtE,wBAA+B,MAAc,EAAE,KAAa;IAC1D,IAAA,4BAAe,EAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9C,IAAA,4BAAe,EAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,kFAAkF;AAClF,2BAAkC,SAAmB;IACnD,IAAA,kCAAqB,EAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC9C,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,IAAA,+BAAkB,EAAC,OAAO,EAAE,0BAAmB,EAAE,mBAAmB,CAAC,CAAC;IACxE,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,qBAA4B,GAAW;IACrC,IAAA,+BAAkB,EAAC,GAAG,EAAE,0BAAmB,EAAE,KAAK,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,0BAAiC,QAAiB;IAChD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO;IACnC,IAAA,4BAAe,EAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC"}
|