@mastra/dynamodb 1.0.1 → 1.0.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/dist/index.js CHANGED
@@ -2142,6 +2142,8 @@ function formatWorkflowRun(snapshotData) {
2142
2142
  resourceId: snapshotData.resourceId
2143
2143
  };
2144
2144
  }
2145
+ var MAX_RETRIES = 5;
2146
+ var BASE_DELAY_MS = 50;
2145
2147
  var WorkflowStorageDynamoDB = class extends WorkflowsStorage {
2146
2148
  service;
2147
2149
  ttlConfig;
@@ -2151,9 +2153,41 @@ var WorkflowStorageDynamoDB = class extends WorkflowsStorage {
2151
2153
  this.service = resolved.service;
2152
2154
  this.ttlConfig = resolved.ttl;
2153
2155
  }
2156
+ supportsConcurrentUpdates() {
2157
+ return true;
2158
+ }
2154
2159
  async dangerouslyClearAll() {
2155
2160
  await deleteTableData(this.service, TABLE_WORKFLOW_SNAPSHOT);
2156
2161
  }
2162
+ /**
2163
+ * Helper to check if an error is a DynamoDB conditional check failure
2164
+ */
2165
+ isConditionalCheckFailed(error) {
2166
+ if (error && typeof error === "object") {
2167
+ const err = error;
2168
+ if (err.name === "ConditionalCheckFailedException" || err.code === "ConditionalCheckFailedException" || (err.__type?.includes("ConditionalCheckFailedException") ?? false)) {
2169
+ return true;
2170
+ }
2171
+ if (err.message?.includes("conditional request failed")) {
2172
+ return true;
2173
+ }
2174
+ if (err.cause && typeof err.cause === "object") {
2175
+ const cause = err.cause;
2176
+ if (cause.name === "ConditionalCheckFailedException" || cause.code === "ConditionalCheckFailedException") {
2177
+ return true;
2178
+ }
2179
+ }
2180
+ }
2181
+ return false;
2182
+ }
2183
+ /**
2184
+ * Helper to delay with exponential backoff and jitter
2185
+ */
2186
+ async delay(attempt) {
2187
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
2188
+ const jitter = Math.random() * backoff * 0.5;
2189
+ await new Promise((resolve) => setTimeout(resolve, backoff + jitter));
2190
+ }
2157
2191
  async updateWorkflowResults({
2158
2192
  workflowName,
2159
2193
  runId,
@@ -2161,69 +2195,147 @@ var WorkflowStorageDynamoDB = class extends WorkflowsStorage {
2161
2195
  result,
2162
2196
  requestContext
2163
2197
  }) {
2164
- try {
2165
- const existingSnapshot = await this.loadWorkflowSnapshot({ workflowName, runId });
2166
- let snapshot;
2167
- if (!existingSnapshot) {
2168
- snapshot = {
2169
- context: {},
2170
- activePaths: [],
2171
- timestamp: Date.now(),
2172
- suspendedPaths: {},
2173
- activeStepsPath: {},
2174
- resumeLabels: {},
2175
- serializedStepGraph: [],
2176
- status: "pending",
2177
- value: {},
2178
- waitingPaths: {},
2179
- runId,
2180
- requestContext: {}
2198
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
2199
+ try {
2200
+ const existingRecord = await this.service.entities.workflow_snapshot.get({
2201
+ entity: "workflow_snapshot",
2202
+ workflow_name: workflowName,
2203
+ run_id: runId
2204
+ }).go();
2205
+ const now = /* @__PURE__ */ new Date();
2206
+ let snapshot;
2207
+ let previousUpdatedAt;
2208
+ if (!existingRecord.data) {
2209
+ snapshot = {
2210
+ context: {},
2211
+ activePaths: [],
2212
+ timestamp: Date.now(),
2213
+ suspendedPaths: {},
2214
+ activeStepsPath: {},
2215
+ resumeLabels: {},
2216
+ serializedStepGraph: [],
2217
+ status: "pending",
2218
+ value: {},
2219
+ waitingPaths: {},
2220
+ runId,
2221
+ requestContext: {}
2222
+ };
2223
+ } else {
2224
+ snapshot = existingRecord.data.snapshot;
2225
+ previousUpdatedAt = existingRecord.data.updatedAt;
2226
+ }
2227
+ snapshot.context[stepId] = result;
2228
+ snapshot.requestContext = { ...snapshot.requestContext, ...requestContext };
2229
+ const data = {
2230
+ entity: "workflow_snapshot",
2231
+ workflow_name: workflowName,
2232
+ run_id: runId,
2233
+ snapshot: JSON.stringify(snapshot),
2234
+ createdAt: existingRecord.data?.createdAt ?? now.toISOString(),
2235
+ updatedAt: now.toISOString(),
2236
+ ...getTtlProps("workflow_snapshot", this.ttlConfig)
2181
2237
  };
2182
- } else {
2183
- snapshot = existingSnapshot;
2238
+ if (previousUpdatedAt) {
2239
+ await this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)).go();
2240
+ } else {
2241
+ await this.service.entities.workflow_snapshot.create(data).where((attr, op) => op.notExists(attr.run_id)).go();
2242
+ }
2243
+ return snapshot.context;
2244
+ } catch (error) {
2245
+ if (this.isConditionalCheckFailed(error)) {
2246
+ if (attempt < MAX_RETRIES - 1) {
2247
+ this.logger.debug(
2248
+ `Optimistic locking conflict in updateWorkflowResults, retrying (attempt ${attempt + 1})`
2249
+ );
2250
+ await this.delay(attempt);
2251
+ continue;
2252
+ }
2253
+ }
2254
+ if (error instanceof MastraError) throw error;
2255
+ throw new MastraError(
2256
+ {
2257
+ id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
2258
+ domain: ErrorDomain.STORAGE,
2259
+ category: ErrorCategory.THIRD_PARTY,
2260
+ details: { workflowName, runId, stepId }
2261
+ },
2262
+ error
2263
+ );
2184
2264
  }
2185
- snapshot.context[stepId] = result;
2186
- snapshot.requestContext = { ...snapshot.requestContext, ...requestContext };
2187
- await this.persistWorkflowSnapshot({ workflowName, runId, snapshot });
2188
- return snapshot.context;
2189
- } catch (error) {
2190
- if (error instanceof MastraError) throw error;
2191
- throw new MastraError(
2192
- {
2193
- id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
2194
- domain: ErrorDomain.STORAGE,
2195
- category: ErrorCategory.THIRD_PARTY,
2196
- details: { workflowName, runId, stepId }
2197
- },
2198
- error
2199
- );
2200
2265
  }
2266
+ throw new MastraError(
2267
+ {
2268
+ id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_RESULTS", "MAX_RETRIES_EXCEEDED"),
2269
+ domain: ErrorDomain.STORAGE,
2270
+ category: ErrorCategory.THIRD_PARTY,
2271
+ details: { workflowName, runId, stepId }
2272
+ },
2273
+ new Error("Max retries exceeded for optimistic locking")
2274
+ );
2201
2275
  }
2202
2276
  async updateWorkflowState({
2203
2277
  workflowName,
2204
2278
  runId,
2205
2279
  opts
2206
2280
  }) {
2207
- try {
2208
- const existingSnapshot = await this.loadWorkflowSnapshot({ workflowName, runId });
2209
- if (!existingSnapshot || !existingSnapshot.context) {
2210
- return void 0;
2281
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
2282
+ try {
2283
+ const existingRecord = await this.service.entities.workflow_snapshot.get({
2284
+ entity: "workflow_snapshot",
2285
+ workflow_name: workflowName,
2286
+ run_id: runId
2287
+ }).go();
2288
+ if (!existingRecord.data) {
2289
+ return void 0;
2290
+ }
2291
+ const existingSnapshot = existingRecord.data.snapshot;
2292
+ if (!existingSnapshot || !existingSnapshot.context) {
2293
+ return void 0;
2294
+ }
2295
+ const previousUpdatedAt = existingRecord.data.updatedAt;
2296
+ const updatedSnapshot = { ...existingSnapshot, ...opts };
2297
+ const now = /* @__PURE__ */ new Date();
2298
+ const data = {
2299
+ entity: "workflow_snapshot",
2300
+ workflow_name: workflowName,
2301
+ run_id: runId,
2302
+ snapshot: JSON.stringify(updatedSnapshot),
2303
+ createdAt: existingRecord.data.createdAt,
2304
+ updatedAt: now.toISOString(),
2305
+ resourceId: existingRecord.data.resourceId,
2306
+ ...getTtlProps("workflow_snapshot", this.ttlConfig)
2307
+ };
2308
+ await this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)).go();
2309
+ return updatedSnapshot;
2310
+ } catch (error) {
2311
+ if (this.isConditionalCheckFailed(error)) {
2312
+ if (attempt < MAX_RETRIES - 1) {
2313
+ this.logger.debug(`Optimistic locking conflict in updateWorkflowState, retrying (attempt ${attempt + 1})`);
2314
+ await this.delay(attempt);
2315
+ continue;
2316
+ }
2317
+ }
2318
+ if (error instanceof MastraError) throw error;
2319
+ throw new MastraError(
2320
+ {
2321
+ id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_STATE", "FAILED"),
2322
+ domain: ErrorDomain.STORAGE,
2323
+ category: ErrorCategory.THIRD_PARTY,
2324
+ details: { workflowName, runId }
2325
+ },
2326
+ error
2327
+ );
2211
2328
  }
2212
- const updatedSnapshot = { ...existingSnapshot, ...opts };
2213
- await this.persistWorkflowSnapshot({ workflowName, runId, snapshot: updatedSnapshot });
2214
- return updatedSnapshot;
2215
- } catch (error) {
2216
- if (error instanceof MastraError) throw error;
2217
- throw new MastraError(
2218
- {
2219
- id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_STATE", "FAILED"),
2220
- domain: ErrorDomain.STORAGE,
2221
- category: ErrorCategory.THIRD_PARTY,
2222
- details: { workflowName, runId }
2223
- },
2224
- error
2225
- );
2226
2329
  }
2330
+ throw new MastraError(
2331
+ {
2332
+ id: createStorageErrorId("DYNAMODB", "UPDATE_WORKFLOW_STATE", "MAX_RETRIES_EXCEEDED"),
2333
+ domain: ErrorDomain.STORAGE,
2334
+ category: ErrorCategory.THIRD_PARTY,
2335
+ details: { workflowName, runId }
2336
+ },
2337
+ new Error("Max retries exceeded for optimistic locking")
2338
+ );
2227
2339
  }
2228
2340
  // Workflow operations
2229
2341
  async persistWorkflowSnapshot({