@critical-path/svelte 0.2.0 → 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 +23 -3
- package/LICENSE +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +167 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/project-state.svelte.d.ts +13 -0
- package/dist/project-state.svelte.d.ts.map +1 -0
- package/dist/task-state.svelte.d.ts +16 -0
- package/dist/task-state.svelte.d.ts.map +1 -0
- package/package.json +10 -10
- package/src/index.test.ts +61 -2
- package/src/project-state.svelte.ts +24 -3
- package/src/task-state.svelte.ts +80 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# @critical-path/svelte
|
|
2
2
|
|
|
3
|
-
## 0.2.
|
|
3
|
+
## 0.2.3
|
|
4
4
|
|
|
5
|
-
###
|
|
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
|
+
|
|
17
|
+
## 0.2.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 5160802: Publish Svelte 5 Runes state classes on 0.2.x prerelease line.
|
|
22
|
+
|
|
23
|
+
## 1.0.0
|
|
24
|
+
|
|
25
|
+
### Major Changes
|
|
6
26
|
|
|
7
|
-
-
|
|
27
|
+
- 283e450: Modernize `@critical-path/svelte` for Svelte 5 Runes:
|
|
8
28
|
- Replaced Svelte 4 legacy stores (`createProjectStore`, `createTaskStore`) with Svelte 5 Runes state classes and factories (`ProjectState`, `createProjectState`, `TaskState`, `createTaskState`).
|
|
9
29
|
- Updated peer dependency requirement to `svelte: ^5.0.0`.
|
|
10
30
|
- Components now access reactive state properties directly (e.g. `taskState.data`, `projectState.loading`) without `$` store auto-subscriptions.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack James
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CriticalPathClient, type ClientOptions } from '@critical-path/client';
|
|
2
|
+
export declare function createCriticalPathClient(options: ClientOptions): CriticalPathClient;
|
|
3
|
+
export * from './project-state.svelte.js';
|
|
4
|
+
export * from './task-state.svelte.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,kBAAkB,CAEnF;AAED,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { CriticalPathClient as e } from "@critical-path/client";
|
|
2
|
+
import * as t from "svelte/internal/client";
|
|
3
|
+
//#region src/project-state.svelte.ts
|
|
4
|
+
var n = class {
|
|
5
|
+
client;
|
|
6
|
+
#e = t.state(t.proxy([]));
|
|
7
|
+
get data() {
|
|
8
|
+
return t.get(this.#e);
|
|
9
|
+
}
|
|
10
|
+
set data(e) {
|
|
11
|
+
t.set(this.#e, e, !0);
|
|
12
|
+
}
|
|
13
|
+
#t = t.state(!1);
|
|
14
|
+
get loading() {
|
|
15
|
+
return t.get(this.#t);
|
|
16
|
+
}
|
|
17
|
+
set loading(e) {
|
|
18
|
+
t.set(this.#t, e, !0);
|
|
19
|
+
}
|
|
20
|
+
#n = t.state(null);
|
|
21
|
+
get error() {
|
|
22
|
+
return t.get(this.#n);
|
|
23
|
+
}
|
|
24
|
+
set error(e) {
|
|
25
|
+
t.set(this.#n, e, !0);
|
|
26
|
+
}
|
|
27
|
+
constructor(e) {
|
|
28
|
+
this.client = e;
|
|
29
|
+
}
|
|
30
|
+
async fetch() {
|
|
31
|
+
this.loading = !0, this.error = null;
|
|
32
|
+
try {
|
|
33
|
+
this.data = await this.client.getProjects();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
this.error = e instanceof Error ? e : Error(String(e));
|
|
36
|
+
} finally {
|
|
37
|
+
this.loading = !1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async createProject(e) {
|
|
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
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function r(e) {
|
|
59
|
+
return new n(e);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/task-state.svelte.ts
|
|
63
|
+
var i = class {
|
|
64
|
+
client;
|
|
65
|
+
projectId;
|
|
66
|
+
#e = t.state(t.proxy([]));
|
|
67
|
+
get data() {
|
|
68
|
+
return t.get(this.#e);
|
|
69
|
+
}
|
|
70
|
+
set data(e) {
|
|
71
|
+
t.set(this.#e, e, !0);
|
|
72
|
+
}
|
|
73
|
+
#t = t.state(!1);
|
|
74
|
+
get loading() {
|
|
75
|
+
return t.get(this.#t);
|
|
76
|
+
}
|
|
77
|
+
set loading(e) {
|
|
78
|
+
t.set(this.#t, e, !0);
|
|
79
|
+
}
|
|
80
|
+
#n = t.state(null);
|
|
81
|
+
get error() {
|
|
82
|
+
return t.get(this.#n);
|
|
83
|
+
}
|
|
84
|
+
set error(e) {
|
|
85
|
+
t.set(this.#n, e, !0);
|
|
86
|
+
}
|
|
87
|
+
constructor(e, t) {
|
|
88
|
+
this.client = e, this.projectId = t;
|
|
89
|
+
}
|
|
90
|
+
async fetch() {
|
|
91
|
+
this.loading = !0, this.error = null;
|
|
92
|
+
try {
|
|
93
|
+
this.data = await this.client.getTasks(this.projectId);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
this.error = e instanceof Error ? e : Error(String(e));
|
|
96
|
+
} finally {
|
|
97
|
+
this.loading = !1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async createTask(e) {
|
|
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
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async updateTaskStatus(e, t) {
|
|
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
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async deleteTask(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
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function a(e, t) {
|
|
159
|
+
return new i(e, t);
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/index.ts
|
|
163
|
+
function o(t) {
|
|
164
|
+
return new e(t);
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { n as ProjectState, i as TaskState, o as createCriticalPathClient, r as createProjectState, a as createTaskState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CriticalPathClient } from '@critical-path/client';
|
|
2
|
+
import type { Project } from '@critical-path/core';
|
|
3
|
+
export declare class ProjectState {
|
|
4
|
+
private client;
|
|
5
|
+
data: Project[];
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error: Error | null;
|
|
8
|
+
constructor(client: CriticalPathClient);
|
|
9
|
+
fetch(): Promise<void>;
|
|
10
|
+
createProject(input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createProjectState(client: CriticalPathClient): ProjectState;
|
|
13
|
+
//# sourceMappingURL=project-state.svelte.d.ts.map
|
|
@@ -0,0 +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;CA0B3E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAE3E"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CriticalPathClient } from '@critical-path/client';
|
|
2
|
+
import type { Task, TaskStatus } from '@critical-path/core';
|
|
3
|
+
export declare class TaskState {
|
|
4
|
+
private client;
|
|
5
|
+
projectId?: string | undefined;
|
|
6
|
+
data: Task[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
constructor(client: CriticalPathClient, projectId?: string | undefined);
|
|
10
|
+
fetch(): Promise<void>;
|
|
11
|
+
createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
|
|
12
|
+
updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
|
|
13
|
+
deleteTask(taskId: string): Promise<boolean | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createTaskState(client: CriticalPathClient, projectId?: string): TaskState;
|
|
16
|
+
//# sourceMappingURL=task-state.svelte.d.ts.map
|
|
@@ -0,0 +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,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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@critical-path/svelte",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/Pixerate/
|
|
9
|
+
"url": "git+https://github.com/Pixerate/critical-path.git"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "./dist/index.js",
|
|
@@ -17,14 +17,9 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "vite build && tsc --emitDeclarationOnly",
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"clean": "rm -rf dist *.tsbuildinfo"
|
|
24
|
-
},
|
|
25
20
|
"dependencies": {
|
|
26
|
-
"@critical-path/client": "
|
|
27
|
-
"@critical-path/core": "
|
|
21
|
+
"@critical-path/client": "0.1.4",
|
|
22
|
+
"@critical-path/core": "0.2.1"
|
|
28
23
|
},
|
|
29
24
|
"peerDependencies": {
|
|
30
25
|
"svelte": "^5.0.0"
|
|
@@ -39,5 +34,10 @@
|
|
|
39
34
|
},
|
|
40
35
|
"engines": {
|
|
41
36
|
"node": ">=24.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "vite build && tsc --emitDeclarationOnly",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
package/src/task-state.svelte.ts
CHANGED
|
@@ -25,23 +25,95 @@ export class TaskState {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
37
|
-
this.
|
|
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
|
-
|
|
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
|
|