@critical-path/svelte 0.2.1 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @critical-path/svelte
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 3470b61: add optimistic updates with error rollback to Svelte state classes and React hooks
8
+
9
+ ## 0.2.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [df22aad]
14
+ - @critical-path/core@0.2.1
15
+ - @critical-path/client@0.1.4
16
+
3
17
  ## 0.2.1
4
18
 
5
19
  ### Patch Changes
package/dist/index.js CHANGED
@@ -38,8 +38,21 @@ var n = class {
38
38
  }
39
39
  }
40
40
  async createProject(e) {
41
- let t = await this.client.createProject(e);
42
- return this.data = [...this.data, t], t;
41
+ let t = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, n = (/* @__PURE__ */ new Date()).toISOString(), r = {
42
+ ...e,
43
+ id: t,
44
+ createdAt: n,
45
+ updatedAt: n
46
+ };
47
+ this.data = [...this.data, r];
48
+ try {
49
+ let n = await this.client.createProject(e);
50
+ return this.data = this.data.map((e) => e.id === t ? n : e), n;
51
+ } catch (e) {
52
+ this.data = this.data.filter((e) => e.id !== t);
53
+ let n = e instanceof Error ? e : Error(String(e));
54
+ throw this.error = n, n;
55
+ }
43
56
  }
44
57
  };
45
58
  function r(e) {
@@ -85,15 +98,61 @@ var i = class {
85
98
  }
86
99
  }
87
100
  async createTask(e) {
88
- let t = await this.client.createTask(e);
89
- return this.data = [...this.data, t], t;
101
+ let t = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, n = (/* @__PURE__ */ new Date()).toISOString(), r = {
102
+ ...e,
103
+ id: t,
104
+ createdAt: n,
105
+ updatedAt: n
106
+ };
107
+ this.data = [...this.data, r];
108
+ try {
109
+ let n = await this.client.createTask(e);
110
+ return this.data = this.data.map((e) => e.id === t ? n : e), n;
111
+ } catch (e) {
112
+ this.data = this.data.filter((e) => e.id !== t);
113
+ let n = e instanceof Error ? e : Error(String(e));
114
+ throw this.error = n, n;
115
+ }
90
116
  }
91
117
  async updateTaskStatus(e, t) {
92
- let n = await this.client.updateTask(e, { status: t }), r = this.data.findIndex((t) => t.id === e);
93
- return r !== -1 && (this.data[r] = n), n;
118
+ let n = this.data.findIndex((t) => t.id === e);
119
+ if (n === -1) return this.client.updateTask(e, { status: t });
120
+ let r = this.data[n], i = {
121
+ ...r,
122
+ status: t,
123
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
124
+ }, a = [...this.data];
125
+ a[n] = i, this.data = a;
126
+ try {
127
+ let n = await this.client.updateTask(e, { status: t }), r = this.data.findIndex((t) => t.id === e);
128
+ if (r !== -1) {
129
+ let e = [...this.data];
130
+ e[r] = n, this.data = e;
131
+ }
132
+ return n;
133
+ } catch (t) {
134
+ let n = this.data.findIndex((t) => t.id === e);
135
+ if (n !== -1) {
136
+ let e = [...this.data];
137
+ e[n] = r, this.data = e;
138
+ }
139
+ let i = t instanceof Error ? t : Error(String(t));
140
+ throw this.error = i, i;
141
+ }
94
142
  }
95
143
  async deleteTask(e) {
96
- await this.client.deleteTask(e), this.data = this.data.filter((t) => t.id !== e);
144
+ let t = this.data.findIndex((t) => t.id === e);
145
+ if (t === -1) return this.client.deleteTask(e);
146
+ let n = this.data[t];
147
+ this.data = this.data.filter((t) => t.id !== e);
148
+ try {
149
+ await this.client.deleteTask(e);
150
+ } catch (e) {
151
+ let r = [...this.data];
152
+ r.splice(t, 0, n), this.data = r;
153
+ let i = e instanceof Error ? e : Error(String(e));
154
+ throw this.error = i, i;
155
+ }
97
156
  }
98
157
  };
