@critical-path/svelte 0.15.9 → 0.16.1

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.
@@ -18,7 +18,7 @@ export declare class KanbanState {
18
18
  createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
19
19
  moveTask(taskId: string, targetStatus: TaskStatus): Promise<Task>;
20
20
  updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
21
- deleteTask(taskId: string): Promise<boolean | undefined>;
21
+ deleteTask(taskId: string): Promise<void>;
22
22
  }
23
23
  export declare function createKanbanState(client: CriticalPathClient, projectId?: string, options?: KanbanStateOptions): KanbanState;
24
24
  //# sourceMappingURL=kanban-state.svelte.d.ts.map
@@ -1,17 +1,19 @@
1
1
  import type { CriticalPathClient } from '@critical-path/client';
2
- import type { Task, TaskStatus } from '@critical-path/core';
2
+ import { type Task, type TaskStatus } from '@critical-path/core';
3
3
  export declare class TaskState {
4
4
  private client;
5
5
  projectId?: string | undefined;
6
6
  data: Task[];
7
7
  loading: boolean;
8
8
  error: Error | null;
9
+ private pendingCreations;
10
+ private tempToRealIdMap;
9
11
  constructor(client: CriticalPathClient, projectId?: string | undefined);
10
12
  fetch(): Promise<void>;
11
13
  createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
12
14
  updateTask(taskId: string, updates: Partial<Task>): Promise<Task>;
13
15
  updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
14
- deleteTask(taskId: string): Promise<boolean | undefined>;
16
+ deleteTask(taskId: string): Promise<void>;
15
17
  }
16
18
  export declare function createTaskState(client: CriticalPathClient, projectId?: string): TaskState;
17
19
  //# sourceMappingURL=task-state.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-state.svelte.d.ts","sourceRoot":"","sources":["../src/task-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAE5D,qBAAa,SAAS;IAMlB,OAAO,CAAC,MAAM;IACP,SAAS,CAAC,EAAE,MAAM;IAN3B,IAAI,SAAsB;IAC1B,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAGzB,MAAM,EAAE,kBAAkB,EAC3B,SAAS,CAAC,EAAE,MAAM,YAAA;IAGrB,KAAK;IAYL,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IA2B9D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC;IAyCjD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;IAyCnD,UAAU,CAAC,MAAM,EAAE,MAAM;CAuBhC;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,SAAS,CAEX"}
1
+ {"version":3,"file":"task-state.svelte.d.ts","sourceRoot":"","sources":["../src/task-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAgB,MAAM,qBAAqB,CAAC;AAE/E,qBAAa,SAAS;IASlB,OAAO,CAAC,MAAM;IACP,SAAS,CAAC,EAAE,MAAM;IAT3B,IAAI,SAAsB;IAC1B,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,CAAC,gBAAgB,CAAoC;IAC5D,OAAO,CAAC,eAAe,CAA6B;gBAG1C,MAAM,EAAE,kBAAkB,EAC3B,SAAS,CAAC,EAAE,MAAM,YAAA;IAGrB,KAAK;IAYL,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IAiC9D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC;IAyEjD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;IAInD,UAAU,CAAC,MAAM,EAAE,MAAM;CA4ChC;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,SAAS,CAEX"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.15.9",
3
+ "version": "0.16.1",
4
4
  "description": "Svelte 5 Runes state and reactive integrations for Critical Path headless project management framework",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
@@ -18,9 +18,9 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@critical-path/client": "0.14.5",
22
- "@critical-path/core": "0.20.2",
23
- "@critical-path/mcp": "0.6.8"
21
+ "@critical-path/core": "0.22.0",
22
+ "@critical-path/client": "0.14.7",
23
+ "@critical-path/mcp": "0.7.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "svelte": "^5.0.0"
package/src/index.test.ts CHANGED
@@ -241,6 +241,67 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
241
241
  // Should restore deleted task
242
242
  expect(taskState.data).toEqual([mockTask]);
243
243
  });
