dscs-server 0.1.3 → 0.1.5
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 +59 -17
- package/dist/admin-password.js +27 -0
- package/dist/audit.js +73 -0
- package/dist/db.js +174 -3
- package/dist/http.js +433 -85
- package/dist/index.js +23 -7
- package/dist/session-admin.js +214 -0
- package/dist/session-history.js +134 -0
- package/dist/sessions.js +133 -73
- package/dist/shared/blob-batch.js +4 -2
- package/dist/shared/wire.js +1 -0
- package/package.json +1 -1
- package/web/admin.html +1833 -297
package/dist/sessions.js
CHANGED
|
@@ -3,11 +3,13 @@ import { assertChain, buildManifest, manifestDigest, stableStringify } from './s
|
|
|
3
3
|
import { sha256 } from './shared/crypto.js';
|
|
4
4
|
import { SyncError } from './shared/wire.js';
|
|
5
5
|
import { blobPath } from './blobs.js';
|
|
6
|
-
import { DEFAULT_DATASET, bumpRevision, readAppendRequest,
|
|
6
|
+
import { DEFAULT_DATASET, blobRecord, bumpRevision, readAppendRequest, withTransaction, writeAppendRequest } from './db.js';
|
|
7
7
|
import { requireLease as requireResourceLease, SYNC_RESOURCE } from './leases.js';
|
|
8
|
+
import { recordSessionEvent } from './session-history.js';
|
|
8
9
|
export const MAX_APPEND_BODY_BYTES = 8 * 1024 * 1024;
|
|
9
10
|
const HASH = /^[0-9a-f]{64}$/u;
|
|
10
11
|
const KEY_ID = /^[0-9a-f]{16}$/u;
|
|
12
|
+
export const MAX_FRAME_ATTACHMENT_BLOBS = 256;
|
|
11
13
|
function assertRef(frame, expectedIndex) {
|
|
12
14
|
if (frame.index !== expectedIndex) throw new SyncError('bad-request', `帧下标应为 ${expectedIndex},实际是 ${frame.index}`);
|
|
13
15
|
if (!HASH.test(frame.chainHash) || !HASH.test(frame.blobSha256)) throw new SyncError('bad-request', `帧 ${expectedIndex} 的哈希非法`);
|
|
@@ -27,6 +29,27 @@ function assertRef(frame, expectedIndex) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
if (!Number.isSafeInteger(frame.seqFrom) || !Number.isSafeInteger(frame.seqTo)) throw new SyncError('bad-request', `帧 ${expectedIndex} 的 seq 非法`);
|
|
32
|
+
if (frame.attachmentIndexSha256 !== undefined && !HASH.test(frame.attachmentIndexSha256)) {
|
|
33
|
+
throw new SyncError('bad-request', `帧 ${expectedIndex} 的附件索引哈希非法`);
|
|
34
|
+
}
|
|
35
|
+
const attachments = frame.attachmentBlobs;
|
|
36
|
+
if (attachments !== undefined) {
|
|
37
|
+
if (!Array.isArray(attachments) || attachments.length < 1 || attachments.length > MAX_FRAME_ATTACHMENT_BLOBS) {
|
|
38
|
+
throw new SyncError('bad-request', `帧 ${expectedIndex} 的附件列表必须是 1–${MAX_FRAME_ATTACHMENT_BLOBS} 个哈希`);
|
|
39
|
+
}
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const sha of attachments){
|
|
42
|
+
if (typeof sha !== 'string' || !HASH.test(sha)) throw new SyncError('bad-request', `帧 ${expectedIndex} 的附件哈希非法`);
|
|
43
|
+
if (seen.has(sha)) throw new SyncError('bad-request', `帧 ${expectedIndex} 的附件列表重复:${sha}`);
|
|
44
|
+
seen.add(sha);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function requireStoredBlob(db, dataDir, sha256, kind, label) {
|
|
49
|
+
if (!existsSync(blobPath(dataDir, sha256))) throw new SyncError('bad-request', `${label} ${sha256} 尚未上传`);
|
|
50
|
+
const record = blobRecord(db, sha256);
|
|
51
|
+
if (record === undefined) throw new SyncError('bad-request', `${label} ${sha256} 没有登记`);
|
|
52
|
+
if (record.kind !== kind) throw new SyncError('bad-request', `${label} ${sha256} 的类别是 ${record.kind},应为 ${kind}`);
|
|
30
53
|
}
|
|
31
54
|
function toChain(frames) {
|
|
32
55
|
return frames.map((frame)=>({
|
|
@@ -37,7 +60,18 @@ function toChain(frames) {
|
|
|
37
60
|
}));
|
|
38
61
|
}
|
|
39
62
|
function sessionRow(db, sessionId) {
|
|
40
|
-
return db.handle.prepare('SELECT created_at, format_version, last_seq, frame_count, head_chain_hash, manifest_json, manifest_digest, revision, updated_at, deleted_at, compression, stored_bytes FROM sessions WHERE dataset = ? AND session_id = ?').get(DEFAULT_DATASET, sessionId);
|
|
63
|
+
return db.handle.prepare('SELECT created_at, format_version, last_seq, frame_count, head_chain_hash, manifest_json, manifest_digest, revision, updated_at, deleted_at, purged_at, compression, stored_bytes FROM sessions WHERE dataset = ? AND session_id = ?').get(DEFAULT_DATASET, sessionId);
|
|
64
|
+
}
|
|
65
|
+
function activeReset(db, sessionId, now) {
|
|
66
|
+
const row = db.handle.prepare('SELECT device_id, expires_at FROM session_resets WHERE dataset = ? AND session_id = ?').get(DEFAULT_DATASET, sessionId);
|
|
67
|
+
if (row === undefined) return undefined;
|
|
68
|
+
if (row.expires_at <= now) {
|
|
69
|
+
db.handle.prepare('DELETE FROM session_resets WHERE dataset = ? AND session_id = ?').run(DEFAULT_DATASET, sessionId);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
device_id: row.device_id
|
|
74
|
+
};
|
|
41
75
|
}
|
|
42
76
|
function manifestOf(sessionId, formatVersion, createdAt, updatedAt, frames, tailOpen, safeThroughSeq, workspaceKey, parentSession, origin, delegationDepth) {
|
|
43
77
|
assertChain(toChain(frames));
|
|
@@ -105,6 +139,10 @@ function appendRequestHash(input) {
|
|
|
105
139
|
}
|
|
106
140
|
export function appendFrames(db, dataDir, input, now = Date.now()) {
|
|
107
141
|
return withTransaction(db, ()=>{
|
|
142
|
+
const reset = activeReset(db, input.sessionId, now);
|
|
143
|
+
if (reset !== undefined && reset.device_id !== null && reset.device_id !== input.deviceId) {
|
|
144
|
+
throw new SyncError('reset-pending', '该会话正在等待指定设备重建');
|
|
145
|
+
}
|
|
108
146
|
const idempotencyKey = input.idempotencyKey;
|
|
109
147
|
const requestHash = idempotencyKey === undefined ? undefined : appendRequestHash(input);
|
|
110
148
|
if (idempotencyKey !== undefined && requestHash !== undefined) {
|
|
@@ -125,6 +163,9 @@ export function appendFrames(db, dataDir, input, now = Date.now()) {
|
|
|
125
163
|
let parentSession = input.parentSession;
|
|
126
164
|
let origin = input.origin;
|
|
127
165
|
let delegationDepth = input.delegationDepth;
|
|
166
|
+
if (row !== undefined && row.purged_at !== null) {
|
|
167
|
+
throw new SyncError('gone', `会话 ${input.sessionId} 已移除并释放空间,需要由管理台重置后才能重建`);
|
|
168
|
+
}
|
|
128
169
|
if (row !== undefined && row.deleted_at !== null && row.created_at === input.createdAt) {
|
|
129
170
|
throw new SyncError('gone', `会话 ${input.sessionId} 的这个生命周期已被删除`);
|
|
130
171
|
}
|
|
@@ -180,16 +221,24 @@ export function appendFrames(db, dataDir, input, now = Date.now()) {
|
|
|
180
221
|
if (!existsSync(blobPath(dataDir, frame.blobSha256))) throw new SyncError('bad-request', `帧 ${frame.index} 的 blob 尚未上传`);
|
|
181
222
|
}
|
|
182
223
|
input.frames.forEach((frame, offset)=>assertRef(frame, (row === undefined || replacingLifecycle ? 0 : input.baseIndex + 1) + offset));
|
|
224
|
+
for (const frame of input.frames){
|
|
225
|
+
if (frame.attachmentIndexSha256 !== undefined) {
|
|
226
|
+
requireStoredBlob(db, dataDir, frame.attachmentIndexSha256, 'attachment-index', `帧 ${frame.index} 的附件索引`);
|
|
227
|
+
}
|
|
228
|
+
for (const sha of frame.attachmentBlobs ?? []){
|
|
229
|
+
requireStoredBlob(db, dataDir, sha, 'attachment', `帧 ${frame.index} 的附件`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
183
232
|
const { manifest, digest, headChainHash } = manifestOf(input.sessionId, formatVersion, createdAt, now, combined, input.tailOpen, input.safeThroughSeq, workspaceKey, parentSession, origin, delegationDepth);
|
|
184
233
|
const revision = bumpRevision(db);
|
|
185
234
|
const stats = statsOf(combined);
|
|
186
|
-
db.handle.prepare(`INSERT INTO sessions (dataset, session_id, created_at, format_version, last_seq, frame_count, head_chain_hash, manifest_json, manifest_digest, revision, updated_at, deleted_at, compression, stored_bytes)
|
|
187
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
235
|
+
db.handle.prepare(`INSERT INTO sessions (dataset, session_id, created_at, format_version, last_seq, frame_count, head_chain_hash, manifest_json, manifest_digest, revision, updated_at, deleted_at, purged_at, compression, stored_bytes)
|
|
236
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)
|
|
188
237
|
ON CONFLICT(dataset, session_id) DO UPDATE SET
|
|
189
238
|
created_at = excluded.created_at, format_version = excluded.format_version, last_seq = excluded.last_seq,
|
|
190
239
|
frame_count = excluded.frame_count, head_chain_hash = excluded.head_chain_hash,
|
|
191
240
|
manifest_json = excluded.manifest_json, manifest_digest = excluded.manifest_digest,
|
|
192
|
-
revision = excluded.revision, updated_at = excluded.updated_at, deleted_at = NULL,
|
|
241
|
+
revision = excluded.revision, updated_at = excluded.updated_at, deleted_at = NULL, purged_at = NULL,
|
|
193
242
|
compression = excluded.compression, stored_bytes = excluded.stored_bytes`).run(DEFAULT_DATASET, input.sessionId, createdAt, formatVersion, manifest.lastSeq, combined.length, headChainHash, JSON.stringify(manifest), digest, revision, now, stats.compression, stats.storedBytes);
|
|
194
243
|
const summary = {
|
|
195
244
|
sessionId: input.sessionId,
|
|
@@ -206,6 +255,37 @@ export function appendFrames(db, dataDir, input, now = Date.now()) {
|
|
|
206
255
|
if (idempotencyKey !== undefined && requestHash !== undefined) {
|
|
207
256
|
writeAppendRequest(db, input.deviceId, input.sessionId, idempotencyKey, requestHash, JSON.stringify(summary), now);
|
|
208
257
|
}
|
|
258
|
+
if (reset !== undefined) {
|
|
259
|
+
db.handle.prepare('DELETE FROM session_resets WHERE dataset = ? AND session_id = ?').run(DEFAULT_DATASET, input.sessionId);
|
|
260
|
+
}
|
|
261
|
+
const created = row === undefined || replacingLifecycle;
|
|
262
|
+
const oldSeq = created ? 0 : row.last_seq;
|
|
263
|
+
const oldBytes = created ? 0 : row.stored_bytes;
|
|
264
|
+
const tailReplaced = !created && input.baseIndex < row.frame_count - 1;
|
|
265
|
+
recordSessionEvent(db, {
|
|
266
|
+
sessionId: input.sessionId,
|
|
267
|
+
at: now,
|
|
268
|
+
kind: created ? 'create' : 'append',
|
|
269
|
+
deviceId: input.deviceId,
|
|
270
|
+
actorKind: 'device',
|
|
271
|
+
actorId: input.deviceId,
|
|
272
|
+
events: tailReplaced ? 0 : Math.max(0, summary.lastSeq - oldSeq),
|
|
273
|
+
bytes: Math.max(0, summary.storedBytes - oldBytes),
|
|
274
|
+
seqFrom: input.frames[0]?.seqFrom ?? null,
|
|
275
|
+
seqTo: input.frames[input.frames.length - 1]?.seqTo ?? null,
|
|
276
|
+
batchCount: input.frames.length,
|
|
277
|
+
lastSeq: summary.lastSeq,
|
|
278
|
+
storedBytes: summary.storedBytes,
|
|
279
|
+
detail: {
|
|
280
|
+
createdAt,
|
|
281
|
+
...tailReplaced ? {
|
|
282
|
+
tailReplaced: true
|
|
283
|
+
} : {},
|
|
284
|
+
...workspaceKey === undefined ? {} : {
|
|
285
|
+
workspaceKey
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
209
289
|
return summary;
|
|
210
290
|
});
|
|
211
291
|
}
|
|
@@ -219,6 +299,19 @@ export function deleteSession(db, sessionId, deviceId, clientId, createdAt, now
|
|
|
219
299
|
const revision = bumpRevision(db);
|
|
220
300
|
db.handle.prepare('UPDATE sessions SET deleted_at = ?, revision = ?, updated_at = ? WHERE dataset = ? AND session_id = ?').run(now, revision, now, DEFAULT_DATASET, sessionId);
|
|
221
301
|
db.handle.prepare('INSERT OR REPLACE INTO tombstones (dataset, session_id, created_at, deleted_at, deleted_by) VALUES (?, ?, ?, ?, ?)').run(DEFAULT_DATASET, sessionId, createdAt, now, deviceId);
|
|
302
|
+
recordSessionEvent(db, {
|
|
303
|
+
sessionId,
|
|
304
|
+
at: now,
|
|
305
|
+
kind: 'device-delete',
|
|
306
|
+
deviceId,
|
|
307
|
+
actorKind: 'device',
|
|
308
|
+
actorId: deviceId,
|
|
309
|
+
lastSeq: row.last_seq,
|
|
310
|
+
storedBytes: row.stored_bytes,
|
|
311
|
+
detail: {
|
|
312
|
+
createdAt
|
|
313
|
+
}
|
|
314
|
+
});
|
|
222
315
|
return {
|
|
223
316
|
sessionId,
|
|
224
317
|
revision,
|
|
@@ -253,7 +346,10 @@ export function listBatches(db, sessionId, from, limit) {
|
|
|
253
346
|
}
|
|
254
347
|
const V2_SUMMARY_SELECT = `s.session_id, s.created_at, s.format_version, s.last_seq, s.frame_count,
|
|
255
348
|
s.head_chain_hash, s.manifest_digest, s.revision, s.updated_at, s.deleted_at,
|
|
256
|
-
s.compression, s.stored_bytes,
|
|
349
|
+
s.purged_at, s.compression, s.stored_bytes,
|
|
350
|
+
(SELECT t.deleted_by FROM tombstones t
|
|
351
|
+
WHERE t.dataset = s.dataset AND t.session_id = s.session_id AND t.created_at = s.created_at
|
|
352
|
+
LIMIT 1) AS deleted_by,
|
|
257
353
|
json_extract(s.manifest_json, '$.workspaceKey') AS workspace_key,
|
|
258
354
|
json_extract(s.manifest_json, '$.tailOpen') AS tail_open,
|
|
259
355
|
json_extract(s.manifest_json, '$.safeThroughSeq') AS safe_through_seq,
|
|
@@ -279,15 +375,17 @@ function mapV2Summary(row) {
|
|
|
279
375
|
delegationDepth: typeof row.delegation_depth === 'number' && Number.isSafeInteger(row.delegation_depth) ? row.delegation_depth : null,
|
|
280
376
|
updatedAt: row.updated_at,
|
|
281
377
|
deletedAt: row.deleted_at,
|
|
378
|
+
deletedBy: row.deleted_by,
|
|
379
|
+
purgedAt: row.purged_at,
|
|
282
380
|
compression: compressionOf(row.compression),
|
|
283
381
|
storedBytes: Number.isSafeInteger(row.stored_bytes) ? row.stored_bytes : 0
|
|
284
382
|
};
|
|
285
383
|
}
|
|
286
|
-
export function listV2Sessions(db, after, limit) {
|
|
384
|
+
export function listV2Sessions(db, after, limit, includeDeleted = false) {
|
|
287
385
|
const rows = db.handle.prepare(`SELECT ${V2_SUMMARY_SELECT}
|
|
288
386
|
FROM sessions s
|
|
289
|
-
WHERE s.dataset = ? AND s.deleted_at IS NULL AND s.session_id > ?
|
|
290
|
-
ORDER BY s.session_id ASC LIMIT ?`).all(DEFAULT_DATASET, after ?? '', limit + 1);
|
|
387
|
+
WHERE s.dataset = ? AND (? = 1 OR s.deleted_at IS NULL) AND s.session_id > ?
|
|
388
|
+
ORDER BY s.session_id ASC LIMIT ?`).all(DEFAULT_DATASET, includeDeleted ? 1 : 0, after ?? '', limit + 1);
|
|
291
389
|
const visible = rows.slice(0, limit);
|
|
292
390
|
const last = visible[visible.length - 1];
|
|
293
391
|
return {
|
|
@@ -295,40 +393,6 @@ export function listV2Sessions(db, after, limit) {
|
|
|
295
393
|
nextSessionId: rows.length > limit && last !== undefined ? last.session_id : null
|
|
296
394
|
};
|
|
297
395
|
}
|
|
298
|
-
export function listV2Changes(db, sinceRevision, sinceTombstone, limit) {
|
|
299
|
-
const highWater = Math.max(sinceRevision, readRevision(db));
|
|
300
|
-
const rows = db.handle.prepare(`SELECT ${V2_SUMMARY_SELECT}
|
|
301
|
-
FROM sessions s
|
|
302
|
-
WHERE s.dataset = ? AND s.deleted_at IS NULL AND s.revision > ? AND s.revision <= ?
|
|
303
|
-
ORDER BY s.revision ASC LIMIT ?`).all(DEFAULT_DATASET, sinceRevision, highWater, limit + 1);
|
|
304
|
-
const tombstoneRows = db.handle.prepare(`SELECT session_id, created_at, deleted_at FROM tombstones
|
|
305
|
-
WHERE dataset = ? AND (
|
|
306
|
-
deleted_at > ? OR
|
|
307
|
-
(deleted_at = ? AND session_id > ?) OR
|
|
308
|
-
(deleted_at = ? AND session_id = ? AND created_at > ?)
|
|
309
|
-
)
|
|
310
|
-
ORDER BY deleted_at ASC, session_id ASC, created_at ASC LIMIT ?`).all(DEFAULT_DATASET, sinceTombstone.deletedAt, sinceTombstone.deletedAt, sinceTombstone.sessionId, sinceTombstone.deletedAt, sinceTombstone.sessionId, sinceTombstone.createdAt, limit + 1);
|
|
311
|
-
const visible = rows.slice(0, limit);
|
|
312
|
-
const visibleTombstones = tombstoneRows.slice(0, limit);
|
|
313
|
-
const last = visible[visible.length - 1];
|
|
314
|
-
const lastTombstone = visibleTombstones[visibleTombstones.length - 1];
|
|
315
|
-
const hasMore = rows.length > limit || tombstoneRows.length > limit;
|
|
316
|
-
return {
|
|
317
|
-
sessions: visible.map(mapV2Summary),
|
|
318
|
-
tombstones: visibleTombstones.map((row)=>({
|
|
319
|
-
sessionId: row.session_id,
|
|
320
|
-
createdAt: row.created_at,
|
|
321
|
-
deletedAt: row.deleted_at
|
|
322
|
-
})),
|
|
323
|
-
nextRevision: rows.length > limit && last !== undefined ? last.revision : highWater,
|
|
324
|
-
nextTombstone: lastTombstone === undefined ? sinceTombstone : {
|
|
325
|
-
deletedAt: lastTombstone.deleted_at,
|
|
326
|
-
sessionId: lastTombstone.session_id,
|
|
327
|
-
createdAt: lastTombstone.created_at
|
|
328
|
-
},
|
|
329
|
-
hasMore
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
396
|
export function getV2SessionSummary(db, sessionId) {
|
|
333
397
|
const row = db.handle.prepare(`SELECT ${V2_SUMMARY_SELECT}
|
|
334
398
|
FROM sessions s
|
|
@@ -347,11 +411,26 @@ export function listV2Batches(db, sessionId, from, limit) {
|
|
|
347
411
|
};
|
|
348
412
|
}
|
|
349
413
|
export function adminSessions(db, limit = 500) {
|
|
350
|
-
const rows = db.handle.prepare(`SELECT
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
414
|
+
const rows = db.handle.prepare(`SELECT * FROM (
|
|
415
|
+
SELECT s.session_id, s.created_at, s.format_version, s.frame_count, s.last_seq, s.head_chain_hash,
|
|
416
|
+
s.revision, s.updated_at, s.deleted_at, s.purged_at, s.stored_bytes,
|
|
417
|
+
t.deleted_by, r.device_id AS reset_device_id, r.expires_at AS reset_expires_at, r.reset_at,
|
|
418
|
+
COALESCE(json_extract(s.manifest_json, '$.workspaceKey'), r.workspace_key) AS workspace_key
|
|
419
|
+
FROM sessions s
|
|
420
|
+
LEFT JOIN tombstones t
|
|
421
|
+
ON t.dataset = s.dataset AND t.session_id = s.session_id AND t.created_at = s.created_at
|
|
422
|
+
LEFT JOIN session_resets r
|
|
423
|
+
ON r.dataset = s.dataset AND r.session_id = s.session_id
|
|
424
|
+
WHERE s.dataset = ?
|
|
425
|
+
UNION ALL
|
|
426
|
+
SELECT r.session_id, r.created_at, 0, 0, 0, '', 0, r.reset_at, NULL, NULL, 0,
|
|
427
|
+
NULL, r.device_id, r.expires_at, r.reset_at, r.workspace_key
|
|
428
|
+
FROM session_resets r
|
|
429
|
+
WHERE r.dataset = ? AND NOT EXISTS (
|
|
430
|
+
SELECT 1 FROM sessions s WHERE s.dataset = r.dataset AND s.session_id = r.session_id
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
ORDER BY updated_at DESC LIMIT ?`).all(DEFAULT_DATASET, DEFAULT_DATASET, limit);
|
|
355
434
|
return rows.map((row)=>({
|
|
356
435
|
sessionId: row.session_id,
|
|
357
436
|
createdAt: row.created_at,
|
|
@@ -362,6 +441,12 @@ export function adminSessions(db, limit = 500) {
|
|
|
362
441
|
revision: row.revision,
|
|
363
442
|
updatedAt: row.updated_at,
|
|
364
443
|
deletedAt: row.deleted_at,
|
|
444
|
+
deletedBy: row.deleted_by,
|
|
445
|
+
purgedAt: row.purged_at,
|
|
446
|
+
resetAt: row.reset_at,
|
|
447
|
+
resetDeviceId: row.reset_device_id,
|
|
448
|
+
resetExpiresAt: row.reset_expires_at,
|
|
449
|
+
storedBytes: row.stored_bytes,
|
|
365
450
|
workspaceKey: row.workspace_key === null ? null : String(row.workspace_key)
|
|
366
451
|
}));
|
|
367
452
|
}
|
|
@@ -372,31 +457,6 @@ export function sessionCounts(db) {
|
|
|
372
457
|
deleted: Number(row?.deleted ?? 0)
|
|
373
458
|
};
|
|
374
459
|
}
|
|
375
|
-
export function listChanges(db, since, limit) {
|
|
376
|
-
const rows = db.handle.prepare('SELECT session_id, created_at, format_version, frame_count, head_chain_hash, manifest_digest, revision, updated_at, deleted_at FROM sessions WHERE dataset = ? AND revision > ? ORDER BY revision ASC LIMIT ?').all(DEFAULT_DATASET, since, limit + 1);
|
|
377
|
-
const tombstones = db.handle.prepare('SELECT session_id, created_at, deleted_at FROM tombstones WHERE dataset = ? AND deleted_at > ? ORDER BY deleted_at ASC LIMIT ?').all(DEFAULT_DATASET, since, limit + 1);
|
|
378
|
-
const sessions = rows.slice(0, limit).map((row)=>({
|
|
379
|
-
sessionId: row.session_id,
|
|
380
|
-
revision: row.revision,
|
|
381
|
-
formatVersion: row.format_version,
|
|
382
|
-
createdAt: row.created_at,
|
|
383
|
-
batchCount: row.frame_count,
|
|
384
|
-
headChainHash: row.head_chain_hash,
|
|
385
|
-
manifestDigest: row.manifest_digest,
|
|
386
|
-
updatedAt: row.updated_at,
|
|
387
|
-
deletedAt: row.deleted_at
|
|
388
|
-
}));
|
|
389
|
-
return {
|
|
390
|
-
revision: readRevision(db),
|
|
391
|
-
sessions,
|
|
392
|
-
tombstones: tombstones.slice(0, limit).map((row)=>({
|
|
393
|
-
sessionId: row.session_id,
|
|
394
|
-
createdAt: row.created_at,
|
|
395
|
-
deletedAt: row.deleted_at
|
|
396
|
-
})),
|
|
397
|
-
hasMore: rows.length > limit || tombstones.length > limit
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
460
|
|
|
401
461
|
|
|
402
462
|
//# sourceURL=sessions.ts
|
|
@@ -10,12 +10,14 @@ const VERSION = 1;
|
|
|
10
10
|
const KIND_TO_CODE = {
|
|
11
11
|
frame: 1,
|
|
12
12
|
attachment: 2,
|
|
13
|
-
vault: 3
|
|
13
|
+
vault: 3,
|
|
14
|
+
'attachment-index': 4
|
|
14
15
|
};
|
|
15
16
|
const CODE_TO_KIND = {
|
|
16
17
|
1: 'frame',
|
|
17
18
|
2: 'attachment',
|
|
18
|
-
3: 'vault'
|
|
19
|
+
3: 'vault',
|
|
20
|
+
4: 'attachment-index'
|
|
19
21
|
};
|
|
20
22
|
export function encodeBlobBatch(entries) {
|
|
21
23
|
if (entries.length < 1 || entries.length > MAX_BATCH_BLOBS) {
|
package/dist/shared/wire.js
CHANGED