@morlay/session-rdb 0.1.9 → 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 +24 -3
- package/dist/artifact.d.mts +118 -3
- package/dist/artifact.mjs +5 -3
- package/dist/{log-C0q5svm7.d.mts → backend-BKtJ5sO5.d.mts} +189 -291
- 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 -3
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -3
- package/dist/export.mjs +32 -57
- package/dist/import.d.mts +1 -3
- package/dist/import.mjs +32 -78
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1562 -5
- 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 -50
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -5
- package/dist/testing.mjs +27 -8
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +8 -42
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +32 -36
- 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} +125 -112
- 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 -1630
- 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 -272
- 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-query.ts +17 -4
- 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 +139 -77
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +238 -425
- package/src/storage-takeover/storage-backend.ts +47 -149
- package/src/storage-takeover/types.ts +45 -14
- 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-DbFW3rJX.mjs +0 -391
- package/dist/deletion-ODZFcbZm.mjs +0 -2218
- package/dist/gc.d.mts +0 -6
- package/dist/gc.mjs +0 -49
- package/dist/log-DYxdjJ-l.mjs +0 -562
- package/dist/routes-DGBlBE_I.mjs +0 -10
- package/dist/sqlite-DjOnMRh2.mjs +0 -1777
- 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 -1086
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { and, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
|
|
3
2
|
import {
|
|
4
3
|
SESSION_FORMAT_VERSION,
|
|
5
4
|
SessionId,
|
|
@@ -7,67 +6,27 @@ import {
|
|
|
7
6
|
SessionSeq,
|
|
8
7
|
} from "@deepseek-ai/dsh-session";
|
|
9
8
|
import type { SessionSeqCursor } from "@deepseek-ai/dsh-session";
|
|
10
|
-
import type { WorkspaceId } from "@deepseek-ai/dsh-workspace";
|
|
11
9
|
import type {
|
|
12
10
|
CheckpointIdentity,
|
|
11
|
+
DomainRecord,
|
|
13
12
|
ProjectionCheckpoint,
|
|
14
13
|
ProjectionCheckpointRow,
|
|
15
|
-
ScheduleTask,
|
|
16
14
|
StorageRepository,
|
|
15
|
+
StorageRepositoryHost,
|
|
17
16
|
StoredProjcacheEntry,
|
|
18
17
|
WorkspaceDomainState,
|
|
18
|
+
WorkspaceIndexEntry,
|
|
19
19
|
WorkspaceRecord,
|
|
20
20
|
} from "./types.ts";
|
|
21
|
+
import { E, type Database } from "@morlay/rdbkit";
|
|
22
|
+
import { sessionProjcacheRows, sessions, storageRecords, storageUnits } from "../entities/index.ts";
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
db: () => Promise<unknown>;
|
|
24
|
-
|
|
25
|
-
// 不经 await、直接拿到的数据库句柄;未开放时只走异步的 `db`。
|
|
26
|
-
dbDirect?: () => unknown;
|
|
27
|
-
|
|
28
|
-
tables: Record<string, unknown>;
|
|
29
|
-
|
|
30
|
-
writeAtomically: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function allRows<T>(query: unknown): Promise<T[]> {
|
|
34
|
-
const q = query as { all?: () => T[] };
|
|
35
|
-
if (typeof q.all === "function") return q.all();
|
|
36
|
-
return (await query) as T[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function oneRow<T>(query: unknown): Promise<T | undefined> {
|
|
40
|
-
const q = query as { get?: () => T | undefined };
|
|
41
|
-
if (typeof q.get === "function") return q.get();
|
|
42
|
-
const rows = (await query) as T[];
|
|
43
|
-
return rows[0];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function runQuery(query: unknown): Promise<void> {
|
|
47
|
-
const q = query as { run?: () => unknown };
|
|
48
|
-
if (typeof q.run === "function") {
|
|
49
|
-
q.run();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
await query;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface SessionIdentityRow {
|
|
24
|
+
function identityOfSession(row: {
|
|
56
25
|
fVersion: number;
|
|
57
26
|
fCreatedAt: number;
|
|
58
27
|
fCwd: string | null;
|
|
59
28
|
fSeedLength: number | null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface ProjcacheRowRecord {
|
|
63
|
-
fSessionId: string;
|
|
64
|
-
fKey: string;
|
|
65
|
-
fVer: number;
|
|
66
|
-
fSeq: number;
|
|
67
|
-
fVal: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function identityOfSession(row: SessionIdentityRow): CheckpointIdentity {
|
|
29
|
+
}): CheckpointIdentity {
|
|
71
30
|
return {
|
|
72
31
|
formatVersion: row.fVersion,
|
|
73
32
|
createdAt: row.fCreatedAt,
|
|
@@ -81,439 +40,293 @@ function seqCursor(seq: number): SessionSeqCursor {
|
|
|
81
40
|
return seq === -1 ? -1 : SessionSeq(seq);
|
|
82
41
|
}
|
|
83
42
|
|
|
84
|
-
function pendingMutationOf(row: {
|
|
85
|
-
fPendingOperation: string | null;
|
|
86
|
-
fPendingWorkspaceId: string | null;
|
|
87
|
-
}): WorkspaceDomainState["pendingMutation"] {
|
|
88
|
-
const operation = row.fPendingOperation;
|
|
89
|
-
if (operation !== "create" && operation !== "delete") return undefined;
|
|
90
|
-
return { operation, workspaceId: (row.fPendingWorkspaceId ?? "") as WorkspaceId };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface ScheduleTaskRowRecord {
|
|
94
|
-
fId: string;
|
|
95
|
-
fSessionId: string;
|
|
96
|
-
fStatus: string;
|
|
97
|
-
fRecord: string;
|
|
98
|
-
fLastDelivery: string | null;
|
|
99
|
-
fDeliveryHistory: string | null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 行的读写映射:形状校验在 KV 那一层做(`storage-backend.ts` 的 `scheduleTaskOf`),这里只按列拆装——
|
|
103
|
-
// 归属会话与状态是列,其余三段保持上游契约的 JSON 原样(读回时 `dsh-schedule` 的域 schema 还会再校验)。
|
|
104
|
-
function taskFromRow(row: ScheduleTaskRowRecord): ScheduleTask {
|
|
105
|
-
return {
|
|
106
|
-
sessionId: row.fSessionId as SessionId,
|
|
107
|
-
record: JSON.parse(row.fRecord),
|
|
108
|
-
status: row.fStatus === "inactive" ? "inactive" : "active",
|
|
109
|
-
...(row.fLastDelivery === null ? {} : { lastDelivery: JSON.parse(row.fLastDelivery) }),
|
|
110
|
-
...(row.fDeliveryHistory === null ? {} : { deliveryHistory: JSON.parse(row.fDeliveryHistory) }),
|
|
111
|
-
} as ScheduleTask;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function rowFromTask(id: string, task: ScheduleTask): ScheduleTaskRowRecord {
|
|
115
|
-
return {
|
|
116
|
-
fId: id,
|
|
117
|
-
fSessionId: task.sessionId,
|
|
118
|
-
fStatus: task.status === "inactive" ? "inactive" : "active",
|
|
119
|
-
fRecord: JSON.stringify(task.record),
|
|
120
|
-
fLastDelivery: task.lastDelivery === undefined ? null : JSON.stringify(task.lastDelivery),
|
|
121
|
-
fDeliveryHistory:
|
|
122
|
-
task.deliveryHistory === undefined ? null : JSON.stringify(task.deliveryHistory),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
43
|
export function createStorageRepository(host: StorageRepositoryHost): StorageRepository {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
fSeedLength: tSessions.fSeedLength,
|
|
142
|
-
};
|
|
44
|
+
const records = storageRecords.table(host.database);
|
|
45
|
+
const projcache = sessionProjcacheRows.table(host.database);
|
|
46
|
+
const sessionRows = sessions.table(host.database);
|
|
47
|
+
const units = storageUnits.table(host.database);
|
|
48
|
+
|
|
49
|
+
/** 读一个域的 global(空表名那一行);没有就返回 `null`(域层据此用 initial)。 */
|
|
50
|
+
async function readGlobal(domain: string): Promise<unknown> {
|
|
51
|
+
for await (const row of records
|
|
52
|
+
.select({ value: records.column("f_value") })
|
|
53
|
+
.where(E.and(E.eq(records.f_domain, domain), E.eq(records.f_table, GLOBAL_TABLE)))) {
|
|
54
|
+
return JSON.parse(row.value);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
143
58
|
|
|
144
|
-
const
|
|
59
|
+
const GLOBAL_TABLE = "";
|
|
60
|
+
const WORKSPACE_DOMAIN = "workspace";
|
|
61
|
+
const WORKSPACE_TABLE = "workspaces";
|
|
62
|
+
|
|
63
|
+
// workspace 的 global 里那两个集合仍是会话行上的标记:列表的 archived 与钉住顺序按会话查;
|
|
64
|
+
// 文档是唯一的域存储,这里只写穿(找不到会话行的 id 跳过,与既有语义一致)。
|
|
65
|
+
async function syncSessionFlags(database: Database, state: WorkspaceDomainState): Promise<void> {
|
|
66
|
+
const table = sessions.table(database);
|
|
67
|
+
const pinned = state.pinnedSessionIds ?? [];
|
|
68
|
+
// 钉住:先把不在集合里的置空,再按位置写(找不到会话行就跳过,与既有语义一致)。
|
|
69
|
+
await table.update({ f_pinned_seq: null }, E.isNotNull(table.f_pinned_seq));
|
|
70
|
+
for (const [position, sessionId] of pinned.entries()) {
|
|
71
|
+
await table.update({ f_pinned_seq: position }, E.eq(table.f_session_id, sessionId));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const archived = state.archivedSessionIds;
|
|
75
|
+
await table.update({ f_archived_at: null }, E.isNotNull(table.f_archived_at));
|
|
76
|
+
const stamp = Date.now();
|
|
77
|
+
for (const sessionId of archived) {
|
|
78
|
+
// 旧库的归档集合里可能有还没落会话行的 id:先补一行再打标记。
|
|
79
|
+
await table.insertIgnore(
|
|
80
|
+
{
|
|
81
|
+
f_session_id: sessionId,
|
|
82
|
+
f_head_event_id: "",
|
|
83
|
+
f_head_sequence: -1,
|
|
84
|
+
f_version: SESSION_FORMAT_VERSION,
|
|
85
|
+
f_created_at: stamp,
|
|
86
|
+
f_cwd: null,
|
|
87
|
+
f_parent_session: null,
|
|
88
|
+
f_seed_length: null,
|
|
89
|
+
f_origin: null,
|
|
90
|
+
f_delegation_depth: null,
|
|
91
|
+
f_incarnation: randomUUID(),
|
|
92
|
+
f_revision: 0,
|
|
93
|
+
f_archived_at: stamp,
|
|
94
|
+
},
|
|
95
|
+
["f_session_id"],
|
|
96
|
+
);
|
|
97
|
+
await table.update({ f_archived_at: stamp }, E.eq(table.f_session_id, sessionId));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
145
100
|
|
|
146
101
|
return {
|
|
147
102
|
async readUnitVersion(name: string): Promise<number | undefined> {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
103
|
+
for await (const row of units
|
|
104
|
+
.select({ version: units.column("f_version") })
|
|
105
|
+
.where(E.eq(units.f_name, name))) {
|
|
106
|
+
return row.version;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
153
109
|
},
|
|
154
110
|
|
|
155
111
|
async insertUnitVersion(name: string, version: number): Promise<void> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
112
|
+
await host.writeAtomically(async () => {
|
|
113
|
+
await units.insertIgnore({ f_name: name, f_version: version }, ["f_name"]);
|
|
114
|
+
});
|
|
160
115
|
},
|
|
161
116
|
|
|
162
|
-
async
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
fSessionId: string;
|
|
174
|
-
fPosition: number;
|
|
175
|
-
}>(db.select().from(tWorkspaceSessions));
|
|
176
|
-
const sessions = new Map<string, Array<{ id: string; position: number }>>();
|
|
177
|
-
for (const link of links) {
|
|
178
|
-
const owned = sessions.get(link.fWorkspaceId) ?? [];
|
|
179
|
-
owned.push({ id: link.fSessionId, position: link.fPosition });
|
|
180
|
-
sessions.set(link.fWorkspaceId, owned);
|
|
117
|
+
async listDomainRecords(domain: string): Promise<DomainRecord[]> {
|
|
118
|
+
const result: DomainRecord[] = [];
|
|
119
|
+
for await (const row of records
|
|
120
|
+
.select({
|
|
121
|
+
table: records.column("f_table"),
|
|
122
|
+
key: records.column("f_key"),
|
|
123
|
+
value: records.column("f_value"),
|
|
124
|
+
})
|
|
125
|
+
.where(E.eq(records.f_domain, domain))) {
|
|
126
|
+
if (row.table === "") continue;
|
|
127
|
+
result.push({ table: row.table, key: row.key, value: JSON.parse(row.value) });
|
|
181
128
|
}
|
|
182
|
-
return
|
|
183
|
-
id: row.fWorkspaceId,
|
|
184
|
-
record: {
|
|
185
|
-
path: row.fPath,
|
|
186
|
-
title: row.fTitle,
|
|
187
|
-
sessionIds: (sessions.get(row.fWorkspaceId) ?? [])
|
|
188
|
-
.sort((left, right) => left.position - right.position)
|
|
189
|
-
.map((entry) => SessionId(entry.id)),
|
|
190
|
-
createdAt: row.fCreatedAt,
|
|
191
|
-
updatedAt: row.fUpdatedAt,
|
|
192
|
-
},
|
|
193
|
-
}));
|
|
129
|
+
return result;
|
|
194
130
|
},
|
|
195
131
|
|
|
196
|
-
async
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
const values = {
|
|
200
|
-
fWorkspaceId: id,
|
|
201
|
-
fPath: record.path,
|
|
202
|
-
fTitle: record.title,
|
|
203
|
-
fCreatedAt: record.createdAt,
|
|
204
|
-
fUpdatedAt: record.updatedAt,
|
|
205
|
-
};
|
|
206
|
-
await runQuery(
|
|
207
|
-
db
|
|
208
|
-
.insert(tWorkspaces)
|
|
209
|
-
.values(values)
|
|
210
|
-
.onConflictDoUpdate({ target: tWorkspaces.fWorkspaceId, set: values }),
|
|
211
|
-
);
|
|
132
|
+
async readDomainGlobal(domain: string): Promise<unknown> {
|
|
133
|
+
return await readGlobal(domain);
|
|
134
|
+
},
|
|
212
135
|
|
|
213
|
-
|
|
214
|
-
|
|
136
|
+
async putDomainRecord(
|
|
137
|
+
domain: string,
|
|
138
|
+
table: string,
|
|
139
|
+
key: string,
|
|
140
|
+
value: unknown,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
await host.writeAtomically(async () => {
|
|
143
|
+
await records.upsert(
|
|
144
|
+
{ f_domain: domain, f_table: table, f_key: key, f_value: JSON.stringify(value) },
|
|
145
|
+
["f_domain", "f_table", "f_key"],
|
|
146
|
+
["f_value"],
|
|
215
147
|
);
|
|
216
|
-
const links = record.sessionIds.map((sessionId, position) => ({
|
|
217
|
-
fWorkspaceId: id,
|
|
218
|
-
fSessionId: sessionId,
|
|
219
|
-
fPosition: position,
|
|
220
|
-
}));
|
|
221
|
-
if (links.length > 0) await runQuery(db.insert(tWorkspaceSessions).values(links));
|
|
222
148
|
});
|
|
223
149
|
},
|
|
224
150
|
|
|
225
|
-
async
|
|
151
|
+
async deleteDomainRecord(domain: string, table: string, key: string): Promise<void> {
|
|
226
152
|
await host.writeAtomically(async () => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)),
|
|
153
|
+
await records.delete(
|
|
154
|
+
E.and(E.eq(records.f_domain, domain), E.eq(records.f_table, table), E.eq(records.f_key, key)),
|
|
230
155
|
);
|
|
231
|
-
await runQuery(db.delete(tWorkspaces).where(eq(tWorkspaces.fWorkspaceId, id)));
|
|
232
156
|
});
|
|
233
157
|
},
|
|
234
158
|
|
|
235
|
-
async
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}>(db.select().from(tWorkspaceState).where(eq(tWorkspaceState.fSingleton, 1)));
|
|
242
|
-
if (row === undefined) return null;
|
|
243
|
-
const ordered = await allRows<{ fWorkspaceId: string }>(
|
|
244
|
-
db
|
|
245
|
-
.select()
|
|
246
|
-
.from(tWorkspaces)
|
|
247
|
-
.where(gte(tWorkspaces.fPosition, 0))
|
|
248
|
-
.orderBy(tWorkspaces.fPosition),
|
|
249
|
-
);
|
|
250
|
-
const archives = await allRows<{ fSessionId: string }>(
|
|
251
|
-
db
|
|
252
|
-
.select({ fSessionId: tSessions.fSessionId })
|
|
253
|
-
.from(tSessions)
|
|
254
|
-
.where(isNotNull(tSessions.fArchivedAt))
|
|
255
|
-
.orderBy(tSessions.fArchivedAt),
|
|
256
|
-
);
|
|
257
|
-
const pinned = await allRows<{ fSessionId: string; fPinnedSeq: number | null }>(
|
|
258
|
-
db
|
|
259
|
-
.select({ fSessionId: tSessions.fSessionId, fPinnedSeq: tSessions.fPinnedSeq })
|
|
260
|
-
.from(tSessions)
|
|
261
|
-
.where(isNotNull(tSessions.fPinnedSeq))
|
|
262
|
-
.orderBy(tSessions.fPinnedSeq),
|
|
263
|
-
);
|
|
264
|
-
const pendingMutation = pendingMutationOf(row);
|
|
265
|
-
return {
|
|
266
|
-
initialized: row.fInitialized !== 0,
|
|
267
|
-
workspaceIds: ordered.map((entry) => entry.fWorkspaceId as WorkspaceId),
|
|
268
|
-
archivedSessionIds: archives.map((entry) => entry.fSessionId as SessionId),
|
|
269
|
-
pinnedSessionIds: pinned.map((entry) => entry.fSessionId as SessionId),
|
|
270
|
-
...(pendingMutation === undefined ? {} : { pendingMutation }),
|
|
271
|
-
};
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
async writeWorkspaceState(state: WorkspaceDomainState): Promise<void> {
|
|
159
|
+
async deleteDomainRecords(
|
|
160
|
+
domain: string,
|
|
161
|
+
table: string,
|
|
162
|
+
keys: readonly string[],
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (keys.length === 0) return;
|
|
275
165
|
await host.writeAtomically(async () => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
fInitialized: state.initialized ? 1 : 0,
|
|
283
|
-
fPendingOperation:
|
|
284
|
-
pending === undefined || typeof pending.operation !== "string"
|
|
285
|
-
? null
|
|
286
|
-
: pending.operation,
|
|
287
|
-
fPendingWorkspaceId:
|
|
288
|
-
pending === undefined || typeof pending.workspaceId !== "string"
|
|
289
|
-
? null
|
|
290
|
-
: pending.workspaceId,
|
|
291
|
-
};
|
|
292
|
-
await runQuery(
|
|
293
|
-
db
|
|
294
|
-
.insert(tWorkspaceState)
|
|
295
|
-
.values(values)
|
|
296
|
-
.onConflictDoUpdate({ target: tWorkspaceState.fSingleton, set: values }),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
|
|
300
|
-
for (const [position, workspaceId] of state.workspaceIds.entries()) {
|
|
301
|
-
await runQuery(
|
|
302
|
-
db
|
|
303
|
-
.update(tWorkspaces)
|
|
304
|
-
.set({ fPosition: position })
|
|
305
|
-
.where(eq(tWorkspaces.fWorkspaceId, workspaceId)),
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// 钉住:未在集合里的清掉,集合里的按数组位置写序号(读回即按它排序)。
|
|
310
|
-
// 找不到会话行的 id 跳过:pin 只作用于已存在的会话(与归档不同,不凭空造行)。
|
|
311
|
-
// 既有 storages 文档里可能没有这个集合,导入路径按空处理。
|
|
312
|
-
const pinned = state.pinnedSessionIds ?? [];
|
|
313
|
-
await runQuery(
|
|
314
|
-
pinned.length === 0
|
|
315
|
-
? db.update(tSessions).set({ fPinnedSeq: null }).where(isNotNull(tSessions.fPinnedSeq))
|
|
316
|
-
: db
|
|
317
|
-
.update(tSessions)
|
|
318
|
-
.set({ fPinnedSeq: null })
|
|
319
|
-
.where(
|
|
320
|
-
and(isNotNull(tSessions.fPinnedSeq), notInArray(tSessions.fSessionId, pinned)),
|
|
321
|
-
),
|
|
322
|
-
);
|
|
323
|
-
for (const [position, sessionId] of pinned.entries()) {
|
|
324
|
-
await runQuery(
|
|
325
|
-
db
|
|
326
|
-
.update(tSessions)
|
|
327
|
-
.set({ fPinnedSeq: position })
|
|
328
|
-
.where(eq(tSessions.fSessionId, sessionId)),
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const archived = state.archivedSessionIds;
|
|
333
|
-
await runQuery(
|
|
334
|
-
archived.length === 0
|
|
335
|
-
? db
|
|
336
|
-
.update(tSessions)
|
|
337
|
-
.set({ fArchivedAt: null })
|
|
338
|
-
.where(isNotNull(tSessions.fArchivedAt))
|
|
339
|
-
: db
|
|
340
|
-
.update(tSessions)
|
|
341
|
-
.set({ fArchivedAt: null })
|
|
342
|
-
.where(
|
|
343
|
-
and(isNotNull(tSessions.fArchivedAt), notInArray(tSessions.fSessionId, archived)),
|
|
344
|
-
),
|
|
166
|
+
await records.delete(
|
|
167
|
+
E.and(
|
|
168
|
+
E.eq(records.f_domain, domain),
|
|
169
|
+
E.eq(records.f_table, table),
|
|
170
|
+
E.inList(records.f_key, [...new Set(keys)]),
|
|
171
|
+
),
|
|
345
172
|
);
|
|
346
|
-
const stamp = Date.now();
|
|
347
|
-
for (const sessionId of archived) {
|
|
348
|
-
await runQuery(
|
|
349
|
-
db
|
|
350
|
-
.insert(tSessions)
|
|
351
|
-
.values({
|
|
352
|
-
fSessionId: sessionId,
|
|
353
|
-
fHeadEventId: "",
|
|
354
|
-
fHeadSequence: -1,
|
|
355
|
-
fVersion: SESSION_FORMAT_VERSION,
|
|
356
|
-
fCreatedAt: stamp,
|
|
357
|
-
fCwd: null,
|
|
358
|
-
fParentSession: null,
|
|
359
|
-
fSeedLength: null,
|
|
360
|
-
fOrigin: null,
|
|
361
|
-
fDelegationDepth: null,
|
|
362
|
-
fIncarnation: randomUUID(),
|
|
363
|
-
fRevision: 0,
|
|
364
|
-
fArchivedAt: stamp,
|
|
365
|
-
})
|
|
366
|
-
.onConflictDoUpdate({
|
|
367
|
-
target: tSessions.fSessionId,
|
|
368
|
-
|
|
369
|
-
set: { fArchivedAt: sql`COALESCE(${tSessions.fArchivedAt}, ${stamp})` },
|
|
370
|
-
}),
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
173
|
});
|
|
374
174
|
},
|
|
375
175
|
|
|
376
|
-
async
|
|
377
|
-
const db = await dbx();
|
|
378
|
-
const rows = await allRows<ScheduleTaskRowRecord>(db.select().from(tScheduleTasks));
|
|
379
|
-
return rows.map((row) => ({ id: row.fId, task: taskFromRow(row) }));
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
async putScheduleTask(id: string, task: ScheduleTask): Promise<void> {
|
|
176
|
+
async writeDomainGlobal(domain: string, value: unknown): Promise<void> {
|
|
383
177
|
await host.writeAtomically(async () => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
.insert(tScheduleTasks)
|
|
389
|
-
.values(values)
|
|
390
|
-
.onConflictDoUpdate({ target: tScheduleTasks.fId, set: values }),
|
|
178
|
+
await records.upsert(
|
|
179
|
+
{ f_domain: domain, f_table: "", f_key: "", f_value: JSON.stringify(value) },
|
|
180
|
+
["f_domain", "f_table", "f_key"],
|
|
181
|
+
["f_value"],
|
|
391
182
|
);
|
|
183
|
+
// workspace 的归档 / 钉住仍是会话行上的标记(列表与删除都按会话查),由这里写穿。
|
|
184
|
+
if (domain === WORKSPACE_DOMAIN) {
|
|
185
|
+
await syncSessionFlags(host.database, value as WorkspaceDomainState);
|
|
186
|
+
}
|
|
392
187
|
});
|
|
393
188
|
},
|
|
394
189
|
|
|
395
|
-
async
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
190
|
+
async listWorkspaceIndex(): Promise<WorkspaceIndexEntry[]> {
|
|
191
|
+
const rows: Array<{ id: string; record: WorkspaceRecord }> = [];
|
|
192
|
+
for await (const row of records
|
|
193
|
+
.select({ key: records.column("f_key"), value: records.column("f_value") })
|
|
194
|
+
.where(
|
|
195
|
+
E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, WORKSPACE_TABLE)),
|
|
196
|
+
)) {
|
|
197
|
+
rows.push({ id: row.key, record: JSON.parse(row.value) as WorkspaceRecord });
|
|
198
|
+
}
|
|
199
|
+
const state = (await readGlobal(WORKSPACE_DOMAIN)) as WorkspaceDomainState | null;
|
|
200
|
+
const order = new Map<string, number>(
|
|
201
|
+
(state?.workspaceIds ?? []).map((id, position) => [String(id), position]),
|
|
202
|
+
);
|
|
203
|
+
return rows
|
|
204
|
+
.sort(
|
|
205
|
+
(left, right) =>
|
|
206
|
+
(order.get(left.id) ?? Number.MAX_SAFE_INTEGER) -
|
|
207
|
+
(order.get(right.id) ?? Number.MAX_SAFE_INTEGER),
|
|
208
|
+
)
|
|
209
|
+
.map(({ id, record }) => ({ id, title: record.title, sessionIds: [...record.sessionIds] }));
|
|
400
210
|
},
|
|
401
211
|
|
|
402
212
|
async loadProjcache(): Promise<StoredProjcacheEntry[]> {
|
|
403
|
-
const db = await dbx();
|
|
404
|
-
const rows = await allRows<ProjcacheRowRecord>(db.select().from(tProjcacheRows));
|
|
405
|
-
if (rows.length === 0) return [];
|
|
406
|
-
const sessions = await allRows<SessionIdentityRow & { fSessionId: string }>(
|
|
407
|
-
db.select(sessionIdentity).from(tSessions),
|
|
408
|
-
);
|
|
409
213
|
const entries = new Map<string, StoredProjcacheEntry>();
|
|
410
|
-
for (const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
214
|
+
for await (const row of sessionRows.select({
|
|
215
|
+
sessionId: sessionRows.f_session_id,
|
|
216
|
+
version: sessionRows.f_version,
|
|
217
|
+
createdAt: sessionRows.f_created_at,
|
|
218
|
+
cwd: sessionRows.f_cwd,
|
|
219
|
+
seedLength: sessionRows.f_seed_length,
|
|
220
|
+
title: sessionRows.f_title,
|
|
221
|
+
titleSeq: sessionRows.f_title_seq,
|
|
222
|
+
})) {
|
|
223
|
+
entries.set(row.sessionId, {
|
|
224
|
+
sessionId: SessionId(row.sessionId),
|
|
225
|
+
identity: identityOfSession({
|
|
226
|
+
fVersion: row.version,
|
|
227
|
+
// `f_created_at` 是 bigint(读回 BigInt):投影身份的 createdAt 用 number。
|
|
228
|
+
fCreatedAt: Number(row.createdAt),
|
|
229
|
+
fCwd: row.cwd,
|
|
230
|
+
fSeedLength: row.seedLength,
|
|
231
|
+
}),
|
|
414
232
|
rows: {},
|
|
233
|
+
...(typeof row.title === "string" && typeof row.titleSeq === "number"
|
|
234
|
+
? { title: { title: row.title, seq: row.titleSeq } }
|
|
235
|
+
: {}),
|
|
415
236
|
});
|
|
416
237
|
}
|
|
417
|
-
|
|
418
|
-
|
|
238
|
+
if (entries.size === 0) return [];
|
|
239
|
+
for await (const row of projcache.select({
|
|
240
|
+
sessionId: projcache.f_session_id,
|
|
241
|
+
key: projcache.f_key,
|
|
242
|
+
ver: projcache.f_ver,
|
|
243
|
+
seq: projcache.f_seq,
|
|
244
|
+
val: projcache.f_val,
|
|
245
|
+
})) {
|
|
246
|
+
const entry = entries.get(row.sessionId);
|
|
419
247
|
if (entry === undefined) continue;
|
|
420
|
-
entry.rows[row.
|
|
421
|
-
ver: row.
|
|
422
|
-
seq: seqCursor(row.
|
|
423
|
-
val: JSON.parse(row.
|
|
248
|
+
entry.rows[row.key] = {
|
|
249
|
+
ver: row.ver,
|
|
250
|
+
seq: seqCursor(row.seq),
|
|
251
|
+
val: JSON.parse(row.val),
|
|
424
252
|
} satisfies ProjectionCheckpointRow;
|
|
425
253
|
}
|
|
426
|
-
|
|
427
254
|
return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
|
|
428
255
|
},
|
|
429
256
|
|
|
257
|
+
async readSessionTitle(sessionId: string): Promise<{ title: string; seq: number } | undefined> {
|
|
258
|
+
const table = sessions.table(host.database);
|
|
259
|
+
for await (const row of table
|
|
260
|
+
.select({ title: table.f_title, seq: table.f_title_seq })
|
|
261
|
+
.where(E.eq(table.f_session_id, sessionId))) {
|
|
262
|
+
return row.title === null || row.seq === null
|
|
263
|
+
? undefined
|
|
264
|
+
: { title: row.title, seq: row.seq };
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
},
|
|
268
|
+
|
|
430
269
|
async putProjcache(sessionId: string, rows: ProjectionCheckpoint): Promise<void> {
|
|
270
|
+
const values = Object.entries(rows).map(([key, row]) => ({
|
|
271
|
+
f_session_id: sessionId,
|
|
272
|
+
f_key: key,
|
|
273
|
+
f_ver: row.ver,
|
|
274
|
+
f_seq: row.seq,
|
|
275
|
+
f_val: JSON.stringify(row.val),
|
|
276
|
+
}));
|
|
431
277
|
await host.writeAtomically(async () => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
278
|
+
await projcache.delete(E.eq(projcache.f_session_id, sessionId));
|
|
279
|
+
if (values.length > 0) await projcache.insertMany(values);
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async patchProjcache(
|
|
284
|
+
sessionId: string,
|
|
285
|
+
changed: ProjectionCheckpoint,
|
|
286
|
+
removedKeys: readonly string[],
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
const values = Object.entries(changed).map(([key, row]) => ({
|
|
289
|
+
f_session_id: sessionId,
|
|
290
|
+
f_key: key,
|
|
291
|
+
f_ver: row.ver,
|
|
292
|
+
f_seq: row.seq,
|
|
293
|
+
f_val: JSON.stringify(row.val),
|
|
294
|
+
}));
|
|
295
|
+
const touched = [...Object.keys(changed), ...removedKeys];
|
|
296
|
+
if (touched.length === 0) return;
|
|
297
|
+
await host.writeAtomically(async () => {
|
|
298
|
+
// 只碰这些 key 的行:先删(同时承担 removedKeys 的删除),再插入变化行。
|
|
299
|
+
await projcache.delete(
|
|
300
|
+
E.and(E.eq(projcache.f_session_id, sessionId), E.inList(projcache.f_key, touched)),
|
|
301
|
+
);
|
|
302
|
+
if (values.length > 0) await projcache.insertMany(values);
|
|
442
303
|
});
|
|
443
304
|
},
|
|
444
305
|
|
|
445
306
|
async pruneStaleProjcache(): Promise<number> {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
.from(tSessions)
|
|
459
|
-
.where(gte(tSessions.fHeadSequence, 0)),
|
|
460
|
-
),
|
|
307
|
+
const stale: string[] = [];
|
|
308
|
+
for await (const row of projcache
|
|
309
|
+
.select({ sessionId: projcache.f_session_id })
|
|
310
|
+
.where(
|
|
311
|
+
E.and(
|
|
312
|
+
E.lt(projcache.f_seq, 0),
|
|
313
|
+
E.inSubquery(
|
|
314
|
+
projcache.f_session_id,
|
|
315
|
+
sessionRows
|
|
316
|
+
.select({ id: sessionRows.f_session_id })
|
|
317
|
+
.where(E.gte(sessionRows.f_head_sequence, 0))
|
|
318
|
+
.toFragment(),
|
|
461
319
|
),
|
|
462
320
|
),
|
|
463
|
-
|
|
321
|
+
)) {
|
|
322
|
+
stale.push(row.sessionId);
|
|
323
|
+
}
|
|
464
324
|
if (stale.length === 0) return 0;
|
|
465
|
-
await
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
tProjcacheRows.fSessionId,
|
|
469
|
-
stale.map((row) => row.fSessionId),
|
|
470
|
-
),
|
|
471
|
-
),
|
|
472
|
-
);
|
|
325
|
+
await host.writeAtomically(async () => {
|
|
326
|
+
await projcache.delete(E.inList(projcache.f_session_id, stale));
|
|
327
|
+
});
|
|
473
328
|
return stale.length;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
...(host.dbDirect === undefined
|
|
477
|
-
? {}
|
|
478
|
-
: {
|
|
479
|
-
readProjcacheDirect: (sessionId: string): StoredProjcacheEntry | undefined => {
|
|
480
|
-
const db = host.dbDirect!() as any;
|
|
481
|
-
const stored = db
|
|
482
|
-
.select()
|
|
483
|
-
.from(tProjcacheRows)
|
|
484
|
-
.where(eq(tProjcacheRows.fSessionId, sessionId))
|
|
485
|
-
.all() as ProjcacheRowRecord[];
|
|
486
|
-
if (stored.length === 0) return undefined;
|
|
487
|
-
const session = db
|
|
488
|
-
.select(sessionIdentity)
|
|
489
|
-
.from(tSessions)
|
|
490
|
-
.where(eq(tSessions.fSessionId, sessionId))
|
|
491
|
-
.get() as (SessionIdentityRow & { fSessionId: string }) | undefined;
|
|
329
|
+
}
|
|
492
330
|
|
|
493
|
-
if (session === undefined) return undefined;
|
|
494
|
-
const rows: ProjectionCheckpoint = {};
|
|
495
|
-
for (const row of stored) {
|
|
496
|
-
rows[row.fKey] = {
|
|
497
|
-
ver: row.fVer,
|
|
498
|
-
seq: seqCursor(row.fSeq),
|
|
499
|
-
val: JSON.parse(row.fVal),
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
return { sessionId: SessionId(sessionId), identity: identityOfSession(session), rows };
|
|
503
|
-
},
|
|
504
|
-
readSessionTitleDirect: (
|
|
505
|
-
sessionId: string,
|
|
506
|
-
): { title: string; seq: number } | undefined => {
|
|
507
|
-
const db = host.dbDirect!() as any;
|
|
508
|
-
const row = db
|
|
509
|
-
.select({ fTitle: tSessions.fTitle, fTitleSeq: tSessions.fTitleSeq })
|
|
510
|
-
.from(tSessions)
|
|
511
|
-
.where(eq(tSessions.fSessionId, sessionId))
|
|
512
|
-
.get() as { fTitle: string | null; fTitleSeq: number | null } | undefined;
|
|
513
|
-
if (row === undefined || row.fTitle === null || row.fTitleSeq === null)
|
|
514
|
-
return undefined;
|
|
515
|
-
return { title: row.fTitle, seq: row.fTitleSeq };
|
|
516
|
-
},
|
|
517
|
-
}),
|
|
518
331
|
};
|
|
519
332
|
}
|