244
+
245
+ it('resolves in-flight temporary task when updated or deleted before creation completes', async () => {
246
+ let resolveCreate: (value: Task) => void;
247
+ const createPromise = new Promise<Task>((resolve) => {
248
+ resolveCreate = resolve;
249
+ });
250
+
251
+ const mockClient = {
252
+ getTasks: vi.fn().mockResolvedValue([]),
253
+ createTask: vi.fn().mockReturnValue(createPromise),
254
+ updateTask: vi.fn().mockImplementation((taskId: string, updates: Partial<Task>) =>
255
+ Promise.resolve({ id: taskId, projectId: 'proj_1', title: 'Real Task', status: 'todo', priority: 'medium', createdAt: '2026-01-01', updatedAt: '2026-01-01', ...updates } as Task)
256
+ ),
257
+ deleteTask: vi.fn().mockResolvedValue(undefined)
258
+ } as unknown as CriticalPathClient;
259
+
260
+ const taskState = createTaskState(mockClient, 'proj_1');
261
+ await taskState.fetch();
262
+
263
+ // Start creation
264
+ const creationAction = taskState.createTask({
265
+ projectId: 'proj_1',
266
+ title: 'Draft Task',
267
+ status: 'todo',
268
+ priority: 'medium'
269
+ });
270
+
271
+ // While in flight, optimistic task with temp_ ID is present
272
+ expect(taskState.data.length).toBe(1);
273
+ const tempId = taskState.data[0].id;
274
+ expect(tempId.startsWith('temp_')).toBe(true);
275
+
276
+ // Immediately update using tempId before creation resolves
277
+ const updateAction = taskState.updateTask(tempId, { title: 'Renamed Draft Task' });
278
+
279
+ // Local optimistic state is updated immediately
280
+ expect(taskState.data[0].title).toBe('Renamed Draft Task');
281
+
282
+ // Now resolve the server creation
283
+ const serverTask: Task = {
284
+ id: 'real_task_99',
285
+ projectId: 'proj_1',
286
+ title: 'Draft Task',
287
+ status: 'todo',
288
+ priority: 'medium',
289
+ createdAt: '2026-01-01',
290
+ updatedAt: '2026-01-01'
291
+ };
292
+ resolveCreate!(serverTask);
293
+
294
+ await creationAction;
295
+ await updateAction;
296
+
297
+ // Verify server update was called with the real task ID, not the temp ID
298
+ expect(mockClient.updateTask).toHaveBeenCalledWith('real_task_99', { title: 'Renamed Draft Task' });
299
+
300
+ // And deleting with the tempId resolves to real_task_99
301
+ await taskState.deleteTask(tempId);
302
+ expect(mockClient.deleteTask).toHaveBeenCalledWith('real_task_99');
303
+ expect(taskState.data.length).toBe(0);
304
+ });
244
305
  });
245
306
 
