trekoon 0.3.6 → 0.3.7

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.
@@ -6,6 +6,15 @@ import { TrackerDomain } from "../domain/tracker-domain";
6
6
  import { DomainError } from "../domain/types";
7
7
  import { buildBoardSnapshot } from "./snapshot";
8
8
 
9
+ interface SnapshotDeltaSelection {
10
+ readonly epicIds?: readonly string[];
11
+ readonly taskIds?: readonly string[];
12
+ readonly subtaskIds?: readonly string[];
13
+ readonly dependencyIds?: readonly string[];
14
+ readonly deletedSubtaskIds?: readonly string[];
15
+ readonly deletedDependencyIds?: readonly string[];
16
+ }
17
+
9
18
  interface BoardRouteContext {
10
19
  readonly db: Database;
11
20
  readonly cwd: string;
@@ -19,6 +28,31 @@ interface BoardRouteError {
19
28
  readonly details?: Record<string, unknown>;
20
29
  }
21
30
 
31
+ function buildCreateSubtaskFingerprint(body: Record<string, unknown>): string {
32
+ return JSON.stringify({
33
+ taskId: readRequiredString(body, "taskId"),
34
+ title: readRequiredString(body, "title"),
35
+ description: readOptionalString(body, "description") ?? null,
36
+ status: readOptionalString(body, "status") ?? null,
37
+ });
38
+ }
39
+
40
+ function buildCreateDependencyFingerprint(sourceId: string, dependsOnId: string): string {
41
+ return JSON.stringify({ sourceId, dependsOnId });
42
+ }
43
+
44
+ function buildDeleteSubtaskFingerprint(subtaskId: string): string {
45
+ return JSON.stringify({ subtaskId });
46
+ }
47
+
48
+ function buildDeleteDependencyFingerprint(sourceId: string, dependsOnId: string): string {
49
+ return JSON.stringify({ sourceId, dependsOnId });
50
+ }
51
+
52
+ function compactIds(ids: readonly string[]): string[] {
53
+ return ids.filter((id) => id.length > 0);
54
+ }
55
+
22
56
  function jsonResponse(status: number, data: unknown): Response {
23
57
  return new Response(JSON.stringify(data), {
24
58
  status,
@@ -106,16 +140,84 @@ function describeBoardError(mutations: MutationService, error: unknown, requestL
106
140
  };
107
141
  }
108
142
 
109
- function buildMutationResponse(domain: TrackerDomain, data: Record<string, unknown>, status = 200): Response {
143
+ function buildMutationResponse(_domain: TrackerDomain, data: Record<string, unknown>, status = 200): Response {
110
144
  return jsonResponse(status, {
111
145
  ok: true,
112
- data: {
113
- ...data,
114
- snapshot: buildBoardSnapshot(domain),
115
- },
146
+ data,
116
147
  });
117
148
  }
118
149
 
150
+ function buildSnapshotDelta(domain: TrackerDomain, selection: SnapshotDeltaSelection): Record<string, unknown> {
151
+ const snapshot = buildBoardSnapshot(domain);
152
+ const epicIdSet = new Set(selection.epicIds ?? []);
153
+ const taskIdSet = new Set(selection.taskIds ?? []);
154
+ const subtaskIdSet = new Set(selection.subtaskIds ?? []);
155
+ const dependencyIdSet = new Set(selection.dependencyIds ?? []);
156
+
157
+ return {
158
+ generatedAt: snapshot.generatedAt,
159
+ epics: snapshot.epics.filter((epic) => epicIdSet.has(epic.id)),
160
+ tasks: snapshot.tasks.filter((task) => taskIdSet.has(task.id)),
161
+ subtasks: snapshot.subtasks.filter((subtask) => subtaskIdSet.has(subtask.id)),
162
+ dependencies: snapshot.dependencies.filter((dependency) => dependencyIdSet.has(dependency.id)),
163
+ deletedSubtaskIds: [...(selection.deletedSubtaskIds ?? [])],
164
+ deletedDependencyIds: [...(selection.deletedDependencyIds ?? [])],
165
+ };
166
+ }
167
+
168
+ function buildMutationDeltaResponse(
169
+ domain: TrackerDomain,
170
+ data: Record<string, unknown>,
171
+ selection: SnapshotDeltaSelection,
172
+ status = 200,
173
+ ): Response {
174
+ return buildMutationResponse(domain, {
175
+ ...data,
176
+ snapshotDelta: buildSnapshotDelta(domain, selection),
177
+ }, status);
178
+ }
179
+
180
+ function readRecordId(value: unknown): string | null {
181
+ if (!value || typeof value !== "object") {
182
+ return null;
183
+ }
184
+
185
+ const id = (value as { id?: unknown }).id;
186
+ return typeof id === "string" && id.length > 0 ? id : null;
187
+ }
188
+
189
+ function withFreshReplaySnapshotDelta(
190
+ domain: TrackerDomain,
191
+ responseData: Record<string, unknown>,
192
+ selection: SnapshotDeltaSelection,
193
+ ): Record<string, unknown> {
194
+ return {
195
+ ...responseData,
196
+ snapshotDelta: buildSnapshotDelta(domain, selection),
197
+ };
198
+ }
199
+
200
+ function readSnapshotDelta(responseData: Record<string, unknown>): Record<string, unknown> | null {
201
+ const snapshotDelta = responseData.snapshotDelta;
202
+ return snapshotDelta && typeof snapshotDelta === "object" ? snapshotDelta as Record<string, unknown> : null;
203
+ }
204
+
205
+ function readRecordIds(value: unknown): string[] {
206
+ if (!Array.isArray(value)) {
207
+ return [];
208
+ }
209
+
210
+ return value.map((item) => readRecordId(item)).filter((id): id is string => id !== null);
211
+ }
212
+
213
+ function readStringArray(value: unknown): string[] {
214
+ if (!Array.isArray(value)) {
215
+ return [];
216
+ }
217
+
218
+ return value.filter((item): item is string => typeof item === "string" && item.length > 0);
219
+ }
220
+
119
221
  async function parseJsonBody(request: Request): Promise<Record<string, unknown>> {
120
222
  const contentType: string = request.headers.get("content-type") ?? "";
121
223
  if (!contentType.includes("application/json")) {
@@ -175,6 +277,41 @@ function readRequiredString(body: Record<string, unknown>, field: string): strin
175
277
  return value;
176
278
  }
177
279
 
280
+ function readOptionalNullableString(body: Record<string, unknown>, field: string): string | null | undefined {
281
+ const value = body[field];
282
+ if (value === undefined) {
283
+ return undefined;
284
+ }
285
+
286
+ if (value === null) {
287
+ return null;
288
+ }
289
+
290
+ if (typeof value !== "string") {
291
+ throw new DomainError({
292
+ code: "invalid_input",
293
+ message: `${field} must be a string or null`,
294
+ details: { field },
295
+ });
296
+ }
297
+
298
+ return value;
299
+ }
300
+
301
+ function readIdempotencyKey(request: Request, body: Record<string, unknown>): string | null {
302
+ const headerKey = request.headers.get("x-trekoon-idempotency-key");
303
+ if (typeof headerKey === "string" && headerKey.trim().length > 0) {
304
+ return headerKey.trim();
305
+ }
306
+
307
+ const bodyKey = body.clientRequestId;
308
+ if (typeof bodyKey === "string" && bodyKey.trim().length > 0) {
309
+ return bodyKey.trim();
310
+ }
311
+
312
+ return null;
313
+ }
314
+
178
315
  export function createBoardApiHandler(context: BoardRouteContext): (request: Request) => Promise<Response> {
179
316
  return async (request: Request): Promise<Response> => {
180
317
  const url = new URL(request.url);
@@ -203,89 +340,292 @@ export function createBoardApiHandler(context: BoardRouteContext): (request: Req
203
340
  });
204
341
  }
205
342
 
206
- const epicCascadeMatch = request.method === "PATCH" ? url.pathname.match(/^\/api\/epics\/([^/]+)\/cascade$/u) : null;
207
- if (epicCascadeMatch) {
208
- const body = await parseJsonBody(request);
209
- const status = readRequiredString(body, "status");
210
- const plan = mutations.updateEpicStatusCascade(epicCascadeMatch[1] ?? "", status);
211
- return buildMutationResponse(domain, { plan });
212
- }
343
+ const epicCascadeMatch = request.method === "PATCH" ? url.pathname.match(/^\/api\/epics\/([^/]+)\/cascade$/u) : null;
344
+ if (epicCascadeMatch) {
345
+ const body = await parseJsonBody(request);
346
+ const status = readRequiredString(body, "status");
347
+ const plan = mutations.updateEpicStatusCascade(epicCascadeMatch[1] ?? "", status);
348
+ return buildMutationDeltaResponse(domain, {
349
+ plan,
350
+ }, {
351
+ epicIds: [epicCascadeMatch[1] ?? ""],
352
+ taskIds: plan.orderedChanges.filter((change) => change.kind === "task").map((change) => change.id),
353
+ subtaskIds: plan.orderedChanges.filter((change) => change.kind === "subtask").map((change) => change.id),
354
+ });
355
+ }
213
356
 
214
357
  const epicMatch = request.method === "PATCH" ? url.pathname.match(/^\/api\/epics\/([^/]+)$/u) : null;
215
- if (epicMatch) {
358
+ if (epicMatch) {
216
359
  const body = await parseJsonBody(request);
217
360
  const epic = mutations.updateEpic(epicMatch[1] ?? "", {
218
361
  title: readOptionalString(body, "title"),
219
362
  description: readOptionalString(body, "description"),
220
363
  status: readOptionalString(body, "status"),
221
364
  });
222
- return buildMutationResponse(domain, { epic });
223
- }
365
+ return buildMutationDeltaResponse(domain, { epic }, { epicIds: [epic.id] });
366
+ }
224
367
 
225
368
  const taskMatch = request.method === "PATCH" ? url.pathname.match(/^\/api\/tasks\/([^/]+)$/u) : null;
226
369
  if (taskMatch) {
227
370
  const body = await parseJsonBody(request);
228
- const task = mutations.updateTask(taskMatch[1] ?? "", {
229
- title: readOptionalString(body, "title"),
230
- description: readOptionalString(body, "description"),
231
- status: readOptionalString(body, "status"),
232
- owner: readOptionalString(body, "owner"),
233
- });
234
- return buildMutationResponse(domain, { task });
235
- }
371
+ const task = mutations.updateTask(taskMatch[1] ?? "", {
372
+ title: readOptionalString(body, "title"),
373
+ description: readOptionalString(body, "description"),
374
+ status: readOptionalString(body, "status"),
375
+ owner: readOptionalNullableString(body, "owner"),
376
+ });
377
+ return buildMutationDeltaResponse(domain, { task }, { epicIds: [task.epicId], taskIds: [task.id] });
378
+ }
236
379
 
237
380
  const subtaskMatch = request.method === "PATCH" ? url.pathname.match(/^\/api\/subtasks\/([^/]+)$/u) : null;
238
381
  if (subtaskMatch) {
239
382
  const body = await parseJsonBody(request);
240
- const subtask = mutations.updateSubtask(subtaskMatch[1] ?? "", {
241
- title: readOptionalString(body, "title"),
242
- description: readOptionalString(body, "description"),
243
- status: readOptionalString(body, "status"),
244
- owner: readOptionalString(body, "owner"),
245
- });
246
- return buildMutationResponse(domain, { subtask });
247
- }
383
+ const subtask = mutations.updateSubtask(subtaskMatch[1] ?? "", {
384
+ title: readOptionalString(body, "title"),
385
+ description: readOptionalString(body, "description"),
386
+ status: readOptionalString(body, "status"),
387
+ owner: readOptionalNullableString(body, "owner"),
388
+ });
389
+ const task = domain.getTaskOrThrow(subtask.taskId);
390
+ return buildMutationDeltaResponse(domain, { subtask }, { epicIds: [task.epicId], taskIds: [task.id], subtaskIds: [subtask.id] });
391
+ }
248
392
 
249
393
  if (request.method === "POST" && url.pathname === "/api/subtasks") {
250
394
  const body = await parseJsonBody(request);
251
- const subtask = mutations.createSubtask({
395
+ const idempotencyKey = readIdempotencyKey(request, body);
396
+ const requestFingerprint = buildCreateSubtaskFingerprint(body);
397
+ if (!idempotencyKey) {
398
+ const subtask = mutations.createSubtask({
399
+ taskId: readRequiredString(body, "taskId"),
400
+ title: readRequiredString(body, "title"),
401
+ description: readOptionalString(body, "description"),
402
+ status: readOptionalString(body, "status"),
403
+ });
404
+ const task = domain.getTaskOrThrow(subtask.taskId);
405
+ const responseData = {
406
+ subtask,
407
+ snapshotDelta: buildSnapshotDelta(domain, {
408
+ epicIds: [task.epicId],
409
+ taskIds: [task.id],
410
+ subtaskIds: [subtask.id],
411
+ }),
412
+ };
413
+ return buildMutationResponse(domain, responseData, 201);
414
+ }
415
+
416
+ const result = mutations.createSubtaskAtomicallyWithIdempotency({
252
417
  taskId: readRequiredString(body, "taskId"),
253
418
  title: readRequiredString(body, "title"),
254
419
  description: readOptionalString(body, "description"),
255
420
  status: readOptionalString(body, "status"),
421
+ claim: {
422
+ scope: "subtask",
423
+ idempotencyKey,
424
+ requestFingerprint,
425
+ conflictMessage: "Idempotency key cannot be reused for a different subtask request",
426
+ },
427
+ buildResponseData: ({ subtask, domain: transactionDomain }) => {
428
+ const task = transactionDomain.getTaskOrThrow(subtask.taskId);
429
+ return {
430
+ subtask,
431
+ snapshotDelta: buildSnapshotDelta(transactionDomain, {
432
+ epicIds: [task.epicId],
433
+ taskIds: [task.id],
434
+ subtaskIds: [subtask.id],
435
+ }),
436
+ };
437
+ },
256
438
  });
257
- return buildMutationResponse(domain, { subtask }, 201);
439
+ const replaySubtaskId = readRecordId(result.responseData.subtask);
440
+ const replayTaskId = replaySubtaskId ? domain.getSubtask(replaySubtaskId)?.taskId ?? null : null;
441
+ const replayEpicId = replayTaskId ? domain.getTask(replayTaskId)?.epicId ?? null : null;
442
+ return buildMutationResponse(domain, result.state === "replay"
443
+ ? withFreshReplaySnapshotDelta(domain, result.responseData, {
444
+ epicIds: replayEpicId ? [replayEpicId] : [],
445
+ taskIds: replayTaskId ? [replayTaskId] : [],
446
+ subtaskIds: replaySubtaskId ? [replaySubtaskId] : [],
447
+ })
448
+ : result.responseData, result.status);
258
449
  }
259
450
 
260
- const deleteSubtaskMatch = request.method === "DELETE" ? url.pathname.match(/^\/api\/subtasks\/([^/]+)$/u) : null;
261
- if (deleteSubtaskMatch) {
262
- const subtaskId = deleteSubtaskMatch[1] ?? "";
263
- mutations.deleteSubtask(subtaskId);
264
- return buildMutationResponse(domain, { subtaskId, deleted: true });
265
- }
451
+ const deleteSubtaskMatch = request.method === "DELETE" ? url.pathname.match(/^\/api\/subtasks\/([^/]+)$/u) : null;
452
+ if (deleteSubtaskMatch) {
453
+ const subtaskId = deleteSubtaskMatch[1] ?? "";
454
+ const idempotencyKey = request.headers.get("x-trekoon-idempotency-key")?.trim() || null;
455
+ const requestFingerprint = buildDeleteSubtaskFingerprint(subtaskId);
456
+ if (!idempotencyKey) {
457
+ const existingSubtask = domain.getSubtaskOrThrow(subtaskId);
458
+ const task = domain.getTaskOrThrow(existingSubtask.taskId);
459
+ const { deletedDependencyIds } = mutations.deleteSubtask(subtaskId);
460
+ const responseData = {
461
+ subtaskId,
462
+ deleted: true,
463
+ snapshotDelta: buildSnapshotDelta(domain, {
464
+ epicIds: [task.epicId],
465
+ taskIds: [task.id],
466
+ deletedSubtaskIds: [subtaskId],
467
+ deletedDependencyIds,
468
+ }),
469
+ };
470
+ return buildMutationResponse(domain, responseData, 200);
471
+ }
472
+
473
+ const result = mutations.deleteSubtaskAtomicallyWithIdempotency({
474
+ id: subtaskId,
475
+ claim: {
476
+ scope: "deleted_subtask",
477
+ idempotencyKey,
478
+ requestFingerprint,
479
+ conflictMessage: "Idempotency key cannot be reused for a different subtask delete request",
480
+ },
481
+ buildResponseData: ({ subtaskId: deletedSubtaskId, deletedDependencyIds, domain: transactionDomain, taskId, epicId }) => ({
482
+ subtaskId: deletedSubtaskId,
483
+ deleted: true,
484
+ snapshotDelta: buildSnapshotDelta(transactionDomain, {
485
+ epicIds: [epicId],
486
+ taskIds: [taskId],
487
+ deletedSubtaskIds: [deletedSubtaskId],
488
+ deletedDependencyIds,
489
+ }),
490
+ }),
491
+ });
492
+ const replaySnapshotDelta = readSnapshotDelta(result.responseData);
493
+ return buildMutationResponse(domain, result.state === "replay"
494
+ ? withFreshReplaySnapshotDelta(domain, result.responseData, {
495
+ epicIds: readRecordIds(replaySnapshotDelta?.epics),
496
+ taskIds: readRecordIds(replaySnapshotDelta?.tasks),
497
+ deletedSubtaskIds: readStringArray(replaySnapshotDelta?.deletedSubtaskIds),
498
+ deletedDependencyIds: readStringArray(replaySnapshotDelta?.deletedDependencyIds),
499
+ })
500
+ : result.responseData, result.status);
501
+ }
266
502
 
267
503
  if (request.method === "POST" && url.pathname === "/api/dependencies") {
268
504
  const body = await parseJsonBody(request);
269
- const dependency = mutations.addDependency(readRequiredString(body, "sourceId"), readRequiredString(body, "dependsOnId"));
270
- return buildMutationResponse(domain, { dependency }, 201);
505
+ const sourceId = readRequiredString(body, "sourceId");
506
+ const dependsOnId = readRequiredString(body, "dependsOnId");
507
+ const idempotencyKey = readIdempotencyKey(request, body);
508
+ const requestFingerprint = buildCreateDependencyFingerprint(sourceId, dependsOnId);
509
+ if (!idempotencyKey) {
510
+ const dependency = mutations.addDependency(sourceId, dependsOnId);
511
+ const responseData = {
512
+ dependency,
513
+ snapshotDelta: buildSnapshotDelta(domain, {
514
+ taskIds: compactIds([dependency.sourceKind === "task" ? dependency.sourceId : "", dependency.dependsOnKind === "task" ? dependency.dependsOnId : ""]),
515
+ subtaskIds: compactIds([dependency.sourceKind === "subtask" ? dependency.sourceId : "", dependency.dependsOnKind === "subtask" ? dependency.dependsOnId : ""]),
516
+ dependencyIds: [dependency.id],
517
+ }),
518
+ };
519
+ return buildMutationResponse(domain, responseData, 201);
520
+ }
521
+
522
+ const result = mutations.addDependencyAtomicallyWithIdempotency({
523
+ sourceId,
524
+ dependsOnId,
525
+ claim: {
526
+ scope: "dependency",
527
+ idempotencyKey,
528
+ requestFingerprint,
529
+ conflictMessage: "Idempotency key cannot be reused for a different dependency request",
530
+ },
531
+ buildResponseData: ({ dependency, domain: transactionDomain }) => ({
532
+ dependency,
533
+ snapshotDelta: buildSnapshotDelta(transactionDomain, {
534
+ taskIds: compactIds([dependency.sourceKind === "task" ? dependency.sourceId : "", dependency.dependsOnKind === "task" ? dependency.dependsOnId : ""]),
535
+ subtaskIds: compactIds([dependency.sourceKind === "subtask" ? dependency.sourceId : "", dependency.dependsOnKind === "subtask" ? dependency.dependsOnId : ""]),
536
+ dependencyIds: [dependency.id],
537
+ }),
538
+ }),
539
+ });
540
+ const replayDependency = result.responseData.dependency;
541
+ const replayDependencyId = readRecordId(replayDependency);
542
+ const replaySelection = replayDependency && typeof replayDependency === "object"
543
+ ? {
544
+ taskIds: compactIds([
545
+ (replayDependency as { sourceKind?: unknown; sourceId?: unknown }).sourceKind === "task"
546
+ ? ((replayDependency as { sourceId?: unknown }).sourceId as string ?? "")
547
+ : "",
548
+ (replayDependency as { dependsOnKind?: unknown; dependsOnId?: unknown }).dependsOnKind === "task"
549
+ ? ((replayDependency as { dependsOnId?: unknown }).dependsOnId as string ?? "")
550
+ : "",
551
+ ]),
552
+ subtaskIds: compactIds([
553
+ (replayDependency as { sourceKind?: unknown; sourceId?: unknown }).sourceKind === "subtask"
554
+ ? ((replayDependency as { sourceId?: unknown }).sourceId as string ?? "")
555
+ : "",
556
+ (replayDependency as { dependsOnKind?: unknown; dependsOnId?: unknown }).dependsOnKind === "subtask"
557
+ ? ((replayDependency as { dependsOnId?: unknown }).dependsOnId as string ?? "")
558
+ : "",
559
+ ]),
560
+ dependencyIds: replayDependencyId ? [replayDependencyId] : [],
561
+ }
562
+ : { dependencyIds: [] };
563
+ return buildMutationResponse(domain, result.state === "replay"
564
+ ? withFreshReplaySnapshotDelta(domain, result.responseData, replaySelection)
565
+ : result.responseData, result.status);
271
566
  }
272
567
 
273
568
  if (request.method === "DELETE" && url.pathname === "/api/dependencies") {
274
569
  const sourceId = url.searchParams.get("sourceId") ?? "";
275
570
  const dependsOnId = url.searchParams.get("dependsOnId") ?? "";
276
- const removed = mutations.removeDependency(sourceId, dependsOnId);
277
- if (removed === 0) {
278
- throw new DomainError({
279
- code: "not_found",
280
- message: "Dependency edge not found",
281
- details: {
282
- sourceId,
283
- dependsOnId,
284
- },
285
- });
571
+ const idempotencyKey = request.headers.get("x-trekoon-idempotency-key")?.trim() || null;
572
+ const requestFingerprint = buildDeleteDependencyFingerprint(sourceId, dependsOnId);
573
+ if (!idempotencyKey) {
574
+ const existingDependencyIds = domain.listDependencies(sourceId)
575
+ .filter((dependency) => dependency.dependsOnId === dependsOnId)
576
+ .map((dependency) => dependency.id);
577
+ const removed = mutations.removeDependency(sourceId, dependsOnId);
578
+ if (removed === 0) {
579
+ throw new DomainError({
580
+ code: "not_found",
581
+ message: "Dependency edge not found",
582
+ details: {
583
+ sourceId,
584
+ dependsOnId,
585
+ },
586
+ });
587
+ }
588
+ const responseData = {
589
+ sourceId,
590
+ dependsOnId,
591
+ removed,
592
+ snapshotDelta: buildSnapshotDelta(domain, {
593
+ taskIds: compactIds([domain.getTask(sourceId)?.id ?? "", domain.getTask(dependsOnId)?.id ?? ""]),
594
+ subtaskIds: compactIds([domain.getSubtask(sourceId)?.id ?? "", domain.getSubtask(dependsOnId)?.id ?? ""]),
595
+ deletedDependencyIds: existingDependencyIds,
596
+ }),
597
+ };
598
+ return buildMutationResponse(domain, responseData, 200);
286
599
  }
287
600
 
288
- return buildMutationResponse(domain, { sourceId, dependsOnId, removed });
601
+ const result = mutations.removeDependencyAtomicallyWithIdempotency({
602
+ sourceId,
603
+ dependsOnId,
604
+ claim: {
605
+ scope: "deleted_dependency",
606
+ idempotencyKey,
607
+ requestFingerprint,
608
+ conflictMessage: "Idempotency key cannot be reused for a different dependency delete request",
609
+ },
610
+ buildResponseData: ({ sourceId: deletedSourceId, dependsOnId: deletedDependsOnId, removed, existingDependencyIds, domain: transactionDomain }) => ({
611
+ sourceId: deletedSourceId,
612
+ dependsOnId: deletedDependsOnId,
613
+ removed,
614
+ snapshotDelta: buildSnapshotDelta(transactionDomain, {
615
+ taskIds: compactIds([transactionDomain.getTask(deletedSourceId)?.id ?? "", transactionDomain.getTask(deletedDependsOnId)?.id ?? ""]),
616
+ subtaskIds: compactIds([transactionDomain.getSubtask(deletedSourceId)?.id ?? "", transactionDomain.getSubtask(deletedDependsOnId)?.id ?? ""]),
617
+ deletedDependencyIds: existingDependencyIds,
618
+ }),
619
+ }),
620
+ });
621
+ const replaySnapshotDelta = readSnapshotDelta(result.responseData);
622
+ return buildMutationResponse(domain, result.state === "replay"
623
+ ? withFreshReplaySnapshotDelta(domain, result.responseData, {
624
+ taskIds: compactIds([domain.getTask(sourceId)?.id ?? "", domain.getTask(dependsOnId)?.id ?? ""]),
625
+ subtaskIds: compactIds([domain.getSubtask(sourceId)?.id ?? "", domain.getSubtask(dependsOnId)?.id ?? ""]),
626
+ deletedDependencyIds: readStringArray(replaySnapshotDelta?.deletedDependencyIds),
627
+ })
628
+ : result.responseData, result.status);
289
629
  }
290
630
 
291
631
  return jsonResponse(404, {