@farukada/aws-langgraph-dynamodb-ts 0.9.0 → 1.0.0-rc.2
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 +1720 -154
- package/dist/backfill/backfill.d.ts +168 -0
- package/dist/backfill/backfill.js +393 -0
- package/dist/checkpointer/actions/delete-thread.d.ts +47 -6
- package/dist/checkpointer/actions/delete-thread.js +58 -21
- package/dist/checkpointer/actions/get-tuple.d.ts +29 -4
- package/dist/checkpointer/actions/get-tuple.js +44 -10
- package/dist/checkpointer/actions/list.d.ts +46 -4
- package/dist/checkpointer/actions/list.js +121 -66
- package/dist/checkpointer/actions/put-writes.d.ts +41 -9
- package/dist/checkpointer/actions/put-writes.js +62 -77
- package/dist/checkpointer/actions/put.d.ts +83 -4
- package/dist/checkpointer/actions/put.js +177 -25
- package/dist/checkpointer/internal/delta-history.d.ts +112 -0
- package/dist/checkpointer/internal/delta-history.js +252 -0
- package/dist/checkpointer/internal/listing.d.ts +149 -0
- package/dist/checkpointer/internal/listing.js +245 -0
- package/dist/checkpointer/internal/parse.d.ts +262 -0
- package/dist/checkpointer/internal/parse.js +372 -0
- package/dist/checkpointer/internal/pending-writes.d.ts +275 -0
- package/dist/checkpointer/internal/pending-writes.js +588 -0
- package/dist/checkpointer/internal/read.d.ts +130 -0
- package/dist/checkpointer/internal/read.js +264 -0
- package/dist/checkpointer/internal/rows.d.ts +571 -0
- package/dist/checkpointer/internal/rows.js +834 -0
- package/dist/checkpointer/internal/setup.d.ts +42 -19
- package/dist/checkpointer/internal/setup.js +65 -29
- package/dist/checkpointer/saver.d.ts +256 -16
- package/dist/checkpointer/saver.js +275 -29
- package/dist/checkpointer/types.d.ts +39 -39
- package/dist/checkpointer/types.js +10 -1
- package/dist/factory/factory.d.ts +134 -28
- package/dist/factory/factory.js +240 -21
- package/dist/factory/types.d.ts +76 -0
- package/dist/factory/types.js +10 -0
- package/dist/history/actions/add-messages.d.ts +31 -4
- package/dist/history/actions/add-messages.js +38 -58
- package/dist/history/actions/clear.d.ts +49 -6
- package/dist/history/actions/clear.js +66 -14
- package/dist/history/actions/get-messages.d.ts +54 -6
- package/dist/history/actions/get-messages.js +126 -43
- package/dist/history/actions/list-sessions.d.ts +52 -10
- package/dist/history/actions/list-sessions.js +139 -40
- package/dist/history/actions/reconcile-count.d.ts +42 -10
- package/dist/history/actions/reconcile-count.js +45 -45
- package/dist/history/chat-message-history.d.ts +220 -33
- package/dist/history/chat-message-history.js +240 -43
- package/dist/history/internal/append.d.ts +212 -0
- package/dist/history/internal/append.js +500 -0
- package/dist/history/internal/message-read.d.ts +84 -0
- package/dist/history/internal/message-read.js +204 -0
- package/dist/history/internal/parse.d.ts +153 -0
- package/dist/history/internal/parse.js +252 -0
- package/dist/history/internal/rows.d.ts +195 -0
- package/dist/history/internal/rows.js +250 -0
- package/dist/history/internal/session.d.ts +331 -0
- package/dist/history/internal/session.js +628 -0
- package/dist/history/internal/setup.d.ts +52 -17
- package/dist/history/internal/setup.js +92 -21
- package/dist/history/session-adapter.d.ts +102 -7
- package/dist/history/session-adapter.js +103 -9
- package/dist/history/types.d.ts +80 -29
- package/dist/history/types.js +10 -1
- package/dist/index.d.ts +42 -11
- package/dist/index.js +33 -12
- package/dist/shared/adapter.d.ts +135 -0
- package/dist/shared/adapter.js +143 -0
- package/dist/shared/clock.d.ts +51 -2
- package/dist/shared/clock.js +57 -2
- package/dist/shared/codec/codec.d.ts +288 -13
- package/dist/shared/codec/codec.js +416 -19
- package/dist/shared/codec/compression.d.ts +43 -7
- package/dist/shared/codec/compression.js +53 -13
- package/dist/shared/codec/json-serde.d.ts +76 -4
- package/dist/shared/codec/json-serde.js +181 -8
- package/dist/shared/codec/s3/client-types.d.ts +53 -0
- package/dist/shared/codec/s3/client-types.js +26 -0
- package/dist/shared/codec/s3/client.d.ts +43 -10
- package/dist/shared/codec/s3/client.js +82 -9
- package/dist/shared/codec/s3/config.d.ts +242 -11
- package/dist/shared/codec/s3/config.js +293 -11
- package/dist/shared/codec/s3/lifecycle.d.ts +164 -6
- package/dist/shared/codec/s3/lifecycle.js +335 -27
- package/dist/shared/codec/s3/offloader.d.ts +393 -18
- package/dist/shared/codec/s3/offloader.js +595 -37
- package/dist/shared/concurrency.d.ts +43 -0
- package/dist/shared/concurrency.js +78 -0
- package/dist/shared/dynamodb/abort.d.ts +47 -0
- package/dist/shared/dynamodb/abort.js +59 -0
- package/dist/shared/dynamodb/batch-write.d.ts +77 -14
- package/dist/shared/dynamodb/batch-write.js +146 -27
- package/dist/shared/dynamodb/cancellation.d.ts +121 -4
- package/dist/shared/dynamodb/cancellation.js +147 -3
- package/dist/shared/dynamodb/client.d.ts +162 -8
- package/dist/shared/dynamodb/client.js +153 -5
- package/dist/shared/dynamodb/idempotent-write.d.ts +551 -0
- package/dist/shared/dynamodb/idempotent-write.js +593 -0
- package/dist/shared/dynamodb/paginate.d.ts +105 -9
- package/dist/shared/dynamodb/paginate.js +175 -7
- package/dist/shared/dynamodb/partition-delete.d.ts +185 -14
- package/dist/shared/dynamodb/partition-delete.js +314 -44
- package/dist/shared/dynamodb/recency-index.d.ts +231 -0
- package/dist/shared/dynamodb/recency-index.js +377 -0
- package/dist/shared/dynamodb/retry.d.ts +276 -8
- package/dist/shared/dynamodb/retry.js +433 -23
- package/dist/shared/dynamodb/table-schema.d.ts +190 -0
- package/dist/shared/dynamodb/table-schema.js +209 -0
- package/dist/shared/errors/base-error.d.ts +184 -10
- package/dist/shared/errors/base-error.js +160 -14
- package/dist/shared/errors/boundary.d.ts +71 -0
- package/dist/shared/errors/boundary.js +143 -0
- package/dist/shared/errors/classify.d.ts +97 -0
- package/dist/shared/errors/classify.js +257 -0
- package/dist/shared/errors/error-code.d.ts +77 -2
- package/dist/shared/errors/error-code.js +83 -1
- package/dist/shared/errors/errors.d.ts +158 -59
- package/dist/shared/errors/errors.js +219 -92
- package/dist/shared/logging/logger.d.ts +69 -3
- package/dist/shared/logging/logger.js +97 -3
- package/dist/shared/logging/redaction.d.ts +92 -8
- package/dist/shared/logging/redaction.js +273 -17
- package/dist/shared/logging/secret-patterns.d.ts +149 -19
- package/dist/shared/logging/secret-patterns.js +188 -27
- package/dist/shared/logging/truncate.d.ts +197 -0
- package/dist/shared/logging/truncate.js +231 -0
- package/dist/shared/options.d.ts +59 -7
- package/dist/shared/options.js +9 -1
- package/dist/shared/ulid.d.ts +77 -7
- package/dist/shared/ulid.js +103 -8
- package/dist/shared/validation/collaborators.d.ts +141 -0
- package/dist/shared/validation/collaborators.js +188 -0
- package/dist/shared/validation/option-shape.d.ts +89 -0
- package/dist/shared/validation/option-shape.js +113 -0
- package/dist/shared/validation/options.d.ts +145 -0
- package/dist/shared/validation/options.js +328 -0
- package/dist/shared/validation/primitives.d.ts +288 -21
- package/dist/shared/validation/primitives.js +353 -50
- package/dist/shared/validation/ttl.d.ts +66 -10
- package/dist/shared/validation/ttl.js +113 -15
- package/dist/store/actions/list-namespaces.d.ts +76 -6
- package/dist/store/actions/list-namespaces.js +166 -24
- package/dist/store/actions/put.d.ts +33 -8
- package/dist/store/actions/put.js +53 -60
- package/dist/store/actions/reconcile-vector-index.d.ts +31 -10
- package/dist/store/actions/reconcile-vector-index.js +34 -15
- package/dist/store/actions/search.d.ts +34 -6
- package/dist/store/actions/search.js +56 -51
- package/dist/store/internal/batch-plan.d.ts +26 -0
- package/dist/store/internal/batch-plan.js +109 -0
- package/dist/store/internal/filter.d.ts +36 -3
- package/dist/store/internal/filter.js +66 -15
- package/dist/store/internal/get-item.d.ts +45 -0
- package/dist/store/internal/get-item.js +115 -0
- package/dist/store/internal/item-write.d.ts +230 -0
- package/dist/store/internal/item-write.js +463 -0
- package/dist/store/internal/parse.d.ts +225 -0
- package/dist/store/internal/parse.js +350 -0
- package/dist/store/internal/rows.d.ts +355 -0
- package/dist/store/internal/rows.js +447 -0
- package/dist/store/internal/semantic-search.d.ts +161 -6
- package/dist/store/internal/semantic-search.js +360 -18
- package/dist/store/internal/setup.d.ts +77 -20
- package/dist/store/internal/setup.js +178 -47
- package/dist/store/internal/table-search.d.ts +100 -0
- package/dist/store/internal/table-search.js +213 -0
- package/dist/store/internal/vector-index.d.ts +247 -0
- package/dist/store/internal/vector-index.js +546 -0
- package/dist/store/store.d.ts +270 -17
- package/dist/store/store.js +329 -38
- package/dist/store/types.d.ts +76 -26
- package/dist/store/types.js +13 -1
- package/dist/store/vector-backend.d.ts +64 -4
- package/dist/store/vector-backend.js +15 -1
- package/package.json +58 -36
- package/dist/checkpointer/actions/delete-thread.d.ts.map +0 -1
- package/dist/checkpointer/actions/delete-thread.js.map +0 -1
- package/dist/checkpointer/actions/get-tuple.d.ts.map +0 -1
- package/dist/checkpointer/actions/get-tuple.js.map +0 -1
- package/dist/checkpointer/actions/list.d.ts.map +0 -1
- package/dist/checkpointer/actions/list.js.map +0 -1
- package/dist/checkpointer/actions/put-writes.d.ts.map +0 -1
- package/dist/checkpointer/actions/put-writes.js.map +0 -1
- package/dist/checkpointer/actions/put.d.ts.map +0 -1
- package/dist/checkpointer/actions/put.js.map +0 -1
- package/dist/checkpointer/internal/assemble.d.ts +0 -10
- package/dist/checkpointer/internal/assemble.d.ts.map +0 -1
- package/dist/checkpointer/internal/assemble.js +0 -37
- package/dist/checkpointer/internal/assemble.js.map +0 -1
- package/dist/checkpointer/internal/configurable.d.ts +0 -13
- package/dist/checkpointer/internal/configurable.d.ts.map +0 -1
- package/dist/checkpointer/internal/configurable.js +0 -23
- package/dist/checkpointer/internal/configurable.js.map +0 -1
- package/dist/checkpointer/internal/fetch.d.ts +0 -10
- package/dist/checkpointer/internal/fetch.d.ts.map +0 -1
- package/dist/checkpointer/internal/fetch.js +0 -46
- package/dist/checkpointer/internal/fetch.js.map +0 -1
- package/dist/checkpointer/internal/filter-match.d.ts +0 -12
- package/dist/checkpointer/internal/filter-match.d.ts.map +0 -1
- package/dist/checkpointer/internal/filter-match.js +0 -14
- package/dist/checkpointer/internal/filter-match.js.map +0 -1
- package/dist/checkpointer/internal/item-reader.d.ts +0 -55
- package/dist/checkpointer/internal/item-reader.d.ts.map +0 -1
- package/dist/checkpointer/internal/item-reader.js +0 -88
- package/dist/checkpointer/internal/item-reader.js.map +0 -1
- package/dist/checkpointer/internal/item-writer.d.ts +0 -26
- package/dist/checkpointer/internal/item-writer.d.ts.map +0 -1
- package/dist/checkpointer/internal/item-writer.js +0 -92
- package/dist/checkpointer/internal/item-writer.js.map +0 -1
- package/dist/checkpointer/internal/keys.d.ts +0 -31
- package/dist/checkpointer/internal/keys.d.ts.map +0 -1
- package/dist/checkpointer/internal/keys.js +0 -87
- package/dist/checkpointer/internal/keys.js.map +0 -1
- package/dist/checkpointer/internal/query.d.ts +0 -20
- package/dist/checkpointer/internal/query.d.ts.map +0 -1
- package/dist/checkpointer/internal/query.js +0 -36
- package/dist/checkpointer/internal/query.js.map +0 -1
- package/dist/checkpointer/internal/setup.d.ts.map +0 -1
- package/dist/checkpointer/internal/setup.js.map +0 -1
- package/dist/checkpointer/internal/special-write-cas.d.ts +0 -30
- package/dist/checkpointer/internal/special-write-cas.d.ts.map +0 -1
- package/dist/checkpointer/internal/special-write-cas.js +0 -104
- package/dist/checkpointer/internal/special-write-cas.js.map +0 -1
- package/dist/checkpointer/internal/special-write-cleanup.d.ts +0 -24
- package/dist/checkpointer/internal/special-write-cleanup.d.ts.map +0 -1
- package/dist/checkpointer/internal/special-write-cleanup.js +0 -47
- package/dist/checkpointer/internal/special-write-cleanup.js.map +0 -1
- package/dist/checkpointer/internal/special-write-verify.d.ts +0 -54
- package/dist/checkpointer/internal/special-write-verify.d.ts.map +0 -1
- package/dist/checkpointer/internal/special-write-verify.js +0 -65
- package/dist/checkpointer/internal/special-write-verify.js.map +0 -1
- package/dist/checkpointer/internal/validation.d.ts +0 -13
- package/dist/checkpointer/internal/validation.d.ts.map +0 -1
- package/dist/checkpointer/internal/validation.js +0 -30
- package/dist/checkpointer/internal/validation.js.map +0 -1
- package/dist/checkpointer/internal/write-guard.d.ts +0 -13
- package/dist/checkpointer/internal/write-guard.d.ts.map +0 -1
- package/dist/checkpointer/internal/write-guard.js +0 -39
- package/dist/checkpointer/internal/write-guard.js.map +0 -1
- package/dist/checkpointer/internal/write-index.d.ts +0 -37
- package/dist/checkpointer/internal/write-index.d.ts.map +0 -1
- package/dist/checkpointer/internal/write-index.js +0 -42
- package/dist/checkpointer/internal/write-index.js.map +0 -1
- package/dist/checkpointer/saver.d.ts.map +0 -1
- package/dist/checkpointer/saver.js.map +0 -1
- package/dist/checkpointer/types.d.ts.map +0 -1
- package/dist/checkpointer/types.js.map +0 -1
- package/dist/factory/factory.d.ts.map +0 -1
- package/dist/factory/factory.js.map +0 -1
- package/dist/history/actions/add-messages.d.ts.map +0 -1
- package/dist/history/actions/add-messages.js.map +0 -1
- package/dist/history/actions/clear.d.ts.map +0 -1
- package/dist/history/actions/clear.js.map +0 -1
- package/dist/history/actions/get-messages.d.ts.map +0 -1
- package/dist/history/actions/get-messages.js.map +0 -1
- package/dist/history/actions/list-sessions.d.ts.map +0 -1
- package/dist/history/actions/list-sessions.js.map +0 -1
- package/dist/history/actions/reconcile-count.d.ts.map +0 -1
- package/dist/history/actions/reconcile-count.js.map +0 -1
- package/dist/history/chat-message-history.d.ts.map +0 -1
- package/dist/history/chat-message-history.js.map +0 -1
- package/dist/history/internal/append-saga.d.ts +0 -20
- package/dist/history/internal/append-saga.d.ts.map +0 -1
- package/dist/history/internal/append-saga.js +0 -35
- package/dist/history/internal/append-saga.js.map +0 -1
- package/dist/history/internal/compensation.d.ts +0 -21
- package/dist/history/internal/compensation.d.ts.map +0 -1
- package/dist/history/internal/compensation.js +0 -84
- package/dist/history/internal/compensation.js.map +0 -1
- package/dist/history/internal/item-mapper.d.ts +0 -12
- package/dist/history/internal/item-mapper.d.ts.map +0 -1
- package/dist/history/internal/item-mapper.js +0 -33
- package/dist/history/internal/item-mapper.js.map +0 -1
- package/dist/history/internal/keys.d.ts +0 -17
- package/dist/history/internal/keys.d.ts.map +0 -1
- package/dist/history/internal/keys.js +0 -49
- package/dist/history/internal/keys.js.map +0 -1
- package/dist/history/internal/message-chunker.d.ts +0 -14
- package/dist/history/internal/message-chunker.d.ts.map +0 -1
- package/dist/history/internal/message-chunker.js +0 -68
- package/dist/history/internal/message-chunker.js.map +0 -1
- package/dist/history/internal/message-transaction.d.ts +0 -26
- package/dist/history/internal/message-transaction.d.ts.map +0 -1
- package/dist/history/internal/message-transaction.js +0 -60
- package/dist/history/internal/message-transaction.js.map +0 -1
- package/dist/history/internal/query.d.ts +0 -10
- package/dist/history/internal/query.d.ts.map +0 -1
- package/dist/history/internal/query.js +0 -31
- package/dist/history/internal/query.js.map +0 -1
- package/dist/history/internal/session-count.d.ts +0 -41
- package/dist/history/internal/session-count.d.ts.map +0 -1
- package/dist/history/internal/session-count.js +0 -109
- package/dist/history/internal/session-count.js.map +0 -1
- package/dist/history/internal/session-title.d.ts +0 -20
- package/dist/history/internal/session-title.d.ts.map +0 -1
- package/dist/history/internal/session-title.js +0 -44
- package/dist/history/internal/session-title.js.map +0 -1
- package/dist/history/internal/session-update.d.ts +0 -28
- package/dist/history/internal/session-update.d.ts.map +0 -1
- package/dist/history/internal/session-update.js +0 -70
- package/dist/history/internal/session-update.js.map +0 -1
- package/dist/history/internal/setup.d.ts.map +0 -1
- package/dist/history/internal/setup.js.map +0 -1
- package/dist/history/internal/title-generator.d.ts +0 -13
- package/dist/history/internal/title-generator.d.ts.map +0 -1
- package/dist/history/internal/title-generator.js +0 -25
- package/dist/history/internal/title-generator.js.map +0 -1
- package/dist/history/internal/ttl-anchor.d.ts +0 -25
- package/dist/history/internal/ttl-anchor.d.ts.map +0 -1
- package/dist/history/internal/ttl-anchor.js +0 -38
- package/dist/history/internal/ttl-anchor.js.map +0 -1
- package/dist/history/internal/validation.d.ts +0 -9
- package/dist/history/internal/validation.d.ts.map +0 -1
- package/dist/history/internal/validation.js +0 -16
- package/dist/history/internal/validation.js.map +0 -1
- package/dist/history/session-adapter.d.ts.map +0 -1
- package/dist/history/session-adapter.js.map +0 -1
- package/dist/history/types.d.ts.map +0 -1
- package/dist/history/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/shared/clock.d.ts.map +0 -1
- package/dist/shared/clock.js.map +0 -1
- package/dist/shared/codec/codec.d.ts.map +0 -1
- package/dist/shared/codec/codec.js.map +0 -1
- package/dist/shared/codec/compression.d.ts.map +0 -1
- package/dist/shared/codec/compression.js.map +0 -1
- package/dist/shared/codec/descriptor-keys.d.ts +0 -4
- package/dist/shared/codec/descriptor-keys.d.ts.map +0 -1
- package/dist/shared/codec/descriptor-keys.js +0 -14
- package/dist/shared/codec/descriptor-keys.js.map +0 -1
- package/dist/shared/codec/json-serde.d.ts.map +0 -1
- package/dist/shared/codec/json-serde.js.map +0 -1
- package/dist/shared/codec/s3/client.d.ts.map +0 -1
- package/dist/shared/codec/s3/client.js.map +0 -1
- package/dist/shared/codec/s3/config.d.ts.map +0 -1
- package/dist/shared/codec/s3/config.js.map +0 -1
- package/dist/shared/codec/s3/delete.d.ts +0 -8
- package/dist/shared/codec/s3/delete.d.ts.map +0 -1
- package/dist/shared/codec/s3/delete.js +0 -29
- package/dist/shared/codec/s3/delete.js.map +0 -1
- package/dist/shared/codec/s3/lifecycle.d.ts.map +0 -1
- package/dist/shared/codec/s3/lifecycle.js.map +0 -1
- package/dist/shared/codec/s3/offloader.d.ts.map +0 -1
- package/dist/shared/codec/s3/offloader.js.map +0 -1
- package/dist/shared/codec/s3/orphans.d.ts +0 -18
- package/dist/shared/codec/s3/orphans.d.ts.map +0 -1
- package/dist/shared/codec/s3/orphans.js +0 -58
- package/dist/shared/codec/s3/orphans.js.map +0 -1
- package/dist/shared/codec/s3/read-write.d.ts +0 -14
- package/dist/shared/codec/s3/read-write.d.ts.map +0 -1
- package/dist/shared/codec/s3/read-write.js +0 -43
- package/dist/shared/codec/s3/read-write.js.map +0 -1
- package/dist/shared/codec/s3/retry.d.ts +0 -5
- package/dist/shared/codec/s3/retry.d.ts.map +0 -1
- package/dist/shared/codec/s3/retry.js +0 -25
- package/dist/shared/codec/s3/retry.js.map +0 -1
- package/dist/shared/constants.d.ts +0 -64
- package/dist/shared/constants.d.ts.map +0 -1
- package/dist/shared/constants.js +0 -67
- package/dist/shared/constants.js.map +0 -1
- package/dist/shared/dynamodb/backoff.d.ts +0 -15
- package/dist/shared/dynamodb/backoff.d.ts.map +0 -1
- package/dist/shared/dynamodb/backoff.js +0 -48
- package/dist/shared/dynamodb/backoff.js.map +0 -1
- package/dist/shared/dynamodb/batch-write.d.ts.map +0 -1
- package/dist/shared/dynamodb/batch-write.js.map +0 -1
- package/dist/shared/dynamodb/cancellation.d.ts.map +0 -1
- package/dist/shared/dynamodb/cancellation.js.map +0 -1
- package/dist/shared/dynamodb/client.d.ts.map +0 -1
- package/dist/shared/dynamodb/client.js.map +0 -1
- package/dist/shared/dynamodb/conditional-put.d.ts +0 -51
- package/dist/shared/dynamodb/conditional-put.d.ts.map +0 -1
- package/dist/shared/dynamodb/conditional-put.js +0 -59
- package/dist/shared/dynamodb/conditional-put.js.map +0 -1
- package/dist/shared/dynamodb/drain-unprocessed.d.ts +0 -19
- package/dist/shared/dynamodb/drain-unprocessed.d.ts.map +0 -1
- package/dist/shared/dynamodb/drain-unprocessed.js +0 -44
- package/dist/shared/dynamodb/drain-unprocessed.js.map +0 -1
- package/dist/shared/dynamodb/paginate-core.d.ts +0 -22
- package/dist/shared/dynamodb/paginate-core.d.ts.map +0 -1
- package/dist/shared/dynamodb/paginate-core.js +0 -52
- package/dist/shared/dynamodb/paginate-core.js.map +0 -1
- package/dist/shared/dynamodb/paginate.d.ts.map +0 -1
- package/dist/shared/dynamodb/paginate.js.map +0 -1
- package/dist/shared/dynamodb/partition-delete.d.ts.map +0 -1
- package/dist/shared/dynamodb/partition-delete.js.map +0 -1
- package/dist/shared/dynamodb/retry-classifier.d.ts +0 -9
- package/dist/shared/dynamodb/retry-classifier.d.ts.map +0 -1
- package/dist/shared/dynamodb/retry-classifier.js +0 -87
- package/dist/shared/dynamodb/retry-classifier.js.map +0 -1
- package/dist/shared/dynamodb/retry.d.ts.map +0 -1
- package/dist/shared/dynamodb/retry.js.map +0 -1
- package/dist/shared/dynamodb/scan.d.ts +0 -15
- package/dist/shared/dynamodb/scan.d.ts.map +0 -1
- package/dist/shared/dynamodb/scan.js +0 -20
- package/dist/shared/dynamodb/scan.js.map +0 -1
- package/dist/shared/dynamodb/types.d.ts +0 -24
- package/dist/shared/dynamodb/types.d.ts.map +0 -1
- package/dist/shared/dynamodb/types.js +0 -3
- package/dist/shared/dynamodb/types.js.map +0 -1
- package/dist/shared/errors/base-error.d.ts.map +0 -1
- package/dist/shared/errors/base-error.js.map +0 -1
- package/dist/shared/errors/error-code.d.ts.map +0 -1
- package/dist/shared/errors/error-code.js.map +0 -1
- package/dist/shared/errors/errors.d.ts.map +0 -1
- package/dist/shared/errors/errors.js.map +0 -1
- package/dist/shared/errors/wrap-error.d.ts +0 -16
- package/dist/shared/errors/wrap-error.d.ts.map +0 -1
- package/dist/shared/errors/wrap-error.js +0 -30
- package/dist/shared/errors/wrap-error.js.map +0 -1
- package/dist/shared/logging/logger.d.ts.map +0 -1
- package/dist/shared/logging/logger.js.map +0 -1
- package/dist/shared/logging/redaction-walk.d.ts +0 -23
- package/dist/shared/logging/redaction-walk.d.ts.map +0 -1
- package/dist/shared/logging/redaction-walk.js +0 -92
- package/dist/shared/logging/redaction-walk.js.map +0 -1
- package/dist/shared/logging/redaction.d.ts.map +0 -1
- package/dist/shared/logging/redaction.js.map +0 -1
- package/dist/shared/logging/secret-patterns.d.ts.map +0 -1
- package/dist/shared/logging/secret-patterns.js.map +0 -1
- package/dist/shared/options.d.ts.map +0 -1
- package/dist/shared/options.js.map +0 -1
- package/dist/shared/ulid.d.ts.map +0 -1
- package/dist/shared/ulid.js.map +0 -1
- package/dist/shared/validation/primitives.d.ts.map +0 -1
- package/dist/shared/validation/primitives.js.map +0 -1
- package/dist/shared/validation/ttl.d.ts.map +0 -1
- package/dist/shared/validation/ttl.js.map +0 -1
- package/dist/store/actions/get.d.ts +0 -5
- package/dist/store/actions/get.d.ts.map +0 -1
- package/dist/store/actions/get.js +0 -35
- package/dist/store/actions/get.js.map +0 -1
- package/dist/store/actions/list-namespaces.d.ts.map +0 -1
- package/dist/store/actions/list-namespaces.js.map +0 -1
- package/dist/store/actions/put.d.ts.map +0 -1
- package/dist/store/actions/put.js.map +0 -1
- package/dist/store/actions/reconcile-vector-index.d.ts.map +0 -1
- package/dist/store/actions/reconcile-vector-index.js.map +0 -1
- package/dist/store/actions/search.d.ts.map +0 -1
- package/dist/store/actions/search.js.map +0 -1
- package/dist/store/internal/backend-search.d.ts +0 -5
- package/dist/store/internal/backend-search.d.ts.map +0 -1
- package/dist/store/internal/backend-search.js +0 -68
- package/dist/store/internal/backend-search.js.map +0 -1
- package/dist/store/internal/filter.d.ts.map +0 -1
- package/dist/store/internal/filter.js.map +0 -1
- package/dist/store/internal/index-reconcile.d.ts +0 -22
- package/dist/store/internal/index-reconcile.d.ts.map +0 -1
- package/dist/store/internal/index-reconcile.js +0 -105
- package/dist/store/internal/index-reconcile.js.map +0 -1
- package/dist/store/internal/index-sync.d.ts +0 -11
- package/dist/store/internal/index-sync.d.ts.map +0 -1
- package/dist/store/internal/index-sync.js +0 -26
- package/dist/store/internal/index-sync.js.map +0 -1
- package/dist/store/internal/item-mapper.d.ts +0 -25
- package/dist/store/internal/item-mapper.d.ts.map +0 -1
- package/dist/store/internal/item-mapper.js +0 -53
- package/dist/store/internal/item-mapper.js.map +0 -1
- package/dist/store/internal/keys.d.ts +0 -18
- package/dist/store/internal/keys.d.ts.map +0 -1
- package/dist/store/internal/keys.js +0 -42
- package/dist/store/internal/keys.js.map +0 -1
- package/dist/store/internal/namespace-match.d.ts +0 -12
- package/dist/store/internal/namespace-match.d.ts.map +0 -1
- package/dist/store/internal/namespace-match.js +0 -41
- package/dist/store/internal/namespace-match.js.map +0 -1
- package/dist/store/internal/overwrite-swap.d.ts +0 -33
- package/dist/store/internal/overwrite-swap.d.ts.map +0 -1
- package/dist/store/internal/overwrite-swap.js +0 -62
- package/dist/store/internal/overwrite-swap.js.map +0 -1
- package/dist/store/internal/persist.d.ts +0 -27
- package/dist/store/internal/persist.d.ts.map +0 -1
- package/dist/store/internal/persist.js +0 -59
- package/dist/store/internal/persist.js.map +0 -1
- package/dist/store/internal/query.d.ts +0 -6
- package/dist/store/internal/query.d.ts.map +0 -1
- package/dist/store/internal/query.js +0 -32
- package/dist/store/internal/query.js.map +0 -1
- package/dist/store/internal/ranker.d.ts +0 -13
- package/dist/store/internal/ranker.d.ts.map +0 -1
- package/dist/store/internal/ranker.js +0 -31
- package/dist/store/internal/ranker.js.map +0 -1
- package/dist/store/internal/read-existing.d.ts +0 -19
- package/dist/store/internal/read-existing.d.ts.map +0 -1
- package/dist/store/internal/read-existing.js +0 -29
- package/dist/store/internal/read-existing.js.map +0 -1
- package/dist/store/internal/score-direction.d.ts +0 -32
- package/dist/store/internal/score-direction.d.ts.map +0 -1
- package/dist/store/internal/score-direction.js +0 -39
- package/dist/store/internal/score-direction.js.map +0 -1
- package/dist/store/internal/search-filter.d.ts +0 -4
- package/dist/store/internal/search-filter.d.ts.map +0 -1
- package/dist/store/internal/search-filter.js +0 -11
- package/dist/store/internal/search-filter.js.map +0 -1
- package/dist/store/internal/semantic-search.d.ts.map +0 -1
- package/dist/store/internal/semantic-search.js.map +0 -1
- package/dist/store/internal/setup.d.ts.map +0 -1
- package/dist/store/internal/setup.js.map +0 -1
- package/dist/store/internal/validation.d.ts +0 -13
- package/dist/store/internal/validation.d.ts.map +0 -1
- package/dist/store/internal/validation.js +0 -35
- package/dist/store/internal/validation.js.map +0 -1
- package/dist/store/internal/write-verify.d.ts +0 -37
- package/dist/store/internal/write-verify.d.ts.map +0 -1
- package/dist/store/internal/write-verify.js +0 -68
- package/dist/store/internal/write-verify.js.map +0 -1
- package/dist/store/store.d.ts.map +0 -1
- package/dist/store/store.js.map +0 -1
- package/dist/store/types.d.ts.map +0 -1
- package/dist/store/types.js.map +0 -1
- package/dist/store/vector-backend.d.ts.map +0 -1
- package/dist/store/vector-backend.js.map +0 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hides the SESSION row: what it carries and how each field changes.
|
|
4
|
+
*
|
|
5
|
+
* A session's metadata — `messageCount`, `title`, `createdAt`, `updatedAt`,
|
|
6
|
+
* `ttl`, the `writeId` of the last append, and the recency-index keys — lives
|
|
7
|
+
* on one row, and `messageCount` is a copy of a fact the message rows hold.
|
|
8
|
+
* Keeping the copy in step is this module's job and nobody else's: the append
|
|
9
|
+
* adds to it in the same transaction that writes the messages, a rolled-back
|
|
10
|
+
* append subtracts exactly what it added (and never from a later incarnation of
|
|
11
|
+
* the session), a repair recomputes it under a compare-and-swap, and a listing
|
|
12
|
+
* reads it back only from a row this release can summarise. The recency-index
|
|
13
|
+
* keys are the one field this module does not write alone: `sessionIndexTarget`
|
|
14
|
+
* below tells `src/backfill/backfill.ts` which row is a SESSION row, and
|
|
15
|
+
* backfill writes the keys onto it directly, for a row that predates the index.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.revertSessionCount = revertSessionCount;
|
|
19
|
+
exports.revertSessionCreation = revertSessionCreation;
|
|
20
|
+
exports.buildSessionUpdate = buildSessionUpdate;
|
|
21
|
+
exports.removeRolledBackTitle = removeRolledBackTitle;
|
|
22
|
+
exports.resolveTtlAnchor = resolveTtlAnchor;
|
|
23
|
+
exports.deriveTitle = deriveTitle;
|
|
24
|
+
exports.repairMessageCount = repairMessageCount;
|
|
25
|
+
exports.summariseSession = summariseSession;
|
|
26
|
+
exports.sessionIndexTarget = sessionIndexTarget;
|
|
27
|
+
const clock_1 = require("../../shared/clock");
|
|
28
|
+
const cancellation_1 = require("../../shared/dynamodb/cancellation");
|
|
29
|
+
const idempotent_write_1 = require("../../shared/dynamodb/idempotent-write");
|
|
30
|
+
const recency_index_1 = require("../../shared/dynamodb/recency-index");
|
|
31
|
+
const retry_1 = require("../../shared/dynamodb/retry");
|
|
32
|
+
const table_schema_1 = require("../../shared/dynamodb/table-schema");
|
|
33
|
+
const classify_1 = require("../../shared/errors/classify");
|
|
34
|
+
const error_code_1 = require("../../shared/errors/error-code");
|
|
35
|
+
const errors_1 = require("../../shared/errors/errors");
|
|
36
|
+
const message_read_1 = require("./message-read");
|
|
37
|
+
const rows_1 = require("./rows");
|
|
38
|
+
/**
|
|
39
|
+
* True when the transaction's first item — the SESSION row update or delete,
|
|
40
|
+
* since both callers below send a single-item transaction — failed its
|
|
41
|
+
* ConditionExpression. Named distinctly from
|
|
42
|
+
* shared/dynamodb/idempotent-write.ts's isConditionalCheckFailed, which
|
|
43
|
+
* classifies a rejection (a PutItem exception or a cancelled transaction's
|
|
44
|
+
* reasons alike) without asking which item caused it — sharing that name
|
|
45
|
+
* would be a real trap for whoever read one assuming it was the other.
|
|
46
|
+
*/
|
|
47
|
+
function isCancelledByCondition(error) {
|
|
48
|
+
return (0, cancellation_1.conditionFailedAt)(error, 0);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Subtract a count this append already added from the session, leaving it consistent.
|
|
52
|
+
* Guarded so a concurrently-deleted SESSION row is never resurrected as a
|
|
53
|
+
* permanent, ttl-less junk row: if the row is already gone there is nothing
|
|
54
|
+
* to revert, so that specific condition failure is swallowed rather than
|
|
55
|
+
* surfaced — this runs only from an already-in-progress rollback, where a
|
|
56
|
+
* spurious error for a no-op would misrepresent what happened.
|
|
57
|
+
*
|
|
58
|
+
* The same condition also pins the *incarnation*: `createdAt <= createdBefore`
|
|
59
|
+
* (this call's own append timestamp). A session `clear()`-ed and re-created by
|
|
60
|
+
* another caller between this call's commit and its rollback carries a later
|
|
61
|
+
* `createdAt`; decrementing it would corrupt the new incarnation's count, and
|
|
62
|
+
* its rows were never this call's to revert. That rejection is swallowed too,
|
|
63
|
+
* for the same reason as a vanished row.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately does not revert a `forceTtlRefresh`-driven ttl SET from an
|
|
66
|
+
* earlier committed chunk: the healed anchor is never shorter than what
|
|
67
|
+
* was there before, so leaving it in place after a rollback only means the
|
|
68
|
+
* session's metadata row outlives its content a bit longer than ideal —
|
|
69
|
+
* self-healing (the next successful append, or DynamoDB's own TTL sweep,
|
|
70
|
+
* resolves it), unlike reverting, which would need to re-check for a
|
|
71
|
+
* concurrent legitimate extension to avoid regressing it. See README.md's
|
|
72
|
+
* "TTL expiry" section.
|
|
73
|
+
*
|
|
74
|
+
* Accepts: `delta` — how many messages to subtract; `0` is a no-op and spends
|
|
75
|
+
* no write. `createdBefore` — this call's own append timestamp, which pins the
|
|
76
|
+
* incarnation.
|
|
77
|
+
*
|
|
78
|
+
* Returns: nothing, whether the decrement applied or the guard correctly
|
|
79
|
+
* refused it.
|
|
80
|
+
*
|
|
81
|
+
* Throws: whatever the write throws other than its own condition failure. A
|
|
82
|
+
* vanished row and a newer incarnation are both "nothing of mine to revert",
|
|
83
|
+
* not errors — this runs from an in-progress rollback, where a spurious error
|
|
84
|
+
* for a no-op would misrepresent what happened.
|
|
85
|
+
*
|
|
86
|
+
* Guarantees: the decrement is applied at most once, however often the request
|
|
87
|
+
* is re-sent. `ADD #count :neg` is one of the two writes in this package that
|
|
88
|
+
* are not naturally idempotent — the append's own `ADD #count :n` is the
|
|
89
|
+
* other — and applied twice it subtracts twice, with nothing reading the row
|
|
90
|
+
* back afterwards to notice, so a re-sent attempt must be answered from
|
|
91
|
+
* DynamoDB's idempotency cache rather than re-evaluated. Two things hold
|
|
92
|
+
* that together and only together: the `ClientRequestToken`, which makes a
|
|
93
|
+
* re-send a no-op, and the deadline of `MAX_WRITE_LIFETIME_MS`, which
|
|
94
|
+
* stops the retrying while that token is still honoured. Nothing in the token
|
|
95
|
+
* enforces that window — the service honours it for its own ten minutes
|
|
96
|
+
* whatever a caller's retry policy says — so without the deadline a long
|
|
97
|
+
* policy could still be retrying after the window closed, and the re-send
|
|
98
|
+
* would then land as a second subtraction, leaving `messageCount` quietly
|
|
99
|
+
* wrong. `reconcileMessageCount` is the repair if that happens anyway: it
|
|
100
|
+
* recounts the live messages and writes the true total back.
|
|
101
|
+
*
|
|
102
|
+
* The guard is no second line of defence for it. An attempt the condition
|
|
103
|
+
* turns away commits nothing, so DynamoDB caches no result for that attempt's
|
|
104
|
+
* token and a retry is a fresh evaluation rather than a deduplicated one — the
|
|
105
|
+
* exactly-once promise holds for an attempt that **committed**, which is also
|
|
106
|
+
* the only attempt whose re-send could subtract twice.
|
|
107
|
+
*/
|
|
108
|
+
async function revertSessionCount(context, sessionId, delta, createdBefore) {
|
|
109
|
+
if (delta === 0)
|
|
110
|
+
return;
|
|
111
|
+
const update = {
|
|
112
|
+
TableName: context.tableName,
|
|
113
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
114
|
+
UpdateExpression: 'ADD #count :neg',
|
|
115
|
+
ConditionExpression: `attribute_exists(${table_schema_1.PARTITION_KEY_ATTRIBUTE}) AND #c <= :now`,
|
|
116
|
+
ExpressionAttributeNames: { '#count': 'messageCount', '#c': 'createdAt' },
|
|
117
|
+
ExpressionAttributeValues: { ':neg': -delta, ':now': createdBefore },
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await (0, idempotent_write_1.transactIdempotently)(context, [{ Update: update }]);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (isCancelledByCondition(error))
|
|
124
|
+
return;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Undo a rolled-back append's effect on the session row.
|
|
130
|
+
*
|
|
131
|
+
* When this call is the one that created the row — `createdAt` still equals
|
|
132
|
+
* this call's timestamp, and the only messages counted on it are the ones
|
|
133
|
+
* being reverted — the whole row is deleted. Without that, a failed first
|
|
134
|
+
* append left a "ghost session": `title`, `createdAt` and `sessionId` are all
|
|
135
|
+
* written via `if_not_exists`, so they were never reverted and never set
|
|
136
|
+
* again, leaving `listSessions()` reporting a session with `messageCount: 0`
|
|
137
|
+
* whose title still held up to 80 characters of a message the caller was told
|
|
138
|
+
* had not persisted, with no API to clear it.
|
|
139
|
+
*
|
|
140
|
+
* Both conditions are load-bearing. `createdAt = :now` establishes that this
|
|
141
|
+
* call created the row; `messageCount = :total` establishes that nothing else
|
|
142
|
+
* has added to it since. A concurrent append to the same brand-new session
|
|
143
|
+
* fails the count check, because deleting the row would destroy that caller's
|
|
144
|
+
* committed messages — so it falls through to the plain decrement, and then
|
|
145
|
+
* strips just the title this call contributed, which is the only part of the
|
|
146
|
+
* row still carrying rolled-back message content.
|
|
147
|
+
*
|
|
148
|
+
* Accepts: `created` — what this call contributed to the row. `created.total`
|
|
149
|
+
* — every message this call counted onto the row; `0` is a no-op.
|
|
150
|
+
* `created.createdAt` — this call's timestamp, which is what "I created this
|
|
151
|
+
* row" means here. `created.title` — the title this call may have contributed.
|
|
152
|
+
*
|
|
153
|
+
* Returns: nothing. The row is deleted, or decremented and stripped of this
|
|
154
|
+
* call's title; both are a complete undo of what this call contributed.
|
|
155
|
+
*
|
|
156
|
+
* Throws: whatever the writes throw other than their own condition failures.
|
|
157
|
+
*
|
|
158
|
+
* Guarantees: the delete is applied at most once, and inside the window its
|
|
159
|
+
* token is honoured for, a re-send of an attempt that **committed** is
|
|
160
|
+
* answered from DynamoDB's idempotency cache rather than re-evaluated. The
|
|
161
|
+
* condition would already stop such a re-send from removing anything it should
|
|
162
|
+
* not; what the token adds is that it comes back as the success it was.
|
|
163
|
+
* Re-evaluated instead, it finds the row gone, fails both equalities, and the
|
|
164
|
+
* cancellation is read below as "a concurrent append has added to this row" —
|
|
165
|
+
* sending a rollback that already completed down the decrement-and-strip path
|
|
166
|
+
* meant for the case where the row survived. Both writes on that path are
|
|
167
|
+
* themselves guarded, and the decrement's incarnation pin refuses a session
|
|
168
|
+
* re-created in the meantime, so the price is two spurious conditional writes
|
|
169
|
+
* rather than a wrong count; the token is what keeps them from being spent.
|
|
170
|
+
*
|
|
171
|
+
* A rejection carries no idempotency forward — a cancelled attempt commits
|
|
172
|
+
* nothing, so nothing is cached for its token — and here that is exactly the
|
|
173
|
+
* wanted behaviour, since the fall-through below is a fresh decision about
|
|
174
|
+
* what to do instead. The deadline drawn beside the token is what keeps the
|
|
175
|
+
* retrying inside that window; the token enforces no window of its own.
|
|
176
|
+
*/
|
|
177
|
+
async function revertSessionCreation(context, sessionId, created) {
|
|
178
|
+
if (created.total === 0)
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
await (0, idempotent_write_1.transactIdempotently)(context, [
|
|
182
|
+
{
|
|
183
|
+
Delete: {
|
|
184
|
+
TableName: context.tableName,
|
|
185
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
186
|
+
ConditionExpression: '#count = :total AND #c = :now',
|
|
187
|
+
ExpressionAttributeNames: { '#count': 'messageCount', '#c': 'createdAt' },
|
|
188
|
+
ExpressionAttributeValues: { ':total': created.total, ':now': created.createdAt },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (!isCancelledByCondition(error))
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
await revertSessionCount(context, sessionId, created.total, created.createdAt);
|
|
199
|
+
if (created.title !== undefined) {
|
|
200
|
+
await removeRolledBackTitle(context, sessionId, created.createdAt, created.title);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build the metadata `Update` transact-item: `ADD` the message count and `SET`
|
|
205
|
+
* `updatedAt` and the appending write's id every time, while `createdAt`,
|
|
206
|
+
* `sessionId`, `title`, and the `ttl` anchor are written once via
|
|
207
|
+
* `if_not_exists`. Folding the `ttl` anchor in here means the first append
|
|
208
|
+
* fixes one shared expiry atomically with the count, with no separate
|
|
209
|
+
* pre-write that could orphan a metadata-only row. When
|
|
210
|
+
* `forceTtlRefresh` is set (because {@link resolveTtlAnchor} found the persisted
|
|
211
|
+
* anchor missing or already expired), the `ttl` clause instead does a plain
|
|
212
|
+
* `SET`, so the SESSION row's own stale attribute actually heals instead of
|
|
213
|
+
* being permanently blocked by `if_not_exists`. When forceTtlRefresh is set,
|
|
214
|
+
* the SET is additionally guarded by a ConditionExpression so a concurrent
|
|
215
|
+
* caller's already-healed anchor can never be regressed backward — see
|
|
216
|
+
* append.ts for how a lost race is retried without forcing.
|
|
217
|
+
*
|
|
218
|
+
* Accepts: `count` — how many messages this append adds, which `ADD` applies to
|
|
219
|
+
* whatever the row holds, so two concurrent appends both count. `title` —
|
|
220
|
+
* written once and never overwritten, so a session keeps the title its first
|
|
221
|
+
* turn produced. `writeId` — the appending write's own id, rewritten on every
|
|
222
|
+
* append so the row always names the write that last added to it.
|
|
223
|
+
* `ttlTimestamp` — absent leaves the row without an expiry. `forceTtlRefresh` —
|
|
224
|
+
* see above.
|
|
225
|
+
*
|
|
226
|
+
* Returns: the `Update` transact-item. It creates the row when there is none:
|
|
227
|
+
* every once-only field is an `if_not_exists`, so the first append and the
|
|
228
|
+
* thousandth build the same item.
|
|
229
|
+
*
|
|
230
|
+
* Throws: nothing. The condition it carries is evaluated by DynamoDB, and a
|
|
231
|
+
* failed condition surfaces from the transaction, not from here.
|
|
232
|
+
*/
|
|
233
|
+
function buildSessionUpdate(tableName, fields) {
|
|
234
|
+
const index = (0, recency_index_1.indexKeys)('SESS', fields.sessionId, fields.now, fields.indexShards ?? recency_index_1.DEFAULT_INDEX_SHARDS);
|
|
235
|
+
const names = {
|
|
236
|
+
'#count': 'messageCount',
|
|
237
|
+
'#u': 'updatedAt',
|
|
238
|
+
'#c': 'createdAt',
|
|
239
|
+
'#sid': 'sessionId',
|
|
240
|
+
'#wid': 'writeId',
|
|
241
|
+
'#v': 'v',
|
|
242
|
+
'#gpk': 'gsi1pk',
|
|
243
|
+
'#gsk': 'gsi1sk',
|
|
244
|
+
};
|
|
245
|
+
const values = {
|
|
246
|
+
':n': fields.count,
|
|
247
|
+
':u': fields.now,
|
|
248
|
+
':c': fields.now,
|
|
249
|
+
':sid': fields.sessionId,
|
|
250
|
+
':wid': fields.writeId,
|
|
251
|
+
':v': table_schema_1.ROW_FORMAT_VERSION,
|
|
252
|
+
':gpk': index.gsi1pk,
|
|
253
|
+
':gsk': index.gsi1sk,
|
|
254
|
+
};
|
|
255
|
+
// The row's format version is rewritten on every update, not only on
|
|
256
|
+
// creation: an append by this version leaves a row this version wrote, and a
|
|
257
|
+
// reader must be told that rather than infer it from which attributes happen
|
|
258
|
+
// to be present.
|
|
259
|
+
const sets = [
|
|
260
|
+
'#u = :u',
|
|
261
|
+
'#c = if_not_exists(#c, :c)',
|
|
262
|
+
'#sid = if_not_exists(#sid, :sid)',
|
|
263
|
+
// An unconditional `SET`, deliberately unlike the three `if_not_exists`
|
|
264
|
+
// clauses around it. This is not a once-only field: its whole content is
|
|
265
|
+
// that it moves. Written in their style it would stamp the id when the
|
|
266
|
+
// session was created and never again, and a delete pinning on the id it
|
|
267
|
+
// observed would be present, well formed, and always pass — which is the
|
|
268
|
+
// failure it exists to prevent.
|
|
269
|
+
'#wid = :wid',
|
|
270
|
+
'#v = :v',
|
|
271
|
+
// The session row is listed by recency across partitions, so it carries the
|
|
272
|
+
// index keys — rewritten on every append, which is what keeps "most
|
|
273
|
+
// recently updated first" true without an in-memory sort.
|
|
274
|
+
'#gpk = :gpk',
|
|
275
|
+
'#gsk = :gsk',
|
|
276
|
+
];
|
|
277
|
+
if (fields.title !== undefined) {
|
|
278
|
+
names['#title'] = 'title';
|
|
279
|
+
values[':title'] = fields.title;
|
|
280
|
+
sets.push('#title = if_not_exists(#title, :title)');
|
|
281
|
+
}
|
|
282
|
+
let conditionExpression;
|
|
283
|
+
if (fields.ttlTimestamp !== undefined) {
|
|
284
|
+
names['#ttl'] = 'ttl';
|
|
285
|
+
values[':ttl'] = fields.ttlTimestamp;
|
|
286
|
+
if (fields.forceTtlRefresh) {
|
|
287
|
+
sets.push('#ttl = :ttl');
|
|
288
|
+
// Guards the force-overwrite so a concurrent caller's already-healed,
|
|
289
|
+
// equal-or-later anchor can never be regressed backward by this one.
|
|
290
|
+
// `<=` (not `<`): two concurrent healers of the same stale anchor
|
|
291
|
+
// typically compute the identical target timestamp, and `<=` lets
|
|
292
|
+
// the second one succeed by re-applying the same value instead of
|
|
293
|
+
// failing the condition and paying a full transaction retry for a
|
|
294
|
+
// write that was never actually a regression.
|
|
295
|
+
conditionExpression = 'attribute_not_exists(#ttl) OR #ttl <= :ttl';
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
sets.push('#ttl = if_not_exists(#ttl, :ttl)');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
Update: {
|
|
303
|
+
TableName: tableName,
|
|
304
|
+
Key: (0, rows_1.sessionRowKey)(fields.sessionId),
|
|
305
|
+
UpdateExpression: `ADD #count :n SET ${sets.join(', ')}`,
|
|
306
|
+
...(conditionExpression ? { ConditionExpression: conditionExpression } : {}),
|
|
307
|
+
ExpressionAttributeNames: names,
|
|
308
|
+
ExpressionAttributeValues: values,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/** True when a plain UpdateItem was turned away by its ConditionExpression. */
|
|
313
|
+
function isConditionRejected(error) {
|
|
314
|
+
return (0, classify_1.classifyAwsError)(error) === error_code_1.ErrorCode.CONDITION_CONFLICT;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Strip a title this call contributed to a session row it created, when the
|
|
318
|
+
* row itself could not be deleted because a concurrent append has since added
|
|
319
|
+
* messages to it.
|
|
320
|
+
*
|
|
321
|
+
* Without this, that narrow window reopens exactly the leak C4 closes: the
|
|
322
|
+
* title is derived from the first human message of an append the caller was
|
|
323
|
+
* told had failed, and `if_not_exists` means nothing ever overwrites it — so
|
|
324
|
+
* a row that now belongs to a different caller keeps up to 80 characters of
|
|
325
|
+
* rolled-back message content.
|
|
326
|
+
*
|
|
327
|
+
* Both guards are load-bearing. `createdAt = :now` establishes that this call
|
|
328
|
+
* created the row, and `title = :title` that the title on it is still the one
|
|
329
|
+
* this call wrote — so a pre-existing title, or one a concurrent caller won
|
|
330
|
+
* the `if_not_exists` race for, is never removed. A condition rejection means
|
|
331
|
+
* exactly that and is not an error; anything else is.
|
|
332
|
+
*
|
|
333
|
+
* Accepts: `createdAt` — this call's own timestamp. `title` — the exact string
|
|
334
|
+
* this call wrote, compared as a value so nothing else's title is removed.
|
|
335
|
+
*
|
|
336
|
+
* Returns: nothing, whether the title was removed or the guards correctly
|
|
337
|
+
* refused.
|
|
338
|
+
*
|
|
339
|
+
* Throws: whatever the update throws other than its own condition failure.
|
|
340
|
+
*/
|
|
341
|
+
async function removeRolledBackTitle(context, sessionId, createdAt, title) {
|
|
342
|
+
try {
|
|
343
|
+
await (0, retry_1.withDynamoDBRetry)((request) => context.client.update({
|
|
344
|
+
TableName: context.tableName,
|
|
345
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
346
|
+
UpdateExpression: 'REMOVE #title',
|
|
347
|
+
ConditionExpression: '#c = :now AND #title = :title',
|
|
348
|
+
ExpressionAttributeNames: { '#title': 'title', '#c': 'createdAt' },
|
|
349
|
+
ExpressionAttributeValues: { ':now': createdAt, ':title': title },
|
|
350
|
+
}, request), context.retry);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (isConditionRejected(error))
|
|
354
|
+
return;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Resolve the session's creation-anchored TTL: the value already stored on the
|
|
360
|
+
* SESSION item, if one exists AND is still in the future; otherwise the
|
|
361
|
+
* supplied `candidate`, with `refresh: true` so the caller force-overwrites
|
|
362
|
+
* the stale/missing persisted anchor instead of leaving it stuck (DynamoDB's
|
|
363
|
+
* `if_not_exists` would otherwise never correct an already-expired anchor).
|
|
364
|
+
* This is a strongly-consistent read, never a write, so it cannot leave a
|
|
365
|
+
* metadata-only orphan row when the following append transaction fails.
|
|
366
|
+
*
|
|
367
|
+
* When a stale anchor is healed, only the persisted SESSION row's `ttl` is
|
|
368
|
+
* force-refreshed — the message rows already written under the expired
|
|
369
|
+
* anchor keep their own (already-expired) `ttl` and get swept independently
|
|
370
|
+
* by DynamoDB's TTL sweep. Until that sweep runs (and until
|
|
371
|
+
* `reconcileMessageCount` repairs the count), `messageCount` can therefore be
|
|
372
|
+
* temporarily overstated relative to what `getMessages` actually returns.
|
|
373
|
+
* This is expected, not a bug.
|
|
374
|
+
*
|
|
375
|
+
* Accepts: `candidate` — the anchor this append would use if the session has
|
|
376
|
+
* none, already computed from the configured ttl.
|
|
377
|
+
*
|
|
378
|
+
* Returns: the anchor to stamp on this append's messages, and whether the
|
|
379
|
+
* SESSION row's own `ttl` must be force-overwritten rather than left to
|
|
380
|
+
* `if_not_exists`.
|
|
381
|
+
*
|
|
382
|
+
* Throws: whatever the read throws after retries.
|
|
383
|
+
*
|
|
384
|
+
* Guarantees: a read, never a write — so a failure of the append that follows
|
|
385
|
+
* cannot leave a metadata-only orphan row behind. Strongly consistent, so an
|
|
386
|
+
* anchor an earlier append committed is always seen. Two appends that start
|
|
387
|
+
* together on a session that has none each propose their own candidate; the
|
|
388
|
+
* append transaction's own condition is what settles which persists, so this
|
|
389
|
+
* read never has to be the arbiter (see {@link buildSessionUpdate}).
|
|
390
|
+
*/
|
|
391
|
+
async function resolveTtlAnchor(context, sessionId, candidate, signal) {
|
|
392
|
+
const result = await (0, retry_1.withDynamoDBRetry)((request) => context.client.get({
|
|
393
|
+
TableName: context.tableName,
|
|
394
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
395
|
+
ConsistentRead: true,
|
|
396
|
+
ProjectionExpression: '#ttl',
|
|
397
|
+
ExpressionAttributeNames: { '#ttl': 'ttl' },
|
|
398
|
+
}, request), (0, retry_1.retryFor)(context, signal));
|
|
399
|
+
const ttl = result.Item?.ttl;
|
|
400
|
+
if (typeof ttl === 'number' && ttl > (0, clock_1.nowSeconds)()) {
|
|
401
|
+
return { ttlTimestamp: ttl, refresh: false };
|
|
402
|
+
}
|
|
403
|
+
return { ttlTimestamp: candidate, refresh: true };
|
|
404
|
+
}
|
|
405
|
+
const MAX_TITLE_LENGTH = 80;
|
|
406
|
+
function isTextBlock(block) {
|
|
407
|
+
const candidate = block;
|
|
408
|
+
return candidate.type === 'text' && typeof candidate.text === 'string';
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The human-readable text of a message's `content`: the string itself, or the
|
|
412
|
+
* first `text` block of a content-block array (a multimodal message carries
|
|
413
|
+
* image and text blocks side by side). Undefined when neither yields text.
|
|
414
|
+
*/
|
|
415
|
+
function textOf(content) {
|
|
416
|
+
if (typeof content === 'string')
|
|
417
|
+
return content;
|
|
418
|
+
if (!Array.isArray(content))
|
|
419
|
+
return undefined;
|
|
420
|
+
const block = content.find((entry) => typeof entry === 'object' && entry !== null && isTextBlock(entry));
|
|
421
|
+
return block?.text;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Derive a session title from the first human message's text content, at most
|
|
425
|
+
* {@link MAX_TITLE_LENGTH} characters including the ellipsis. Returns undefined
|
|
426
|
+
* when there is no usable text.
|
|
427
|
+
*
|
|
428
|
+
* Truncation counts *code points*, not UTF-16 code units: slicing by index
|
|
429
|
+
* could cut a surrogate pair in half, leaving a lone surrogate that no longer
|
|
430
|
+
* round-trips through UTF-8. The ellipsis is also counted against the maximum
|
|
431
|
+
* rather than appended past it.
|
|
432
|
+
*
|
|
433
|
+
* Accepts: `messages` — one append's messages, in order. The first `human` one
|
|
434
|
+
* is the title's source; an append of only AI or tool messages has none, and an
|
|
435
|
+
* append to an existing session produces a title that is then discarded by
|
|
436
|
+
* `if_not_exists`.
|
|
437
|
+
*
|
|
438
|
+
* Returns: the title, or undefined when there is no usable text — no human
|
|
439
|
+
* message, empty content, or a multimodal message carrying no text block.
|
|
440
|
+
* Undefined means "do not write a title", not "write an empty one".
|
|
441
|
+
*
|
|
442
|
+
* Throws: nothing. A title is a convenience; nothing about it may fail an
|
|
443
|
+
* append.
|
|
444
|
+
*/
|
|
445
|
+
function deriveTitle(messages) {
|
|
446
|
+
const firstHuman = messages.find((message) => message.type === 'human');
|
|
447
|
+
const content = firstHuman === undefined ? undefined : textOf(firstHuman.data.content);
|
|
448
|
+
if (content === undefined || content.length === 0)
|
|
449
|
+
return undefined;
|
|
450
|
+
const codePoints = [...content];
|
|
451
|
+
if (codePoints.length <= MAX_TITLE_LENGTH)
|
|
452
|
+
return content;
|
|
453
|
+
return `${codePoints.slice(0, MAX_TITLE_LENGTH - 1).join('')}…`;
|
|
454
|
+
}
|
|
455
|
+
/** Read the count this repair is about to replace, strongly consistently. */
|
|
456
|
+
async function observeCount(context, sessionId, signal) {
|
|
457
|
+
const result = await (0, retry_1.withDynamoDBRetry)((request) => context.client.get({
|
|
458
|
+
TableName: context.tableName,
|
|
459
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
460
|
+
ConsistentRead: true,
|
|
461
|
+
ProjectionExpression: '#count',
|
|
462
|
+
ExpressionAttributeNames: { '#count': 'messageCount' },
|
|
463
|
+
}, request), (0, retry_1.retryFor)(context, signal));
|
|
464
|
+
if (!result.Item)
|
|
465
|
+
return { exists: false };
|
|
466
|
+
const count = result.Item.messageCount;
|
|
467
|
+
return typeof count === 'number' ? { exists: true, count } : { exists: true };
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* The condition admitting the repair only while the row still holds the count
|
|
471
|
+
* it was computed against. A row written before the attribute existed carries
|
|
472
|
+
* none, and pinning its *absence* is what makes the guard correct there too.
|
|
473
|
+
*/
|
|
474
|
+
function countGuard(observed) {
|
|
475
|
+
if (observed.count === undefined) {
|
|
476
|
+
return {
|
|
477
|
+
ConditionExpression: `attribute_exists(${table_schema_1.PARTITION_KEY_ATTRIBUTE}) AND attribute_not_exists(#count)`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
ConditionExpression: `attribute_exists(${table_schema_1.PARTITION_KEY_ATTRIBUTE}) AND #count = :expected`,
|
|
482
|
+
ExpressionAttributeValues: { ':expected': observed.count },
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Write the recomputed count, pinned to what the row held when it was computed. */
|
|
486
|
+
async function writeCount(context, sessionId, repair, signal) {
|
|
487
|
+
const guard = countGuard(repair.observed);
|
|
488
|
+
await (0, retry_1.withDynamoDBRetry)((request) => context.client.update({
|
|
489
|
+
TableName: context.tableName,
|
|
490
|
+
Key: (0, rows_1.sessionRowKey)(sessionId),
|
|
491
|
+
UpdateExpression: 'SET #count = :count',
|
|
492
|
+
ExpressionAttributeNames: { '#count': 'messageCount' },
|
|
493
|
+
ExpressionAttributeValues: { ':count': repair.count, ...guard.ExpressionAttributeValues },
|
|
494
|
+
ConditionExpression: guard.ConditionExpression,
|
|
495
|
+
}, request), (0, retry_1.retryFor)(context, signal));
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Recompute `messageCount` from the stored message rows and write it, pinned to
|
|
499
|
+
* the count the row held when the recount began.
|
|
500
|
+
*
|
|
501
|
+
* Accepts: `sessionId` — parsed. `signal` — cancels the reads, the recount and
|
|
502
|
+
* the write.
|
|
503
|
+
*
|
|
504
|
+
* Returns: the count written.
|
|
505
|
+
*
|
|
506
|
+
* Throws: `CONDITION_CONFLICT` when the session does not exist, or when it
|
|
507
|
+
* changed during every one of the compare-and-swap's attempts;
|
|
508
|
+
* `FORMAT_UNSUPPORTED` for a message row a newer release wrote; `VALIDATION`
|
|
509
|
+
* naming `message` for a row in the message key space this package did not
|
|
510
|
+
* write; whatever the reads and the write throw.
|
|
511
|
+
*/
|
|
512
|
+
async function repairMessageCount(context, sessionId, signal) {
|
|
513
|
+
for (let attempt = 1; attempt <= idempotent_write_1.OVERWRITE_CAS_MAX_ATTEMPTS; attempt++) {
|
|
514
|
+
const observed = await observeCount(context, sessionId, signal);
|
|
515
|
+
if (!observed.exists) {
|
|
516
|
+
throw (0, errors_1.conflictError)(`Cannot reconcile messageCount: session "${sessionId}" does not exist`);
|
|
517
|
+
}
|
|
518
|
+
const count = await (0, message_read_1.countLiveMessages)(context, sessionId, signal);
|
|
519
|
+
try {
|
|
520
|
+
await writeCount(context, sessionId, { count, observed }, signal);
|
|
521
|
+
return count;
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if ((0, classify_1.classifyAwsError)(error) !== error_code_1.ErrorCode.CONDITION_CONFLICT)
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
throw (0, errors_1.conflictError)(`Cannot reconcile messageCount: session "${sessionId}" changed during every one of ` +
|
|
529
|
+
`${idempotent_write_1.OVERWRITE_CAS_MAX_ATTEMPTS} attempts; retry when it is quieter`);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Whether a row's `ttl` is an instant a session listing can both judge and
|
|
533
|
+
* render.
|
|
534
|
+
*
|
|
535
|
+
* Neither of the two things done with it refuses a value it cannot use.
|
|
536
|
+
* {@link isExpiredRow} compares it against the clock, and a non-number compares
|
|
537
|
+
* `false` against every clock, so an unreadable ttl reads as *live* rather than
|
|
538
|
+
* being filtered out. `expiresAt` then renders it, and `NaN`, `Infinity` and
|
|
539
|
+
* anything past the ±8.64e12 seconds a `Date` spans are all numbers whose
|
|
540
|
+
* `toISOString` throws `RangeError` — which failed the whole listing.
|
|
541
|
+
*/
|
|
542
|
+
function hasReadableTtl(ttl) {
|
|
543
|
+
if (ttl === undefined)
|
|
544
|
+
return true;
|
|
545
|
+
return typeof ttl === 'number' && Number.isFinite(new Date(ttl * 1000).getTime());
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Whether every attribute {@link summariseSession} hands back is the type this package
|
|
549
|
+
* writes there.
|
|
550
|
+
*
|
|
551
|
+
* The identity test {@link summariseSession} makes first proves a row is a
|
|
552
|
+
* session row; this proves its own attributes are usable. They are returned
|
|
553
|
+
* under declared types, so a row that disagrees answers the caller with a lie
|
|
554
|
+
* — `messageCount: 'many'` handed back as a number — or, for the ttl, with a
|
|
555
|
+
* `RangeError`. A row this release cannot speak for is dropped the way a
|
|
556
|
+
* foreign row is, never at the cost of the rest of the page.
|
|
557
|
+
*/
|
|
558
|
+
function isSummarisable(raw) {
|
|
559
|
+
return (typeof raw.messageCount === 'number' &&
|
|
560
|
+
typeof raw.createdAt === 'string' &&
|
|
561
|
+
typeof raw.updatedAt === 'string' &&
|
|
562
|
+
(raw.title === undefined || typeof raw.title === 'string') &&
|
|
563
|
+
hasReadableTtl(raw.ttl));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* The public summary of a SESSION row: the session a row describes, or
|
|
567
|
+
* undefined for a foreign, malformed or expired row.
|
|
568
|
+
*
|
|
569
|
+
* The `sessionId` is bound to the partition the row was found in, as
|
|
570
|
+
* `parseMetaRow`, `parseStoreRow` and `parseMessageRow` bind theirs.
|
|
571
|
+
* Both reads that reach here select rows by something other than the partition
|
|
572
|
+
* — a table scan filtered on the sort key, and a recency-index query — so
|
|
573
|
+
* without the binding a row planted anywhere in the table under this adapter's
|
|
574
|
+
* SESSION sort key was summarised under whatever `sessionId` it claimed, and a
|
|
575
|
+
* caller taking that id to `getMessages` read a partition the row never lived
|
|
576
|
+
* in.
|
|
577
|
+
*
|
|
578
|
+
* Accepts: `raw` — a row a listing read. `atSeconds` — the listing's clock,
|
|
579
|
+
* against which an expired row is absent.
|
|
580
|
+
*
|
|
581
|
+
* Returns: the summary, or `undefined` for a foreign, malformed or expired row.
|
|
582
|
+
*
|
|
583
|
+
* Throws: `FORMAT_UNSUPPORTED` for a row a newer release wrote. It is not a
|
|
584
|
+
* foreign row to skip, and summarising it under this release's rules could
|
|
585
|
+
* return its attributes with a meaning they no longer have. Checked before the
|
|
586
|
+
* shape, the binding and the ttl — as every other read of this package's rows
|
|
587
|
+
* checks it — so a newer row is refused rather than judged against attribute
|
|
588
|
+
* names it may no longer use, and the answer does not depend on the reading
|
|
589
|
+
* machine's clock.
|
|
590
|
+
*/
|
|
591
|
+
function summariseSession(raw, atSeconds) {
|
|
592
|
+
const item = raw;
|
|
593
|
+
(0, table_schema_1.assertReadableRow)(item, 'session');
|
|
594
|
+
if (item.SK !== rows_1.SESSION_SORT_KEY || typeof item.sessionId !== 'string')
|
|
595
|
+
return undefined;
|
|
596
|
+
if (item.PK !== (0, rows_1.sessionPartition)(item.sessionId))
|
|
597
|
+
return undefined;
|
|
598
|
+
if (!isSummarisable(raw) || (0, table_schema_1.isExpiredRow)(item, atSeconds))
|
|
599
|
+
return undefined;
|
|
600
|
+
return {
|
|
601
|
+
sessionId: item.sessionId,
|
|
602
|
+
title: item.title,
|
|
603
|
+
messageCount: item.messageCount,
|
|
604
|
+
createdAt: item.createdAt,
|
|
605
|
+
updatedAt: item.updatedAt,
|
|
606
|
+
expiresAt: item.ttl === undefined ? undefined : new Date(item.ttl * 1000).toISOString(),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Where a history row sits in the recency index, for a row written before the
|
|
611
|
+
* index existed.
|
|
612
|
+
*
|
|
613
|
+
* Accepts: `row` — any row of the table.
|
|
614
|
+
*
|
|
615
|
+
* Returns: a SESSION row's identity — its session id, at its own `updatedAt` —
|
|
616
|
+
* or `undefined` for a message row or a row of another adapter.
|
|
617
|
+
*
|
|
618
|
+
* Throws: nothing.
|
|
619
|
+
*/
|
|
620
|
+
function sessionIndexTarget(row) {
|
|
621
|
+
const pk = typeof row.PK === 'string' ? row.PK : '';
|
|
622
|
+
const sk = typeof row.SK === 'string' ? row.SK : '';
|
|
623
|
+
if (!pk.startsWith((0, rows_1.historyPartitionPrefix)()))
|
|
624
|
+
return undefined;
|
|
625
|
+
return sk.endsWith('SESSION') && typeof row.sessionId === 'string'
|
|
626
|
+
? { tag: 'SESS', id: row.sessionId, at: (0, recency_index_1.backfilledAt)(row.updatedAt) }
|
|
627
|
+
: undefined;
|
|
628
|
+
}
|