@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-alpha.2
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/README.md +18 -3
- package/dist/artifact.d.mts +118 -2
- package/dist/artifact.mjs +5 -3
- package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
- package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
- package/dist/branch-CG1PUlO6.mjs +833 -0
- package/dist/deletion-SSX7IqU8.mjs +35 -0
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -1
- package/dist/export.mjs +31 -55
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +31 -76
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1561 -4
- package/dist/open-YoyD_kah.mjs +561 -0
- package/dist/organize.d.mts +9 -0
- package/dist/organize.mjs +30 -0
- package/dist/route-DpfY0zq-.mjs +114 -0
- package/dist/storage.d.mts +41 -48
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -1
- package/dist/testing.mjs +25 -4
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +7 -40
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +10 -14
- package/src/artifact.ts +4 -1
- package/src/backend-rdb.ts +331 -0
- package/src/backend.ts +32 -15
- package/src/{branch.ts → branch/index.ts} +109 -110
- package/src/branch/port.ts +62 -0
- package/src/branch/semantics.ts +69 -0
- package/src/db/open.ts +180 -0
- package/src/db/stats.ts +449 -0
- package/src/deletion-error.ts +17 -0
- package/src/deletion.ts +19 -72
- package/src/entities/index.ts +16 -1
- package/src/entities/v3/event-usage.ts +3 -3
- package/src/entities/v3/events.ts +3 -3
- package/src/entities/v3/index.ts +4 -16
- package/src/entities/v3/persistence-state.ts +3 -3
- package/src/entities/v3/schema-meta.ts +3 -3
- package/src/entities/v3/session-counts.ts +3 -3
- package/src/entities/v3/session-events.ts +3 -3
- package/src/entities/v3/session-projcache-rows.ts +3 -3
- package/src/entities/v3/session-usage.ts +3 -3
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-records.ts +17 -0
- package/src/entities/v3/storage-units.ts +3 -3
- package/src/{schema.ts → event-shape.ts} +0 -46
- package/src/export.ts +32 -78
- package/src/handles.ts +349 -0
- package/src/http/route.ts +178 -0
- package/src/import-storages.ts +5 -2
- package/src/import.ts +63 -125
- package/src/index.ts +15 -1666
- package/src/legacy.ts +3 -1
- package/src/live-routing.ts +205 -0
- package/src/log/export.ts +84 -0
- package/src/{log.ts → log/repair.ts} +1 -268
- package/src/log/rows.ts +107 -0
- package/src/log/usage-row.ts +64 -0
- package/src/migrations.ts +196 -0
- package/src/organize.ts +38 -0
- package/src/read-path.ts +190 -0
- package/src/routes.ts +2 -1
- package/src/rows.ts +25 -77
- package/src/session-ops.ts +444 -0
- package/src/session-store.ts +675 -0
- package/src/stats-fold.ts +44 -0
- package/src/storage-takeover/index.ts +2 -5
- package/src/storage-takeover/legacy-domain-migration.ts +118 -0
- package/src/storage-takeover/projection-cache.ts +122 -71
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +236 -442
- package/src/storage-takeover/storage-backend.ts +44 -159
- package/src/storage-takeover/types.ts +45 -19
- package/src/storage-takeover/workspace-documents.ts +65 -0
- package/src/storage.ts +4 -2
- package/src/testing.ts +21 -1
- package/src/usage.ts +11 -65
- package/src/version.ts +13 -0
- package/src/write-path.ts +286 -0
- package/dist/branch-CoPCRKer.mjs +0 -401
- package/dist/deletion-CWl89TIR.mjs +0 -2249
- package/dist/gc.d.mts +0 -4
- package/dist/gc.mjs +0 -48
- package/dist/log-CcWy46VE.mjs +0 -558
- package/dist/routes-hZjTcX_R.mjs +0 -9
- package/dist/sqlite-BNRd1aqR.mjs +0 -1778
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
- package/src/adapters/index.ts +0 -12
- package/src/adapters/to-postgres.ts +0 -93
- package/src/adapters/to-sqlite.ts +0 -83
- package/src/adapters/types.ts +0 -44
- package/src/drizzle/postgres-v2.ts +0 -10
- package/src/drizzle/postgres-v3.ts +0 -18
- package/src/drizzle/sqlite-v2.ts +0 -9
- package/src/drizzle/sqlite-v3.ts +0 -18
- package/src/entities/v2/index.ts +0 -20
- package/src/entities/v2/session-events.ts +0 -10
- package/src/entities/v3/schedule-tasks.ts +0 -16
- package/src/entities/v3/workspace-sessions.ts +0 -19
- package/src/entities/v3/workspace-state.ts +0 -12
- package/src/entities/v3/workspaces.ts +0 -14
- package/src/gc.ts +0 -77
- package/src/postgres.ts +0 -1015
- package/src/sqlite.ts +0 -1096
package/src/handles.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SessionAlreadyExistsError,
|
|
3
|
+
SessionAlreadyOwnedError,
|
|
4
|
+
SessionHandleClosedError,
|
|
5
|
+
SessionPersistenceNotFoundError,
|
|
6
|
+
SessionPersistenceRevision,
|
|
7
|
+
SessionReadOnlyError,
|
|
8
|
+
assertContiguous,
|
|
9
|
+
materializeAppendBatch,
|
|
10
|
+
type SessionAccess,
|
|
11
|
+
type SessionHandle,
|
|
12
|
+
type SessionHandleAppendOptions,
|
|
13
|
+
type SessionHandleFlushOptions,
|
|
14
|
+
type SessionHandleReadOptions,
|
|
15
|
+
type SessionHandleReadResult,
|
|
16
|
+
} from "@deepseek-ai/dsh-session-persistence";
|
|
17
|
+
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
18
|
+
import type { SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
|
|
19
|
+
import type { SessionPersistenceRdb } from "./session-store.ts";
|
|
20
|
+
import { repairReadView } from "./log/repair.ts";
|
|
21
|
+
|
|
22
|
+
interface PendingSession {
|
|
23
|
+
readonly header: SessionHeader;
|
|
24
|
+
readonly revision: SessionPersistenceRevision;
|
|
25
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
26
|
+
|
|
27
|
+
readonly cursor: number;
|
|
28
|
+
|
|
29
|
+
readonly everAppended: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class RdbBackendTracker {
|
|
33
|
+
private readonly writers = new Map<SessionId, RdbSessionHandle | null>();
|
|
34
|
+
private readonly pending = new Map<SessionId, PendingSession>();
|
|
35
|
+
private readonly openHandles = new Set<RdbSessionHandle>();
|
|
36
|
+
private counter = 0;
|
|
37
|
+
|
|
38
|
+
constructor(private readonly name: string) {}
|
|
39
|
+
|
|
40
|
+
registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void {
|
|
41
|
+
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
42
|
+
this.writers.set(header.id, null);
|
|
43
|
+
this.pending.set(header.id, {
|
|
44
|
+
header,
|
|
45
|
+
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
46
|
+
inheritedEventCount,
|
|
47
|
+
cursor: 0,
|
|
48
|
+
everAppended: false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
updatePending(id: SessionId, cursor: number, everAppended: boolean): void {
|
|
53
|
+
const entry = this.pending.get(id);
|
|
54
|
+
if (entry === undefined) return;
|
|
55
|
+
this.pending.set(id, { ...entry, cursor, everAppended });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
claimWrite(id: SessionId): void {
|
|
59
|
+
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
60
|
+
this.writers.set(id, null);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
releaseClaim(id: SessionId): void {
|
|
64
|
+
this.writers.delete(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pendingOf(id: SessionId): PendingSession | undefined {
|
|
68
|
+
return this.pending.get(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
hasPending(id: SessionId): boolean {
|
|
72
|
+
return this.pending.has(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pendingEntries(): IterableIterator<[SessionId, PendingSession]> {
|
|
76
|
+
return this.pending.entries();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
materialized(id: SessionId): void {
|
|
80
|
+
this.pending.delete(id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
adopt(handle: RdbSessionHandle): RdbSessionHandle {
|
|
84
|
+
this.openHandles.add(handle);
|
|
85
|
+
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
86
|
+
return handle;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
release(handle: RdbSessionHandle, materialized: boolean): void {
|
|
90
|
+
this.openHandles.delete(handle);
|
|
91
|
+
if (handle.access !== "write") return;
|
|
92
|
+
this.writers.delete(handle.id);
|
|
93
|
+
if (!materialized) this.pending.delete(handle.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
writerOf(id: SessionId): RdbSessionHandle | undefined {
|
|
97
|
+
const writer = this.writers.get(id);
|
|
98
|
+
return writer === null ? undefined : writer;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
hasOpenHandle(id: SessionId): boolean {
|
|
102
|
+
for (const handle of this.openHandles) {
|
|
103
|
+
if (handle.id === id) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async flushAll(): Promise<void> {
|
|
109
|
+
const errors: unknown[] = [];
|
|
110
|
+
for (const writer of this.writers.values()) {
|
|
111
|
+
if (writer === null) continue;
|
|
112
|
+
try {
|
|
113
|
+
await writer.drainLive();
|
|
114
|
+
await writer.flush();
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
if (error instanceof SessionHandleClosedError) continue;
|
|
117
|
+
errors.push(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async closeAll(): Promise<void> {
|
|
124
|
+
const errors: unknown[] = [];
|
|
125
|
+
for (const handle of this.openHandles) {
|
|
126
|
+
try {
|
|
127
|
+
await handle.close();
|
|
128
|
+
} catch (error: unknown) {
|
|
129
|
+
errors.push(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class RdbSessionHandle implements SessionHandle {
|
|
137
|
+
private chain: Promise<unknown> = Promise.resolve();
|
|
138
|
+
private closing: Promise<void> | undefined;
|
|
139
|
+
|
|
140
|
+
private cursor: number;
|
|
141
|
+
private materialized: boolean;
|
|
142
|
+
|
|
143
|
+
private buffered: SessionEvent[] = [];
|
|
144
|
+
private batchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
145
|
+
private drainPaused = false;
|
|
146
|
+
private draining: Promise<void> | undefined;
|
|
147
|
+
|
|
148
|
+
private tornTruncateTo: number | undefined;
|
|
149
|
+
|
|
150
|
+
private everAppended = false;
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
private readonly persistence: SessionPersistenceRdb,
|
|
154
|
+
readonly id: SessionId,
|
|
155
|
+
readonly header: SessionHeader,
|
|
156
|
+
readonly access: SessionAccess,
|
|
157
|
+
private readonly state: {
|
|
158
|
+
cursor: number;
|
|
159
|
+
materialized: boolean;
|
|
160
|
+
inheritedEventCount: SessionLogOffset;
|
|
161
|
+
tornTruncateTo?: number;
|
|
162
|
+
},
|
|
163
|
+
) {
|
|
164
|
+
this.cursor = state.cursor;
|
|
165
|
+
this.materialized = state.materialized;
|
|
166
|
+
this.tornTruncateTo = state.tornTruncateTo;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get inheritedEventCount(): SessionLogOffset {
|
|
170
|
+
return this.state.inheritedEventCount;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get cursorValue(): number {
|
|
174
|
+
return this.cursor;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async read(
|
|
178
|
+
offset = 0,
|
|
179
|
+
length = Number.MAX_SAFE_INTEGER,
|
|
180
|
+
options?: SessionHandleReadOptions,
|
|
181
|
+
): Promise<SessionHandleReadResult> {
|
|
182
|
+
this.assertOpen("read");
|
|
183
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
184
|
+
throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
185
|
+
}
|
|
186
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
187
|
+
throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
188
|
+
}
|
|
189
|
+
options?.signal?.throwIfAborted();
|
|
190
|
+
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
191
|
+
if (log === undefined) {
|
|
192
|
+
if (this.persistence.tracker.hasPending(this.id)) {
|
|
193
|
+
return { eventState: "detached", events: [] };
|
|
194
|
+
}
|
|
195
|
+
throw new SessionPersistenceNotFoundError(this.id);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
repairReadView(log.events);
|
|
199
|
+
return { eventState: "detached", events: log.events.slice(offset, offset + length) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async append(
|
|
203
|
+
events: readonly SessionEvent[],
|
|
204
|
+
options?: SessionHandleAppendOptions,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
this.assertOpen("append");
|
|
207
|
+
const batch = materializeAppendBatch(events);
|
|
208
|
+
return this.run("append", async () => {
|
|
209
|
+
options?.signal?.throwIfAborted();
|
|
210
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
211
|
+
if (batch.length === 0) return;
|
|
212
|
+
this.everAppended = true;
|
|
213
|
+
assertContiguous(this.id, batch, this.cursor);
|
|
214
|
+
await this.persistence.appendBatch(
|
|
215
|
+
this.header,
|
|
216
|
+
this.state.inheritedEventCount,
|
|
217
|
+
batch,
|
|
218
|
+
this.tornTruncateTo,
|
|
219
|
+
);
|
|
220
|
+
this.tornTruncateTo = undefined;
|
|
221
|
+
|
|
222
|
+
this.cursor += batch.length;
|
|
223
|
+
this.materialized = true;
|
|
224
|
+
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async flush(options?: SessionHandleFlushOptions): Promise<void> {
|
|
229
|
+
return this.run("flush", async () => {
|
|
230
|
+
options?.signal?.throwIfAborted();
|
|
231
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
232
|
+
if (this.materialized) return;
|
|
233
|
+
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
234
|
+
this.materialized = true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
close(): Promise<void> {
|
|
239
|
+
return (this.closing ??= (async () => {
|
|
240
|
+
let drainFailure: unknown;
|
|
241
|
+
for (;;) {
|
|
242
|
+
try {
|
|
243
|
+
await this.drainLive();
|
|
244
|
+
} catch (error: unknown) {
|
|
245
|
+
drainFailure = error;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
await this.chain;
|
|
249
|
+
if (this.buffered.length === 0) break;
|
|
250
|
+
}
|
|
251
|
+
await this.chain;
|
|
252
|
+
const failures: Error[] = [];
|
|
253
|
+
if (drainFailure !== undefined) {
|
|
254
|
+
failures.push(
|
|
255
|
+
drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
259
|
+
if (failures.length > 1)
|
|
260
|
+
throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
261
|
+
if (failures[0] !== undefined) throw failures[0];
|
|
262
|
+
})());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
[Symbol.asyncDispose](): Promise<void> {
|
|
266
|
+
return this.close();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void {
|
|
270
|
+
this.buffered.push(structuredClone(event));
|
|
271
|
+
if (this.batchTimer !== undefined || this.drainPaused) return;
|
|
272
|
+
this.batchTimer = setTimeout(() => {
|
|
273
|
+
this.batchTimer = undefined;
|
|
274
|
+
this.drainLive().catch(reportBackgroundFailure);
|
|
275
|
+
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
resetAfterRewind(cursor: number, inheritedEventCount?: number): void {
|
|
279
|
+
this.cursor = cursor;
|
|
280
|
+
if (inheritedEventCount !== undefined) {
|
|
281
|
+
(this.state as { inheritedEventCount: SessionLogOffset }).inheritedEventCount =
|
|
282
|
+
SessionLogOffset(inheritedEventCount);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
drainLive(): Promise<void> {
|
|
287
|
+
return (this.draining ??= this.drainBuffered().finally(() => {
|
|
288
|
+
this.draining = undefined;
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async drainBuffered(): Promise<void> {
|
|
293
|
+
if (this.batchTimer !== undefined) {
|
|
294
|
+
clearTimeout(this.batchTimer);
|
|
295
|
+
this.batchTimer = undefined;
|
|
296
|
+
}
|
|
297
|
+
this.drainPaused = false;
|
|
298
|
+
while (this.buffered.length > 0) {
|
|
299
|
+
await this.enqueueChain(async () => {
|
|
300
|
+
const batch = this.buffered.splice(0);
|
|
301
|
+
try {
|
|
302
|
+
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
303
|
+
if (fresh.length === 0) return;
|
|
304
|
+
for (const [index, event] of fresh.entries()) {
|
|
305
|
+
if (event.seq !== this.cursor + index) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
await this.persistence.appendBatch(
|
|
312
|
+
this.header,
|
|
313
|
+
this.state.inheritedEventCount,
|
|
314
|
+
fresh,
|
|
315
|
+
this.tornTruncateTo,
|
|
316
|
+
);
|
|
317
|
+
this.tornTruncateTo = undefined;
|
|
318
|
+
this.cursor += fresh.length;
|
|
319
|
+
this.materialized = true;
|
|
320
|
+
} catch (error: unknown) {
|
|
321
|
+
this.buffered = batch.concat(this.buffered);
|
|
322
|
+
this.drainPaused = true;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private enqueueChain(op: () => Promise<void>): Promise<void> {
|
|
330
|
+
const next = this.chain.then(op);
|
|
331
|
+
this.chain = next.catch(() => {});
|
|
332
|
+
return next;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async run(operation: string, op: () => Promise<void>): Promise<void> {
|
|
336
|
+
this.assertOpen(operation);
|
|
337
|
+
return this.enqueueChain(async () => {
|
|
338
|
+
this.assertOpen(operation);
|
|
339
|
+
return op();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private assertOpen(operation: string): void {
|
|
344
|
+
if (this.closing !== undefined) throw new SessionHandleClosedError(this.id, operation);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
348
|
+
}
|
|
349
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
|
|
3
|
+
/** 一条路由自己决定的状态码(鉴权 / 方法 / 解析之外的那些校验)。 */
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
readonly status: number,
|
|
7
|
+
message: string,
|
|
8
|
+
readonly payload: Record<string, unknown> = {},
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "HttpError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JsonRouteReply {
|
|
16
|
+
status?: number;
|
|
17
|
+
body: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type JsonRouteHandler = (
|
|
21
|
+
body: unknown,
|
|
22
|
+
webCtx: Context,
|
|
23
|
+
) => Promise<JsonRouteReply> | JsonRouteReply;
|
|
24
|
+
|
|
25
|
+
export interface JsonRouteSpec {
|
|
26
|
+
/** 精确路径(`/api/...`)。 */
|
|
27
|
+
path: string;
|
|
28
|
+
/** 业务处理:拿到解析后的 JSON(空 body 视为 `{}`);校验失败抛 `HttpError`。 */
|
|
29
|
+
handle: JsonRouteHandler;
|
|
30
|
+
/** 业务错误的状态码映射(没映射的按 500)。 */
|
|
31
|
+
statusOfError?: (error: unknown) => { status: number; body: unknown } | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeJson(
|
|
35
|
+
res: import("node:http").ServerResponse,
|
|
36
|
+
status: number,
|
|
37
|
+
body: unknown,
|
|
38
|
+
): void {
|
|
39
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify(body));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 挂一条路由:鉴权与方法检查这些每处都一样的部分收在这里。
|
|
45
|
+
*
|
|
46
|
+
* 二进制响应(导出 zip)自己写响应体,所以只共用鉴权 / 405 / 错误兜底。
|
|
47
|
+
*/
|
|
48
|
+
export function mountRoute(
|
|
49
|
+
ctx: Context,
|
|
50
|
+
path: string,
|
|
51
|
+
handler: (
|
|
52
|
+
req: import("node:http").IncomingMessage,
|
|
53
|
+
res: import("node:http").ServerResponse,
|
|
54
|
+
) => Promise<void>,
|
|
55
|
+
): void {
|
|
56
|
+
ctx.inject(["webServer", "connection"] as const, (webCtx) => {
|
|
57
|
+
const webServer = webCtx.webServer as unknown as {
|
|
58
|
+
register(route: {
|
|
59
|
+
kind: "exact";
|
|
60
|
+
path: string;
|
|
61
|
+
handler: (
|
|
62
|
+
req: import("node:http").IncomingMessage,
|
|
63
|
+
res: import("node:http").ServerResponse,
|
|
64
|
+
) => void | Promise<void>;
|
|
65
|
+
}): () => void;
|
|
66
|
+
};
|
|
67
|
+
const connection = webCtx.get("connection") as unknown as {
|
|
68
|
+
requestRejection(request: {
|
|
69
|
+
headers: import("node:http").IncomingHttpHeaders;
|
|
70
|
+
}): number | undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return webCtx.effect(
|
|
74
|
+
() =>
|
|
75
|
+
webServer.register({
|
|
76
|
+
kind: "exact",
|
|
77
|
+
path,
|
|
78
|
+
handler: async (req, res) => {
|
|
79
|
+
const rejection = connection.requestRejection(req);
|
|
80
|
+
if (rejection !== undefined) {
|
|
81
|
+
res.writeHead(rejection);
|
|
82
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (req.method !== "POST") {
|
|
86
|
+
writeJson(res, 405, { error: "method not allowed" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await handler(req, res);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (res.headersSent) return;
|
|
93
|
+
writeJson(res, 500, {
|
|
94
|
+
error: error instanceof Error ? error.message : "request failed",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
`session-rdb: ${path}`,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 挂一条 JSON 路由:鉴权、方法、body 解析、错误码这些每处都一样的部分收在这里。
|
|
106
|
+
*
|
|
107
|
+
* 5 个 JSON 路由(rows / usage / gc / deletion / import)共用 `mountJsonRoute`;`export` 的响应体是 zip,走
|
|
108
|
+
* `mountRoute`,只复用鉴权 / 405 / 500。之前每个文件各抄一遍(约 270 行样板,而且 `import` 那条漏了 405)。
|
|
109
|
+
*/
|
|
110
|
+
export function mountJsonRoute(ctx: Context, spec: JsonRouteSpec): void {
|
|
111
|
+
ctx.inject(["webServer", "connection"] as const, (webCtx) => {
|
|
112
|
+
const webServer = webCtx.webServer as unknown as {
|
|
113
|
+
register(route: {
|
|
114
|
+
kind: "exact";
|
|
115
|
+
path: string;
|
|
116
|
+
handler: (
|
|
117
|
+
req: import("node:http").IncomingMessage,
|
|
118
|
+
res: import("node:http").ServerResponse,
|
|
119
|
+
) => void | Promise<void>;
|
|
120
|
+
}): () => void;
|
|
121
|
+
};
|
|
122
|
+
const connection = webCtx.get("connection") as unknown as {
|
|
123
|
+
requestRejection(request: {
|
|
124
|
+
headers: import("node:http").IncomingHttpHeaders;
|
|
125
|
+
}): number | undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return webCtx.effect(
|
|
129
|
+
() =>
|
|
130
|
+
webServer.register({
|
|
131
|
+
kind: "exact",
|
|
132
|
+
path: spec.path,
|
|
133
|
+
handler: async (req, res) => {
|
|
134
|
+
const rejection = connection.requestRejection(req);
|
|
135
|
+
if (rejection !== undefined) {
|
|
136
|
+
res.writeHead(rejection);
|
|
137
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (req.method !== "POST") {
|
|
141
|
+
writeJson(res, 405, { error: "method not allowed" });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const chunks: Buffer[] = [];
|
|
145
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
146
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
147
|
+
let body: unknown = {};
|
|
148
|
+
if (raw !== "") {
|
|
149
|
+
try {
|
|
150
|
+
body = JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
writeJson(res, 400, { error: "request body is not JSON" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const reply = await spec.handle(body, webCtx);
|
|
158
|
+
writeJson(res, reply.status ?? 200, reply.body);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof HttpError) {
|
|
161
|
+
writeJson(res, error.status, { error: error.message, ...error.payload });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const mapped = spec.statusOfError?.(error);
|
|
165
|
+
if (mapped !== undefined) {
|
|
166
|
+
writeJson(res, mapped.status, mapped.body);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
writeJson(res, 500, {
|
|
170
|
+
error: error instanceof Error ? error.message : "request failed",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
`session-rdb: ${spec.path}`,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
package/src/import-storages.ts
CHANGED
|
@@ -47,11 +47,14 @@ export async function importStorages(
|
|
|
47
47
|
WORKSPACE_UNIT_VERSIONS.values().next().value as number,
|
|
48
48
|
);
|
|
49
49
|
for (const [id, record] of Object.entries(tables["workspaces"] ?? {})) {
|
|
50
|
-
await repository.
|
|
50
|
+
await repository.putDomainRecord("workspace", "workspaces", id, record as WorkspaceRecord);
|
|
51
51
|
result.workspaces += 1;
|
|
52
52
|
}
|
|
53
53
|
if (workspaceDocument.global !== undefined && workspaceDocument.global !== null) {
|
|
54
|
-
await repository.
|
|
54
|
+
await repository.writeDomainGlobal(
|
|
55
|
+
"workspace",
|
|
56
|
+
workspaceDocument.global as WorkspaceDomainState,
|
|
57
|
+
);
|
|
55
58
|
result.workspaceState = true;
|
|
56
59
|
}
|
|
57
60
|
}
|