99
158
  function a(e, t) {
@@ -1 +1 @@
1
- {"version":3,"file":"project-state.svelte.d.ts","sourceRoot":"","sources":["../src/project-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,qBAAa,YAAY;IAKX,OAAO,CAAC,MAAM;IAJ1B,IAAI,YAAyB;IAC7B,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAEf,MAAM,EAAE,kBAAkB;IAExC,KAAK;IAYL,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;CAK3E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAE3E"}
1
+ {"version":3,"file":"project-state.svelte.d.ts","sourceRoot":"","sources":["../src/project-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,qBAAa,YAAY;IAKX,OAAO,CAAC,MAAM;IAJ1B,IAAI,YAAyB;IAC7B,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAEf,MAAM,EAAE,kBAAkB;IAExC,KAAK;IAYL,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;CA0B3E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAE3E"}
@@ -10,7 +10,7 @@ export declare class TaskState {
10
10
  fetch(): Promise<void>;
11
11
  createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
12
12
  updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
13
- deleteTask(taskId: string): Promise<void>;
13
+ deleteTask(taskId: string): Promise<boolean | undefined>;
14
14
  }
15
15
  export declare function createTaskState(client: CriticalPathClient, projectId?: string): TaskState;
16
16
  //# 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;IAM9D,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;IASnD,UAAU,CAAC,MAAM,EAAE,MAAM;CAIhC;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,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,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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,8 +18,8 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@critical-path/client": "0.1.3",
22
- "@critical-path/core": "0.2.0"
21
+ "@critical-path/client": "0.1.4",
22
+ "@critical-path/core": "0.2.1"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "svelte": "^5.0.0"
package/src/index.test.ts CHANGED
@@ -45,7 +45,7 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
45
45
  expect(projectState.error).toBeNull();
46
46
  });
47
47
 
48
- it('creates project and appends to reactive data', async () => {
48
+ it('creates project optimistically and rolls back on failure', async () => {
49
49
  const mockCreated: Project = {
50
50
  id: 'proj_2',
51
51
  name: 'Project 2',
@@ -62,11 +62,20 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
62
62
 
63
63
  expect(res).toEqual(mockCreated);
64
64
  expect(projectState.data).toEqual([mockCreated]);
65
+
66
+ // Test error rollback
67
+ const failingClient = {
68
+ createProject: vi.fn().mockRejectedValue(new Error('Network error'))
69
+ } as unknown as CriticalPathClient;
70
+ const failingState = new ProjectState(failingClient);
71
+
72
+ await expect(failingState.createProject({ name: 'Failed Proj', key: 'FAIL' })).rejects.toThrow('Network error');
73
+ expect(failingState.data).toEqual([]);
65
74
  });
66
75
  });
67
76
 
68
77
  describe('TaskState', () => {
69
- it('fetches, creates, updates status, and deletes tasks', async () => {
78
+ it('fetches, creates, updates status, and deletes tasks with optimistic updates', async () => {
70
79
  const mockTask: Task = {
71
80
  id: 'task_1',
72
81
  projectId: 'proj_1',
@@ -90,11 +99,61 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
90
99
 
91
100
  expect(taskState.data).toEqual([mockTask]);
92
101
 
102
+ // Update status
93
103
  await taskState.updateTaskStatus('task_1', 'in_progress');
94
104
  expect(taskState.data[0].status).toBe('in_progress');
95
105
 
106
+ // Delete task
96
107
  await taskState.deleteTask('task_1');
97
108
  expect(taskState.data).toEqual([]);
98
109
  });
110
+
111
+ it('rolls back task status update on server error', async () => {
112
+ const mockTask: Task = {
113
+ id: 'task_1',
114
+ projectId: 'proj_1',
115
+ title: 'Task 1',
116
+ status: 'todo',
117
+ priority: 'medium',
118
+ createdAt: '2026-01-01',
119
+ updatedAt: '2026-01-01'
120
+ };
121
+
122
+ const failingClient = {
123
+ getTasks: vi.fn().mockResolvedValue([mockTask]),
124
+ updateTask: vi.fn().mockRejectedValue(new Error('Server error'))
125
+ } as unknown as CriticalPathClient;
126
+
127
+ const taskState = createTaskState(failingClient, 'proj_1');
128
+ await taskState.fetch();
129
+
130
+ await expect(taskState.updateTaskStatus('task_1', 'in_progress')).rejects.toThrow('Server error');
131
+ // Should be rolled back to 'todo'
132
+ expect(taskState.data[0].status).toBe('todo');
133
+ });
134
+
135
+ it('rolls back task deletion on server error', async () => {
136
+ const mockTask: Task = {
137
+ id: 'task_1',
138
+ projectId: 'proj_1',
139
+ title: 'Task 1',
140
+ status: 'todo',
141
+ priority: 'medium',
142
+ createdAt: '2026-01-01',
143
+ updatedAt: '2026-01-01'
144
+ };
145
+
146
+ const failingClient = {
147
+ getTasks: vi.fn().mockResolvedValue([mockTask]),
148
+ deleteTask: vi.fn().mockRejectedValue(new Error('Delete failed'))
149
+ } as unknown as CriticalPathClient;
150
+
151
+ const taskState = createTaskState(failingClient, 'proj_1');
152
+ await taskState.fetch();
153
+
154
+ await expect(taskState.deleteTask('task_1')).rejects.toThrow('Delete failed');
155
+ // Should restore deleted task
156
+ expect(taskState.data).toEqual([mockTask]);
157
+ });
99
158
  });
100
159
  });
@@ -22,9 +22,30 @@ export class ProjectState {
22
22
  }
23
23
 
24
24
  async createProject(input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) {
25
- const created = await this.client.createProject(input);
26
- this.data = [...this.data, created];
27
- return created;
25
+ const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
26
+ const now = new Date().toISOString();
27
+ const tempProject: Project = {
28
+ ...input,
29
+ id: tempId,
30
+ createdAt: now,
31
+ updatedAt: now
32
+ };
33
+
34
+ // Optimistically add temp project
35
+ this.data = [...this.data, tempProject];
36
+
37
+ try {
38
+ const created = await this.client.createProject(input);
39
+ // Replace temp project with server created project
40
+ this.data = this.data.map((p) => (p.id === tempId ? created : p));
41
+ return created;
42
+ } catch (err) {
43
+ // Rollback on error
44
+ this.data = this.data.filter((p) => p.id !== tempId);
45
+ const errorObj = err instanceof Error ? err : new Error(String(err));
46
+ this.error = errorObj;
47
+ throw errorObj;
48
+ }
28
49
  }
29
50
  }
30
51
 
@@ -25,23 +25,95 @@ export class TaskState {
25
25
  }
26
26
 
27
27
  async createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) {
28
- const created = await this.client.createTask(input);
29
- this.data = [...this.data, created];
30
- return created;
28
+ const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
29
+ const now = new Date().toISOString();
30
+ const tempTask: Task = {
31
+ ...input,
32
+ id: tempId,
33
+ createdAt: now,
34
+ updatedAt: now
35
+ };
36
+
37
+ // Optimistically add temp task
38
+ this.data = [...this.data, tempTask];
39
+
40
+ try {
41
+ const created = await this.client.createTask(input);
42
+ // Replace temp task with created task from server
43
+ this.data = this.data.map((t) => (t.id === tempId ? created : t));
44
+ return created;
45
+ } catch (err) {
46
+ // Rollback on error
47
+ this.data = this.data.filter((t) => t.id !== tempId);
48
+ const errorObj = err instanceof Error ? err : new Error(String(err));
49
+ this.error = errorObj;
50
+ throw errorObj;
51
+ }
31
52
  }
32
53
 
33
54
  async updateTaskStatus(taskId: string, status: TaskStatus) {
34
- const updated = await this.client.updateTask(taskId, { status });
35
55
  const idx = this.data.findIndex((t) => t.id === taskId);
36
- if (idx !== -1) {
37
- this.data[idx] = updated;
56
+ if (idx === -1) {
57
+ return this.client.updateTask(taskId, { status });
58
+ }
59
+
60
+ const previousTask = this.data[idx];
61
+ const optimisticTask: Task = {
62
+ ...previousTask,
63
+ status,
64
+ updatedAt: new Date().toISOString()
65
+ };
66
+
67
+ // Optimistically update local task status
68
+ const newData = [...this.data];
69
+ newData[idx] = optimisticTask;
70
+ this.data = newData;
71
+
72
+ try {
73
+ const updated = await this.client.updateTask(taskId, { status });
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;
88
+ }
89
+ const errorObj = err instanceof Error ? err : new Error(String(err));
90
+ this.error = errorObj;
91
+ throw errorObj;
38
92
  }
39
- return updated;
40
93
  }
41
94
 
42
95
  async deleteTask(taskId: string) {
43
- await this.client.deleteTask(taskId);
96
+ const idx = this.data.findIndex((t) => t.id === taskId);
97
+ if (idx === -1) {
98
+ return this.client.deleteTask(taskId);
99
+ }
100
+
101
+ const previousTask = this.data[idx];
102
+
103
+ // Optimistically remove task
44
104
  this.data = this.data.filter((t) => t.id !== taskId);
105
+
106
+ try {
107
+ await this.client.deleteTask(taskId);
108
+ } catch (err) {
109
+ // Rollback on error by re-inserting at previous index
110
+ const restoredData = [...this.data];
111
+ restoredData.splice(idx, 0, previousTask);
112
+ this.data = restoredData;
113
+ const errorObj = err instanceof Error ? err : new Error(String(err));
114
+ this.error = errorObj;
115
+ throw errorObj;
116
+ }
45
117
  }
46
118
  }
47
119