@morlay/session-rdb 0.0.15 → 0.0.16-alpha.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.
- package/README.md +11 -13
- package/dist/artifact.d.mts +30 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-GdKkSSb-.d.mts +239 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +763 -168
- package/dist/log-DBFUBPhv.mjs +394 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-CnWwGToZ.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +929 -227
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +110 -0
- package/src/log.ts +143 -91
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-vwzwEXNE.mjs +0 -878
- package/dist/sqlite-CG_Qcx5C.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/src/postgres.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { and, eq, gte, sql } from "drizzle-orm";
|
|
4
|
+
import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
|
|
5
|
+
import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
|
|
6
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
4
7
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
8
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
9
|
import {
|
|
@@ -10,11 +13,13 @@ import {
|
|
|
10
13
|
type EventRow,
|
|
11
14
|
type SessionRow,
|
|
12
15
|
} from "./backend.ts";
|
|
13
|
-
import {
|
|
14
|
-
import { createTablesSql, toPostgresSchema } from "./adapters/index.ts";
|
|
16
|
+
import { toPostgresSchema } from "./adapters/index.ts";
|
|
15
17
|
import { postgresTableDefs } from "./entities/index.ts";
|
|
16
18
|
import { sessionConflictRow, sessionInsertRow } from "./log.ts";
|
|
17
19
|
|
|
20
|
+
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
21
|
+
const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
|
|
22
|
+
|
|
18
23
|
export interface PostgresBackendOptions {
|
|
19
24
|
identityBase: string;
|
|
20
25
|
|
|
@@ -23,60 +28,36 @@ export interface PostgresBackendOptions {
|
|
|
23
28
|
close: () => Promise<void>;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export class PostgresBackend
|
|
31
|
+
export class PostgresBackend implements Backend {
|
|
27
32
|
readonly kind = "postgres" as const;
|
|
28
33
|
storeIdentity!: string;
|
|
29
34
|
|
|
30
35
|
private readonly tables: Record<string, any>;
|
|
31
36
|
|
|
32
37
|
constructor(
|
|
33
|
-
private readonly db:
|
|
38
|
+
private readonly db: NodePgDatabase,
|
|
34
39
|
private readonly options: PostgresBackendOptions,
|
|
35
40
|
) {
|
|
36
41
|
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
async open(): Promise<void> {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) {
|
|
53
|
-
await tx.execute(sql.raw(statement));
|
|
54
|
-
}
|
|
55
|
-
if (!metaExists) {
|
|
56
|
-
await tx
|
|
57
|
-
.insert(this.tables["t_schema_meta"])
|
|
58
|
-
.values([
|
|
59
|
-
{ fKey: "schema_version", fValue: String(SCHEMA_VERSION) },
|
|
60
|
-
{ fKey: "application_id", fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) },
|
|
61
|
-
])
|
|
62
|
-
.execute();
|
|
63
|
-
}
|
|
64
|
-
// 校验:缺版本行 = 有对象但未版本化 → 拒绝,不迁移。
|
|
65
|
-
const version = await this.readMeta(tx, "schema_version");
|
|
66
|
-
const applicationId = await this.readMeta(tx, "application_id");
|
|
67
|
-
if (version === undefined || applicationId === undefined) {
|
|
68
|
-
throw new Error("session database has an unversioned schema or application identity");
|
|
69
|
-
}
|
|
70
|
-
if (Number(version) !== SCHEMA_VERSION) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`session database has schema version ${version}, incompatible with this build (${SCHEMA_VERSION})`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (Number(applicationId) !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
|
|
78
|
-
);
|
|
45
|
+
const schema = this.options.schema ?? "public";
|
|
46
|
+
// drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
|
|
47
|
+
// 本版本只执行 v3 diff(删 f_original_seq)。
|
|
48
|
+
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
49
|
+
const probe = (await this.db.execute(
|
|
50
|
+
sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
|
|
51
|
+
)) as unknown as { rows: { exists: boolean }[] };
|
|
52
|
+
const metaExists = probe.rows[0]?.exists === true;
|
|
53
|
+
if (metaExists) {
|
|
54
|
+
const version = await this.readMeta(this.db, "schema_version");
|
|
55
|
+
if (version === "2") {
|
|
56
|
+
await this.baselineV2();
|
|
79
57
|
}
|
|
58
|
+
}
|
|
59
|
+
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
60
|
+
const storeId = await this.db.transaction(async (tx) => {
|
|
80
61
|
await tx
|
|
81
62
|
.insert(this.tables["t_persistence_state"])
|
|
82
63
|
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
@@ -87,15 +68,36 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
87
68
|
.from(this.tables["t_persistence_state"])
|
|
88
69
|
.where(eq(this.tables["t_persistence_state"].fSingleton, 1))
|
|
89
70
|
.execute();
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
71
|
+
const id = store[0]?.fStoreId;
|
|
72
|
+
if (id === undefined || id.length === 0) {
|
|
92
73
|
throw new Error("session database has no valid store identity");
|
|
93
74
|
}
|
|
94
|
-
return
|
|
75
|
+
return id;
|
|
95
76
|
});
|
|
96
77
|
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
97
78
|
}
|
|
98
79
|
|
|
80
|
+
/** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
|
|
81
|
+
private async baselineV2(): Promise<void> {
|
|
82
|
+
const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
|
|
83
|
+
if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/postgres");
|
|
84
|
+
await this.db.execute(sql`
|
|
85
|
+
CREATE SCHEMA IF NOT EXISTS drizzle
|
|
86
|
+
`);
|
|
87
|
+
await this.db.execute(sql`
|
|
88
|
+
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
89
|
+
id SERIAL PRIMARY KEY,
|
|
90
|
+
hash text NOT NULL,
|
|
91
|
+
created_at bigint,
|
|
92
|
+
name text,
|
|
93
|
+
applied_at timestamp with time zone DEFAULT now()
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
await this.db.execute(
|
|
97
|
+
sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
99
101
|
async close(): Promise<void> {
|
|
100
102
|
await this.options.close();
|
|
101
103
|
}
|
|
@@ -110,17 +112,6 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
110
112
|
)[0] as SessionRow | undefined;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
|
|
114
|
-
return this.db
|
|
115
|
-
.select({
|
|
116
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
117
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
118
|
-
})
|
|
119
|
-
.from(this.tables["t_session_events"])
|
|
120
|
-
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
121
|
-
.execute();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
115
|
async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
|
|
125
116
|
const scoped =
|
|
126
117
|
fromSequence === undefined
|
|
@@ -146,7 +137,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
146
137
|
return this.db.transaction(async (tx) => fn(this.txFor(tx)));
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
private txFor(tx: PgAsyncTransaction<
|
|
140
|
+
private txFor(tx: PgAsyncTransaction<NodePgQueryResultHKT>): BackendTx {
|
|
150
141
|
return {
|
|
151
142
|
upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
|
|
152
143
|
getHead: (id) => this.getHead(tx, id),
|
|
@@ -159,13 +150,15 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
159
150
|
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
160
151
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
161
152
|
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
|
|
162
|
-
getLastBridge: (id) => this.getLastBridge(tx, id),
|
|
163
153
|
};
|
|
164
154
|
}
|
|
165
155
|
|
|
166
156
|
// --- meta helpers ---
|
|
167
157
|
|
|
168
|
-
private async readMeta(
|
|
158
|
+
private async readMeta(
|
|
159
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
160
|
+
key: string,
|
|
161
|
+
): Promise<string | undefined> {
|
|
169
162
|
const rows = await exec
|
|
170
163
|
.select({ fValue: this.tables["t_schema_meta"].fValue })
|
|
171
164
|
.from(this.tables["t_schema_meta"])
|
|
@@ -177,7 +170,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
177
170
|
// --- row primitives (transaction-internal) ---
|
|
178
171
|
|
|
179
172
|
private async upsertSession(
|
|
180
|
-
exec: PgAsyncDatabase<
|
|
173
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
181
174
|
storage: SessionStorageMetadata,
|
|
182
175
|
incarnation: string,
|
|
183
176
|
): Promise<void> {
|
|
@@ -192,7 +185,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
private async getHead(
|
|
195
|
-
exec: PgAsyncDatabase<
|
|
188
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
196
189
|
id: SessionId,
|
|
197
190
|
): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
|
|
198
191
|
const head = (
|
|
@@ -210,7 +203,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
210
203
|
return head;
|
|
211
204
|
}
|
|
212
205
|
|
|
213
|
-
private async getSeedLength(
|
|
206
|
+
private async getSeedLength(
|
|
207
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
208
|
+
id: SessionId,
|
|
209
|
+
): Promise<number | null> {
|
|
214
210
|
const row = (
|
|
215
211
|
await exec
|
|
216
212
|
.select({ fSeedLength: this.tables["t_sessions"].fSeedLength })
|
|
@@ -224,7 +220,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
private async updateSeedLength(
|
|
227
|
-
exec: PgAsyncDatabase<
|
|
223
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
228
224
|
id: SessionId,
|
|
229
225
|
seedLength: number,
|
|
230
226
|
): Promise<void> {
|
|
@@ -237,7 +233,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
237
233
|
|
|
238
234
|
private static readonly INSERT_BATCH_ROWS = 1000;
|
|
239
235
|
|
|
240
|
-
private async insertEvents(
|
|
236
|
+
private async insertEvents(
|
|
237
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
238
|
+
events: EventInsert[],
|
|
239
|
+
): Promise<void> {
|
|
241
240
|
if (events.length === 0) return;
|
|
242
241
|
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
243
242
|
await exec
|
|
@@ -250,12 +249,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
250
249
|
}
|
|
251
250
|
|
|
252
251
|
private async insertBridges(
|
|
253
|
-
exec: PgAsyncDatabase<
|
|
252
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
254
253
|
rows: Array<{
|
|
255
254
|
fSessionId: SessionId;
|
|
256
255
|
fEventId: string;
|
|
257
256
|
fSequence: number;
|
|
258
|
-
fOriginalSeq: number;
|
|
259
257
|
fSurfaceOp: string | null;
|
|
260
258
|
}>,
|
|
261
259
|
): Promise<void> {
|
|
@@ -269,7 +267,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
private async updateHead(
|
|
272
|
-
exec: PgAsyncDatabase<
|
|
270
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
273
271
|
id: SessionId,
|
|
274
272
|
headEventId: string,
|
|
275
273
|
headSequence: number,
|
|
@@ -281,7 +279,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
281
279
|
.execute();
|
|
282
280
|
}
|
|
283
281
|
|
|
284
|
-
private async bumpRevision(
|
|
282
|
+
private async bumpRevision(
|
|
283
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
284
|
+
id: SessionId,
|
|
285
|
+
): Promise<void> {
|
|
285
286
|
await exec
|
|
286
287
|
.update(this.tables["t_sessions"])
|
|
287
288
|
.set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` })
|
|
@@ -290,7 +291,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
private async deleteBridgeTail(
|
|
293
|
-
exec: PgAsyncDatabase<
|
|
294
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
294
295
|
id: SessionId,
|
|
295
296
|
fromSequence: number,
|
|
296
297
|
): Promise<void> {
|
|
@@ -306,7 +307,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
private async getPrevBridge(
|
|
309
|
-
exec: PgAsyncDatabase<
|
|
310
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
310
311
|
id: SessionId,
|
|
311
312
|
sequence: number,
|
|
312
313
|
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
@@ -327,30 +328,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
327
328
|
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
private
|
|
331
|
-
exec: PgAsyncDatabase<THKT>,
|
|
332
|
-
id: SessionId,
|
|
333
|
-
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
334
|
-
return (
|
|
335
|
-
await exec
|
|
336
|
-
.select({
|
|
337
|
-
fEventId: this.tables["t_session_events"].fEventId,
|
|
338
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
339
|
-
})
|
|
340
|
-
.from(this.tables["t_session_events"])
|
|
341
|
-
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
342
|
-
.orderBy(desc(this.tables["t_session_events"].fSequence))
|
|
343
|
-
.limit(1)
|
|
344
|
-
.execute()
|
|
345
|
-
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private eventRows(exec: PgAsyncDatabase<THKT>) {
|
|
331
|
+
private eventRows(exec: PgAsyncDatabase<NodePgQueryResultHKT>) {
|
|
349
332
|
return exec
|
|
350
333
|
.select({
|
|
351
334
|
fEventId: this.tables["t_session_events"].fEventId,
|
|
352
335
|
fSequence: this.tables["t_session_events"].fSequence,
|
|
353
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
354
336
|
fType: this.tables["t_events"].fType,
|
|
355
337
|
fKind: this.tables["t_events"].fKind,
|
|
356
338
|
fRole: this.tables["t_events"].fRole,
|
package/src/schema.ts
CHANGED
|
@@ -2,12 +2,10 @@ import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
|
2
2
|
import { toSqliteSchema } from "./adapters/index.ts";
|
|
3
3
|
import { sqliteTableDefs } from "./entities/index.ts";
|
|
4
4
|
|
|
5
|
-
export const SCHEMA_VERSION =
|
|
5
|
+
export const SCHEMA_VERSION = 3;
|
|
6
6
|
|
|
7
7
|
export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
|
|
8
8
|
|
|
9
|
-
export const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"] as const;
|
|
10
|
-
|
|
11
9
|
export const EVENT_ENCODING = "json";
|
|
12
10
|
|
|
13
11
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -15,6 +13,8 @@ const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
|
|
|
15
13
|
|
|
16
14
|
export const tPersistenceState = sqliteTables["t_persistence_state"]!;
|
|
17
15
|
|
|
16
|
+
export const tSchemaMeta = sqliteTables["t_schema_meta"]!;
|
|
17
|
+
|
|
18
18
|
export const tSessions = sqliteTables["t_sessions"]!;
|
|
19
19
|
|
|
20
20
|
export const tEvents = sqliteTables["t_events"]!;
|
|
@@ -29,17 +29,6 @@ export type JournalMode = "wal" | "delete" | "truncate" | "persist";
|
|
|
29
29
|
|
|
30
30
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
31
31
|
|
|
32
|
-
export function isEphemeralType(type: string): boolean {
|
|
33
|
-
return (EPHEMERAL_EVENT_TYPES as readonly string[]).includes(type);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isPersistedEvent(event: SessionEvent): boolean {
|
|
37
|
-
return (
|
|
38
|
-
!isEphemeralType(event.type) &&
|
|
39
|
-
(event as SessionEvent & { ignorable?: unknown }).ignorable !== true
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
32
|
export type EventKind =
|
|
44
33
|
| "message"
|
|
45
34
|
| "thinking"
|
package/src/sqlite.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { statSync } from "node:fs";
|
|
2
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, open } from "node:fs/promises";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import { and,
|
|
6
|
+
import { and, eq, gte, sql } from "drizzle-orm";
|
|
7
7
|
import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
|
8
|
+
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
8
9
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
9
10
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
10
11
|
import {
|
|
@@ -14,10 +15,7 @@ import {
|
|
|
14
15
|
type EventRow,
|
|
15
16
|
type SessionRow,
|
|
16
17
|
} from "./backend.ts";
|
|
17
|
-
import { createTablesSql } from "./adapters/index.ts";
|
|
18
|
-
import { sqliteTableDefs } from "./entities/index.ts";
|
|
19
18
|
import { sessionConflictRow, sessionInsertRow } from "./log.ts";
|
|
20
|
-
import { migrateSqliteV1ToV2 } from "./migrate.ts";
|
|
21
19
|
import {
|
|
22
20
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
23
21
|
SCHEMA_VERSION,
|
|
@@ -31,6 +29,9 @@ import {
|
|
|
31
29
|
|
|
32
30
|
type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
|
|
33
31
|
|
|
32
|
+
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
33
|
+
const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url).pathname;
|
|
34
|
+
|
|
34
35
|
const sqliteTxQueues = new Map<string, Promise<void>>();
|
|
35
36
|
|
|
36
37
|
function enqueueSqliteTx<T>(path: string, fn: () => Promise<T>): Promise<T> {
|
|
@@ -64,6 +65,28 @@ export function openDatabase(
|
|
|
64
65
|
const db = new DatabaseSync(path);
|
|
65
66
|
try {
|
|
66
67
|
configureDatabase(db, path, journalMode, busyTimeout);
|
|
68
|
+
const dbx = drizzle({ client: db });
|
|
69
|
+
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get() as {
|
|
70
|
+
user_version: number;
|
|
71
|
+
};
|
|
72
|
+
// v2 库(旧版本建表)首次打开:建迁移表并标记 v2 baseline 已应用,
|
|
73
|
+
// 使 migrate 只执行 v3 diff(删 f_original_seq、建 t_schema_meta)。
|
|
74
|
+
if (onDisk === 2 && !hasMigrationsTable(db)) {
|
|
75
|
+
baselineV2(db);
|
|
76
|
+
}
|
|
77
|
+
migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
78
|
+
// store 身份单例:新库由 migrate 建表后插入;v2/v3 库已有行(no-op)。
|
|
79
|
+
dbx
|
|
80
|
+
.insert(tPersistenceState)
|
|
81
|
+
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
82
|
+
.onConflictDoNothing()
|
|
83
|
+
.run();
|
|
84
|
+
if (onDisk === 0 || onDisk === 2) {
|
|
85
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
86
|
+
}
|
|
87
|
+
// journal_mode 不可绑定,经校验后的联合可直接插值;在归属校验、迁移
|
|
88
|
+
// 与身份写入全部成功之后应用(失败时保持磁盘原状)。
|
|
89
|
+
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
|
|
67
90
|
return db;
|
|
68
91
|
} catch (error: unknown) {
|
|
69
92
|
db.close();
|
|
@@ -71,6 +94,32 @@ export function openDatabase(
|
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
|
|
97
|
+
function hasMigrationsTable(db: DatabaseSync): boolean {
|
|
98
|
+
return (
|
|
99
|
+
db
|
|
100
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'")
|
|
101
|
+
.get() !== undefined
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
|
|
106
|
+
function baselineV2(db: DatabaseSync): void {
|
|
107
|
+
const dir = readdirSync(sqliteMigrationsDir).find((name) => name.endsWith("_v2_initial"));
|
|
108
|
+
if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/sqlite");
|
|
109
|
+
db.exec(`
|
|
110
|
+
CREATE TABLE __drizzle_migrations (
|
|
111
|
+
id INTEGER PRIMARY KEY,
|
|
112
|
+
hash text NOT NULL,
|
|
113
|
+
created_at numeric,
|
|
114
|
+
name text,
|
|
115
|
+
applied_at TEXT
|
|
116
|
+
)
|
|
117
|
+
`);
|
|
118
|
+
db.prepare(
|
|
119
|
+
"INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES (?, ?, ?, ?)",
|
|
120
|
+
).run("baseline", 0, dir, new Date().toISOString());
|
|
121
|
+
}
|
|
122
|
+
|
|
74
123
|
function configureDatabase(
|
|
75
124
|
db: DatabaseSync,
|
|
76
125
|
path: string,
|
|
@@ -99,29 +148,16 @@ function configureDatabase(
|
|
|
99
148
|
`session database at "${path}" has an unversioned schema or application identity`,
|
|
100
149
|
);
|
|
101
150
|
}
|
|
102
|
-
if (onDisk
|
|
103
|
-
// 启动时自动迁移 v1 → v2(同一写锁事务内,失败整体回滚)。
|
|
104
|
-
migrateSqliteV1ToV2(db);
|
|
105
|
-
} else if (onDisk !== 0 && onDisk !== SCHEMA_VERSION) {
|
|
151
|
+
if (onDisk !== 0 && onDisk !== 2 && onDisk !== SCHEMA_VERSION) {
|
|
106
152
|
throw new Error(
|
|
107
153
|
`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`,
|
|
108
154
|
);
|
|
109
155
|
}
|
|
110
|
-
if (
|
|
111
|
-
(onDisk === SCHEMA_VERSION || onDisk === 1) &&
|
|
112
|
-
applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID
|
|
113
|
-
) {
|
|
156
|
+
if (onDisk >= 1 && applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
|
|
114
157
|
throw new Error(
|
|
115
158
|
`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
|
|
116
159
|
);
|
|
117
160
|
}
|
|
118
|
-
for (const statement of createTablesSql("sqlite", sqliteTableDefs)) {
|
|
119
|
-
tx.run(sql.raw(statement));
|
|
120
|
-
}
|
|
121
|
-
tx.insert(tPersistenceState)
|
|
122
|
-
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
123
|
-
.onConflictDoNothing()
|
|
124
|
-
.run();
|
|
125
161
|
if (onDisk === 0) {
|
|
126
162
|
tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
|
|
127
163
|
tx.run(sql.raw(`PRAGMA user_version = ${SCHEMA_VERSION}`));
|
|
@@ -129,9 +165,6 @@ function configureDatabase(
|
|
|
129
165
|
},
|
|
130
166
|
{ behavior: "immediate" },
|
|
131
167
|
);
|
|
132
|
-
// journal_mode 不可绑定,经校验后的联合可直接插值;在归属校验与初始化
|
|
133
|
-
// 提交之后应用。
|
|
134
|
-
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
|
|
135
168
|
}
|
|
136
169
|
|
|
137
170
|
export interface SqliteBackendOptions {
|
|
@@ -164,6 +197,9 @@ export class SqliteBackend implements Backend {
|
|
|
164
197
|
this.db = drizzle({
|
|
165
198
|
client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout),
|
|
166
199
|
});
|
|
200
|
+
// drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
|
|
201
|
+
// 本版本只执行 v3 diff(删 f_original_seq、建 t_schema_meta)。
|
|
202
|
+
migrate(this.db, { migrationsFolder: sqliteMigrationsDir });
|
|
167
203
|
});
|
|
168
204
|
try {
|
|
169
205
|
const row = this.db
|
|
@@ -203,14 +239,6 @@ export class SqliteBackend implements Backend {
|
|
|
203
239
|
| undefined;
|
|
204
240
|
}
|
|
205
241
|
|
|
206
|
-
async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
|
|
207
|
-
return this.db
|
|
208
|
-
.select({ fSequence: tSessionEvents.fSequence, fOriginalSeq: tSessionEvents.fOriginalSeq })
|
|
209
|
-
.from(tSessionEvents)
|
|
210
|
-
.where(eq(tSessionEvents.fSessionId, id))
|
|
211
|
-
.all();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
242
|
async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
|
|
215
243
|
const scoped =
|
|
216
244
|
fromSequence === undefined
|
|
@@ -263,7 +291,6 @@ export class SqliteBackend implements Backend {
|
|
|
263
291
|
bumpRevision: (id) => this.bumpRevision(id),
|
|
264
292
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
265
293
|
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
|
|
266
|
-
getLastBridge: (id) => this.getLastBridge(id),
|
|
267
294
|
};
|
|
268
295
|
|
|
269
296
|
// --- row primitives (transaction-internal or standalone) ---
|
|
@@ -328,7 +355,6 @@ export class SqliteBackend implements Backend {
|
|
|
328
355
|
fSessionId: SessionId;
|
|
329
356
|
fEventId: string;
|
|
330
357
|
fSequence: number;
|
|
331
|
-
fOriginalSeq: number;
|
|
332
358
|
fSurfaceOp: string | null;
|
|
333
359
|
}>,
|
|
334
360
|
): Promise<void> {
|
|
@@ -379,24 +405,11 @@ export class SqliteBackend implements Backend {
|
|
|
379
405
|
.get() as { fEventId: string; fSequence: number } | undefined;
|
|
380
406
|
}
|
|
381
407
|
|
|
382
|
-
private async getLastBridge(
|
|
383
|
-
id: SessionId,
|
|
384
|
-
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
385
|
-
return this.db
|
|
386
|
-
.select({ fEventId: tSessionEvents.fEventId, fSequence: tSessionEvents.fSequence })
|
|
387
|
-
.from(tSessionEvents)
|
|
388
|
-
.where(eq(tSessionEvents.fSessionId, id))
|
|
389
|
-
.orderBy(desc(tSessionEvents.fSequence))
|
|
390
|
-
.limit(1)
|
|
391
|
-
.get() as { fEventId: string; fSequence: number } | undefined;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
408
|
private eventRows() {
|
|
395
409
|
return this.db
|
|
396
410
|
.select({
|
|
397
411
|
fEventId: tSessionEvents.fEventId,
|
|
398
412
|
fSequence: tSessionEvents.fSequence,
|
|
399
|
-
fOriginalSeq: tSessionEvents.fOriginalSeq,
|
|
400
413
|
fType: tEvents.fType,
|
|
401
414
|
fKind: tEvents.fKind,
|
|
402
415
|
fRole: tEvents.fRole,
|