@sparkelf/dsh-patch-workspace-storage-restore 0.2.0-rc.3 → 0.2.0-rc.30
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/package.json
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"formatVersion": 1,
|
|
5
5
|
"variants": [
|
|
6
6
|
{
|
|
7
|
-
"dsh": ">=0.1.
|
|
7
|
+
"dsh": ">=0.1.7-rc.2",
|
|
8
8
|
"file": "./patches/workspace-storage-restore.patch",
|
|
9
9
|
"id": "workspace-storage-restore",
|
|
10
10
|
"target": {
|
|
11
|
-
"baseRevision": "
|
|
11
|
+
"baseRevision": "477b4f420553e8a52c2fbccc464d7561b239c443",
|
|
12
12
|
"kind": "dsh-source",
|
|
13
13
|
"paths": [
|
|
14
14
|
"packages/workspace/workspace/"
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"url": "git+https://github.com/SparkElf/deepseek-harness-plus.git"
|
|
36
36
|
},
|
|
37
37
|
"type": "module",
|
|
38
|
-
"version": "0.2.0-rc.
|
|
38
|
+
"version": "0.2.0-rc.30"
|
|
39
39
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
diff --git a/packages/workspace/workspace/src/index.ts b/packages/workspace/workspace/src/index.ts
|
|
2
|
-
index
|
|
2
|
+
index 00eaf0030c..b6e761e1cb 100644
|
|
3
3
|
--- a/packages/workspace/workspace/src/index.ts
|
|
4
4
|
+++ b/packages/workspace/workspace/src/index.ts
|
|
5
|
-
@@ -10,7 +10,7 @@ import { stat } from 'node:fs/promises'
|
|
5
|
+
@@ -10,7 +10,7 @@ import { mkdir, stat } from 'node:fs/promises'
|
|
6
6
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
7
7
|
import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
|
8
8
|
import type {} from '@deepseek-ai/dsh-session-persistence'
|
|
@@ -11,7 +11,7 @@ index 037344fce4..7b6d915807 100644
|
|
|
11
11
|
import { WorkspaceEntity } from './entity.ts'
|
|
12
12
|
import type { WorkspaceEntityHost } from './entity.ts'
|
|
13
13
|
|
|
14
|
-
@@ -
|
|
14
|
+
@@ -170,6 +170,7 @@ const compareHeaders = (left: SessionHeader, right: SessionHeader): number =>
|
|
15
15
|
export class WorkspaceRegistry extends Service {
|
|
16
16
|
static inject = ['storageDomain', 'sessionPersistence']
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ index 037344fce4..7b6d915807 100644
|
|
|
19
19
|
private table?: KvTable<WorkspaceId, WorkspaceRecord>
|
|
20
20
|
private global?: DomainGlobal<WorkspaceDomainState>
|
|
21
21
|
private state?: WorkspaceDomainState
|
|
22
|
-
@@ -
|
|
22
|
+
@@ -195,8 +196,53 @@ export class WorkspaceRegistry extends Service {
|
|
23
23
|
|
|
24
24
|
/** Open the domain, finish bootstrap when required, and rebuild the ordered cache. */
|
|
25
25
|
protected async [Service.init](): Promise<void> {
|
|
@@ -36,10 +36,12 @@ index 037344fce4..7b6d915807 100644
|
|
|
36
36
|
+ * @returns the operation result after the new durable baseline is available.
|
|
37
37
|
+ */
|
|
38
38
|
+ async withStorageRestore<T>(restore: () => Promise<T>): Promise<T> {
|
|
39
|
-
+ // restore
|
|
39
|
+
+ // The restore replaces durable files, so the domain must be closed first;
|
|
40
|
+
+ // the finally clause reuses the startup path to rebuild every cache.
|
|
40
41
|
+ return await this.enqueueOperation(async () => {
|
|
41
|
-
+
|
|
42
|
-
+
|
|
42
|
+
+ // The queue slot runs recoverPendingMutation first, which requires the
|
|
43
|
+
+ // registry state, so the open domain is present by this point.
|
|
44
|
+
+ const domain = this.domain as Domain<typeof workspaceDomainSpec>
|
|
43
45
|
+ const previousArchivedSessions = this.requireState().archivedSessionIds.length
|
|
44
46
|
+ await domain.close()
|
|
45
47
|
+ delete this.domain
|
|
@@ -72,3 +74,113 @@ index 037344fce4..7b6d915807 100644
|
|
|
72
74
|
this.table = domain.table('workspaces')
|
|
73
75
|
this.global = domain.global
|
|
74
76
|
this.state = domain.global.get()
|
|
77
|
+
diff --git a/packages/workspace/workspace/tests/workspace.spec.ts b/packages/workspace/workspace/tests/workspace.spec.ts
|
|
78
|
+
index ef3116b3d7..67721e6474 100644
|
|
79
|
+
--- a/packages/workspace/workspace/tests/workspace.spec.ts
|
|
80
|
+
+++ b/packages/workspace/workspace/tests/workspace.spec.ts
|
|
81
|
+
@@ -1378,6 +1378,105 @@ describe('first-use Workspace preparation', () => {
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
+describe('WorkspaceRegistry storage restore', () => {
|
|
86
|
+
+ const contexts: Context[] = []
|
|
87
|
+
+
|
|
88
|
+
+ afterEach(async () => {
|
|
89
|
+
+ for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
|
|
90
|
+
+ })
|
|
91
|
+
+
|
|
92
|
+
+ it('serves the replaced baseline, not the pre-restore window, once the restore returns', async () => {
|
|
93
|
+
+ const kept = await makeDir('restore-kept')
|
|
94
|
+
+ const restored = await makeDir('restore-kept-2')
|
|
95
|
+
+ const h = await harness({ sessions: [header('kept', kept, 100)] })
|
|
96
|
+
+ contexts.push(h.ctx)
|
|
97
|
+
+ const created = await h.registry.create(kept)
|
|
98
|
+
+ expect(h.registry.list().map(workspace => workspace.id)).toEqual([created.id])
|
|
99
|
+
+
|
|
100
|
+
+ // A restore replaces the durable medium wholesale: the registry must
|
|
101
|
+
+ // observe the replacement only after reopening, never the old window.
|
|
102
|
+
+ const id = WorkspaceId('restored-workspace')
|
|
103
|
+
+ const replaced = await h.registry.withStorageRestore(async () => {
|
|
104
|
+
+ const medium = h.pool.media.get('workspace')!
|
|
105
|
+
+ medium.tables.set('workspaces', new Map([[id, {
|
|
106
|
+
+ path: restored,
|
|
107
|
+
+ title: 'restored',
|
|
108
|
+
+ sessionIds: [],
|
|
109
|
+
+ createdAt: '1970-01-01T00:00:01.000Z',
|
|
110
|
+
+ updatedAt: '1970-01-01T00:00:01.000Z',
|
|
111
|
+
+ }]]))
|
|
112
|
+
+ medium.global = {
|
|
113
|
+
+ initialized: true,
|
|
114
|
+
+ workspaceIds: [id],
|
|
115
|
+
+ archivedSessionIds: [SessionId('archived-before-restore')],
|
|
116
|
+
+ pinnedSessionIds: [],
|
|
117
|
+
+ }
|
|
118
|
+
+ return 'replaced'
|
|
119
|
+
+ })
|
|
120
|
+
+
|
|
121
|
+
+ expect(replaced).toBe('replaced')
|
|
122
|
+
+ // Both the record set and the registry-global archive set come from the
|
|
123
|
+
+ // reopened domain; the pre-restore workspace and archive set are gone.
|
|
124
|
+
+ expect(h.registry.list().map(workspace => workspace.path)).toEqual([restored])
|
|
125
|
+
+ expect(h.registry.archivedSessionIds).toEqual([SessionId('archived-before-restore')])
|
|
126
|
+
+ expect(h.registry.list().map(workspace => workspace.id)).not.toContain(created.id)
|
|
127
|
+
+ })
|
|
128
|
+
+
|
|
129
|
+
+ it('reopens through startup bootstrap so a restored empty baseline is rebuilt', async () => {
|
|
130
|
+
+ const dir = await makeDir('restore-rebuild')
|
|
131
|
+
+ const h = await harness({ sessions: [header('rebuild', dir, 100)] })
|
|
132
|
+
+ contexts.push(h.ctx)
|
|
133
|
+
+ await h.registry.create(dir)
|
|
134
|
+
+ expect(h.registry.list()).toHaveLength(1)
|
|
135
|
+
+
|
|
136
|
+
+ await h.registry.withStorageRestore(async () => {
|
|
137
|
+
+ const medium = h.pool.media.get('workspace')!
|
|
138
|
+
+ medium.tables.set('workspaces', new Map())
|
|
139
|
+
+ medium.global = null
|
|
140
|
+
+ })
|
|
141
|
+
+
|
|
142
|
+
+ // The restored medium carries no initialized marker, so the reopened
|
|
143
|
+
+ // registry bootstraps the header history again instead of serving a stale cache.
|
|
144
|
+
+ expect(storedState(h.pool).initialized).toBe(true)
|
|
145
|
+
+ expect(h.registry.list()).toHaveLength(1)
|
|
146
|
+
+ })
|
|
147
|
+
+
|
|
148
|
+
+ it('rebuilds every cache on the restored baseline after the replacement throws', async () => {
|
|
149
|
+
+ const dir = await makeDir('restore-throws')
|
|
150
|
+
+ const h = await harness({ sessions: [header('throws', dir, 100)] })
|
|
151
|
+
+ contexts.push(h.ctx)
|
|
152
|
+
+ await h.registry.create(dir)
|
|
153
|
+
+
|
|
154
|
+
+ await expect(h.registry.withStorageRestore(async () => {
|
|
155
|
+
+ const medium = h.pool.media.get('workspace')!
|
|
156
|
+
+ medium.tables.set('workspaces', new Map())
|
|
157
|
+
+ medium.global = null
|
|
158
|
+
+ throw new Error('replacement failed')
|
|
159
|
+
+ })).rejects.toThrow('replacement failed')
|
|
160
|
+
+
|
|
161
|
+
+ // The reopen runs in the finally arm, so the registry is still usable:
|
|
162
|
+
+ // the restored empty medium is bootstrapped from the Session headers again.
|
|
163
|
+
+ expect(h.registry.list().map(workspace => workspace.path)).toEqual([dir])
|
|
164
|
+
+ await expect(h.registry.create(dir)).resolves.toBeDefined()
|
|
165
|
+
+ })
|
|
166
|
+
+
|
|
167
|
+
+ it('serializes a queued mutation behind the restore it was issued during', async () => {
|
|
168
|
+
+ const dir = await makeDir('restore-queued')
|
|
169
|
+
+ const h = await harness({ sessions: [header('queued', dir, 100)] })
|
|
170
|
+
+ contexts.push(h.ctx)
|
|
171
|
+
+ const created = await h.registry.create(dir)
|
|
172
|
+
+ const order: string[] = []
|
|
173
|
+
+ // The restore takes the registry queue slot synchronously, so the delete
|
|
174
|
+
+ // issued next runs only after the domain was closed and reopened.
|
|
175
|
+
+ const restoring = h.registry.withStorageRestore(async () => { order.push('restore') })
|
|
176
|
+
+ const deleting = h.registry.delete(created.id).then((ok) => { order.push('delete'); return ok })
|
|
177
|
+
+ await Promise.all([restoring, deleting])
|
|
178
|
+
+ expect(order).toEqual(['restore', 'delete'])
|
|
179
|
+
+ expect(await deleting).toBe(true)
|
|
180
|
+
+ expect(h.registry.list()).toEqual([])
|
|
181
|
+
+ })
|
|
182
|
+
+})
|
|
183
|
+
+
|
|
184
|
+
// The registry knows no family: the providers merge theirs, and this suite merges its own.
|
|
185
|
+
declare module '@deepseek-ai/dsh-workspace/types' {
|
|
186
|
+
interface SessionActivityKindMap {
|