246
307
  describe('CommentState', () => {
@@ -1,12 +1,15 @@
1
1
  /// <reference types="svelte" />
2
2
  import type { CriticalPathClient } from '@critical-path/client';
3
- import type { Task, TaskStatus } from '@critical-path/core';
3
+ import { type Task, type TaskStatus, isTempTaskId } from '@critical-path/core';
4
4
 
5
5
  export class TaskState {
6
6
  data = $state<Task[]>([]);
7
7
  loading = $state<boolean>(false);
8
8
  error = $state<Error | null>(null);
9
9
 
10
+ private pendingCreations = new Map<string, Promise<Task>>();
11
+ private tempToRealIdMap = new Map<string, string>();
12
+
10
13
  constructor(
11
14
  private client: CriticalPathClient,
12
15
  public projectId?: string
@@ -37,8 +40,12 @@ export class TaskState {
37
40
  // Optimistically add temp task
38
41
  this.data = [...this.data, tempTask];
39
42
 
43
+ const promise = this.client.createTask(input);
44
+ this.pendingCreations.set(tempId, promise);
45
+
40
46
  try {
41
- const created = await this.client.createTask(input);
47
+ const created = await promise;
48
+ this.tempToRealIdMap.set(tempId, created.id);
42
49
  // Replace temp task with created task from server
43
50
  this.data = this.data.map((t) => (t.id === tempId ? created : t));
44
51
  return created;
@@ -48,71 +55,62 @@ export class TaskState {
48
55
  const errorObj = err instanceof Error ? err : new Error(String(err));
49
56
  this.error = errorObj;
50
57
  throw errorObj;
58
+ } finally {
59
+ this.pendingCreations.delete(tempId);
51
60
  }
52
61
  }
53
62
 
54
63
  async updateTask(taskId: string, updates: Partial<Task>) {
55
- const idx = this.data.findIndex((t) => t.id === taskId);
56
- if (idx === -1) {
57
- return this.client.updateTask(taskId, updates);
64
+ let targetId = taskId;
65
+ if (this.tempToRealIdMap.has(taskId)) {
66
+ targetId = this.tempToRealIdMap.get(taskId)!;
58
67
  }
59
68
 
60
- const previousTask = this.data[idx];
61
- const optimisticTask: Task = {
62
- ...previousTask,
63
- ...updates,
64
- updatedAt: new Date().toISOString()
65
- };
66
-
67
- // Optimistically update local task
68
- const newData = [...this.data];
69
- newData[idx] = optimisticTask;
70
- this.data = newData;
69
+ // 1. Immediately apply optimistic updates to the local task (temp or confirmed)
70
+ const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
71
+ let previousTask: Task | undefined;
72
+ if (idx !== -1) {
73
+ previousTask = this.data[idx];
74
+ const optimisticTask: Task = {
75
+ ...previousTask,
76
+ ...updates,
77
+ updatedAt: new Date().toISOString()
78
+ };
79
+ const newData = [...this.data];
80
+ newData[idx] = optimisticTask;
81
+ this.data = newData;
82
+ }
71
83
 
72
- try {
73
- const updated = await this.client.updateTask(taskId, updates);
74
- const currentIdx = this.data.findIndex((t) => t.id === taskId);
75
- if (currentIdx !== -1) {
76
- const confirmedData = [...this.data];
77
- confirmedData[currentIdx] = updated;
78
- this.data = confirmedData;
79
- }
80
- return updated;
81
- } catch (err) {
82
- // Rollback to previous task on error
83
- const rollbackIdx = this.data.findIndex((t) => t.id === taskId);
84
- if (rollbackIdx !== -1) {
85
- const rollbackData = [...this.data];
86
- rollbackData[rollbackIdx] = previousTask;
87
- this.data = rollbackData;
84
+ // 2. If it's a temporary task, await pending creation if in flight
85
+ if (isTempTaskId(targetId)) {
86
+ const pendingPromise =
87
+ this.pendingCreations.get(targetId) ||
88
+ (this.pendingCreations.size === 1
89
+ ? Array.from(this.pendingCreations.values())[0]
90
+ : undefined);
91
+
92
+ if (pendingPromise) {
93
+ try {
94
+ const created = await pendingPromise;
95
+ if (created && created.id) {
96
+ targetId = created.id;
97
+ this.tempToRealIdMap.set(taskId, targetId);
98
+ }
99
+ } catch {
100
+ throw new Error(`Failed to update task: task creation failed for "${taskId}"`);
101
+ }
88
102
  }
89
- const errorObj = err instanceof Error ? err : new Error(String(err));
90
- this.error = errorObj;
91
- throw errorObj;
92
103
  }
93
- }
94
104
 
95
- async updateTaskStatus(taskId: string, status: TaskStatus) {
96
- const idx = this.data.findIndex((t) => t.id === taskId);
97
- if (idx === -1) {
98
- return this.client.updateTask(taskId, { status });
105
+ // If still a temp ID, task exists purely in local client state; avoid sending unresolvable temp ID
106
+ if (isTempTaskId(targetId)) {
107
+ const currentTask = this.data.find((t) => t.id === targetId || t.id === taskId);
108
+ return (currentTask || { id: targetId, ...updates }) as Task;
99
109
  }
100
110
 
101
- const previousTask = this.data[idx];
102
- const optimisticTask: Task = {
103
- ...previousTask,
104
- status,
105
- updatedAt: new Date().toISOString()
106
- };
107
-
108
- // Optimistically update local task status
109
- const newData = [...this.data];
110
- newData[idx] = optimisticTask;
111
- this.data = newData;
112
-
113
111
  try {
114
- const updated = await this.client.updateTask(taskId, { status });
115
- const currentIdx = this.data.findIndex((t) => t.id === taskId);
112
+ const updated = await this.client.updateTask(targetId, updates);
113
+ const currentIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
116
114
  if (currentIdx !== -1) {
117
115
  const confirmedData = [...this.data];
118
116
  confirmedData[currentIdx] = updated;
@@ -121,11 +119,13 @@ export class TaskState {
121
119
  return updated;
122
120
  } catch (err) {
123
121
  // Rollback to previous task on error
124
- const rollbackIdx = this.data.findIndex((t) => t.id === taskId);
125
- if (rollbackIdx !== -1) {
126
- const rollbackData = [...this.data];
127
- rollbackData[rollbackIdx] = previousTask;
128
- this.data = rollbackData;
122
+ if (previousTask) {
123
+ const rollbackIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
124
+ if (rollbackIdx !== -1) {
125
+ const rollbackData = [...this.data];
126
+ rollbackData[rollbackIdx] = previousTask;
127
+ this.data = rollbackData;
128
+ }
129
129
  }
130
130
  const errorObj = err instanceof Error ? err : new Error(String(err));
131
131
  this.error = errorObj;
@@ -133,24 +133,49 @@ export class TaskState {
133
133
  }
134
134
  }
135
135
 
136
+ async updateTaskStatus(taskId: string, status: TaskStatus) {
137
+ return this.updateTask(taskId, { status });
138
+ }
139
+
136
140
  async deleteTask(taskId: string) {
137
- const idx = this.data.findIndex((t) => t.id === taskId);
138
- if (idx === -1) {
139
- return this.client.deleteTask(taskId);
141
+ let targetId = taskId;
142
+ if (this.tempToRealIdMap.has(taskId)) {
143
+ targetId = this.tempToRealIdMap.get(taskId)!;
140
144
  }
141
145
 
142
- const previousTask = this.data[idx];
146
+ const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
147
+ const previousTask = idx !== -1 ? this.data[idx] : undefined;
143
148
 
144
- // Optimistically remove task
145
- this.data = this.data.filter((t) => t.id !== taskId);
149
+ // Optimistically remove task from local state
150
+ if (idx !== -1) {
151
+ this.data = this.data.filter((t) => t.id !== targetId && t.id !== taskId);
152
+ }
153
+
154
+ // If temporary task:
155
+ if (isTempTaskId(targetId)) {
156
+ const pendingPromise = this.pendingCreations.get(targetId);
157
+ if (pendingPromise) {
158
+ try {
159
+ const created = await pendingPromise;
160
+ if (created && created.id) {
161
+ await this.client.deleteTask(created.id);
162
+ }
163
+ } catch {
164
+ // If creation failed anyway, task is already gone
165
+ }
166
+ }
167
+ return;
168
+ }
146
169
 
147
170
  try {
148
- await this.client.deleteTask(taskId);
171
+ await this.client.deleteTask(targetId);
149
172
  } catch (err) {
150
173
  // Rollback on error by re-inserting at previous index
151
- const restoredData = [...this.data];
152
- restoredData.splice(idx, 0, previousTask);
153
- this.data = restoredData;
174
+ if (previousTask && idx !== -1) {
175
+ const restoredData = [...this.data];
176
+ restoredData.splice(idx, 0, previousTask);
177
+ this.data = restoredData;
178
+ }
154
179
  const errorObj = err instanceof Error ? err : new Error(String(err));
155
180
  this.error = errorObj;
156
181
  throw errorObj;