@x1a0f3n9/dsh-session-persistence-jsonl 0.1.5-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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +170 -0
- package/README.zh.md +170 -0
- package/lib/index.js +3726 -0
- package/lib/types/format.d.ts +209 -0
- package/lib/types/generation.d.ts +163 -0
- package/lib/types/history-suffix.d.ts +16 -0
- package/lib/types/index.d.ts +304 -0
- package/lib/types/lease.d.ts +57 -0
- package/lib/types/migration-verifier.d.ts +15 -0
- package/lib/types/storage.d.ts +263 -0
- package/lib/types/testing/generation.d.ts +8 -0
- package/lib/types/win32.d.ts +48 -0
- package/lib/types/worker.d.ts +3 -0
- package/lib/types/zstd-private-decoder.d.ts +33 -0
- package/lib/types/zstd-public-decoder.d.ts +15 -0
- package/lib/types/zstd.d.ts +68 -0
- package/lib/worker.cjs +1246 -0
- package/package.json +50 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL durable session-persistence backend. It stores a header and contiguous
|
|
3
|
+
* events in immutable historical generation files under one directory per session.
|
|
4
|
+
* Normal writes append to the current generation; explicit destructive deletion
|
|
5
|
+
* rewrites that current generation's retained prefix. It serves the handle-based
|
|
6
|
+
* `SessionPersistence` API: `create`/`open` return per-session handles, and
|
|
7
|
+
* every read validates the same fail-closed storage contract.
|
|
8
|
+
* @module @x1a0f3n9/dsh-session-persistence-jsonl
|
|
9
|
+
*/
|
|
10
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
import { SessionPersistence, type SessionAccess, type SessionHandle, type SessionHandleReadResult, type SessionPersistenceCreateOptions, type SessionPersistenceListOptions, type SessionPersistenceOpenOptions, type SessionPersistenceSnapshot, type SessionPersistenceStatOptions, type SessionPersistenceRevision as PersistenceRevision, type SessionHistorySuffix, type SessionHistorySuffixOptions } from '@x1a0f3n9/dsh-session-persistence';
|
|
13
|
+
import { JsonlSessionHandle } from './storage.ts';
|
|
14
|
+
import { SessionWriteLease } from './lease.ts';
|
|
15
|
+
import type { SessionEvent, SessionId, SessionHeader, SessionLogOffset as SessionLogOffsetType } from '@x1a0f3n9/dsh-session';
|
|
16
|
+
import { type JsonlCompression } from './format.ts';
|
|
17
|
+
export type { JsonlCompression } from './format.ts';
|
|
18
|
+
/** Loader schema for the JSONL artifact's physical encoding. */
|
|
19
|
+
export declare const JsonlCompressionSchema: z<JsonlCompression>;
|
|
20
|
+
/** Plugin config for the JSONL backend's root and physical encoding. */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/**
|
|
23
|
+
* Root directory for all session files. Required (no default): a default of
|
|
24
|
+
* `process.cwd()` would scatter session files as the process's cwd changes
|
|
25
|
+
* (bash calls, subprocesses). Sessions group under human-readable project
|
|
26
|
+
* directories, then per-session directories. An existing root must be a
|
|
27
|
+
* readable directory; an absent root is created on first materialization.
|
|
28
|
+
*/
|
|
29
|
+
root: string;
|
|
30
|
+
/** Physical encoding; defaults to checksummed Zstandard frames. */
|
|
31
|
+
compression?: JsonlCompression;
|
|
32
|
+
}
|
|
33
|
+
/** One stored event graph whose producer has established immutable sharing. */
|
|
34
|
+
interface FrozenStoredEvents extends SessionHandleReadResult {
|
|
35
|
+
readonly eventState: 'shared-frozen';
|
|
36
|
+
}
|
|
37
|
+
/** State shared by prepared historical and published current logs. */
|
|
38
|
+
interface StoredLogBase extends FrozenStoredEvents {
|
|
39
|
+
readonly meta: SessionHeader;
|
|
40
|
+
readonly tornTruncateTo: number | undefined;
|
|
41
|
+
/** Complete events recovered from the torn final frame; the write path rewrites them durably. */
|
|
42
|
+
readonly recoveredTail: SessionEvent[];
|
|
43
|
+
/** Exact fork-inherited prefix length stored in the header line. */
|
|
44
|
+
readonly inheritedEventCount: SessionLogOffsetType;
|
|
45
|
+
readonly revision: PersistenceRevision;
|
|
46
|
+
}
|
|
47
|
+
/** A decoded current generation that is already durable. */
|
|
48
|
+
interface CurrentStoredLog extends StoredLogBase {
|
|
49
|
+
readonly status: 'current';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The JSONL persistence backend. Load as a plugin; it registers as
|
|
53
|
+
* `ctx.sessionPersistence`. Sessions materialize lazily: a created session is
|
|
54
|
+
* visible to this process immediately, reaches disk on its first append or
|
|
55
|
+
* flush, and never existed if the process crashes before that.
|
|
56
|
+
*/
|
|
57
|
+
declare class JsonlSessionPersistence extends SessionPersistence {
|
|
58
|
+
config: Config;
|
|
59
|
+
static Config: z<Config>;
|
|
60
|
+
/** Backend label for diagnostics and effects; shadows `Service.name` without changing the service key. */
|
|
61
|
+
readonly name = "session-persistence-jsonl";
|
|
62
|
+
private root;
|
|
63
|
+
private compression;
|
|
64
|
+
private rootEncodingCheck;
|
|
65
|
+
private readonly tracker;
|
|
66
|
+
private readonly generationFormat;
|
|
67
|
+
/**
|
|
68
|
+
* Bounded LRU of parsed, validated stored logs keyed by session id and
|
|
69
|
+
* guarded by the stat-derived revision, so an immediate cold-read handoff
|
|
70
|
+
* (observation then resume) parses the artifact once. Every local mutation
|
|
71
|
+
* for an id invalidates its entry; a foreign write misses through the
|
|
72
|
+
* revision guard.
|
|
73
|
+
*/
|
|
74
|
+
private readonly coldLogMemo;
|
|
75
|
+
/** One joinable decode/migration operation per selected historical Session file revision. */
|
|
76
|
+
private readonly migrationPreparations;
|
|
77
|
+
constructor(ctx: Context, config: Config);
|
|
78
|
+
/**
|
|
79
|
+
* Refusal-diagnostics hook: the absolute target path, without touching the filesystem.
|
|
80
|
+
* @param meta - the stored header naming the session and its cwd.
|
|
81
|
+
* @returns the artifact kind and absolute path.
|
|
82
|
+
*/
|
|
83
|
+
private locate;
|
|
84
|
+
/**
|
|
85
|
+
* Create a new stored session and take its write ownership. The session is
|
|
86
|
+
* visible to this process immediately; the physical artifact appears on the
|
|
87
|
+
* first append or flush.
|
|
88
|
+
* @param header - the immutable header to store; must be losslessly
|
|
89
|
+
* JSON-serializable with a non-negative safe-integer `createdAt`.
|
|
90
|
+
* @param options - optional cancellation.
|
|
91
|
+
* @returns the owned write handle.
|
|
92
|
+
*/
|
|
93
|
+
create(header: SessionHeader, options?: SessionPersistenceCreateOptions): Promise<SessionHandle>;
|
|
94
|
+
/**
|
|
95
|
+
* Open an existing stored session for `read` or single-writer `write`.
|
|
96
|
+
* @param id - the stored session to open.
|
|
97
|
+
* @param access - `read` (no ownership) or `write` (atomic in-process claim).
|
|
98
|
+
* @param options - optional cancellation.
|
|
99
|
+
* @returns the open handle.
|
|
100
|
+
*/
|
|
101
|
+
open(id: SessionId, access: SessionAccess, options?: SessionPersistenceOpenOptions): Promise<SessionHandle>;
|
|
102
|
+
/**
|
|
103
|
+
* Flush every active write handle in one durability barrier; see the seam
|
|
104
|
+
* contract.
|
|
105
|
+
* @returns resolution once every write handle active at the call has flushed.
|
|
106
|
+
*/
|
|
107
|
+
flush(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Read a tail page of one current-generation JSONL log without restoring
|
|
110
|
+
* the whole artifact. Historical generations return `undefined` so the
|
|
111
|
+
* caller can migrate through a full observation.
|
|
112
|
+
* @param id - the stored session to read.
|
|
113
|
+
* @param options - page bounds and cancellation.
|
|
114
|
+
* @returns the covering suffix, or `undefined` when the selected generation
|
|
115
|
+
* is older than this build.
|
|
116
|
+
*/
|
|
117
|
+
readHistorySuffix(id: SessionId, options: SessionHistorySuffixOptions): Promise<SessionHistorySuffix | undefined>;
|
|
118
|
+
/**
|
|
119
|
+
* Observe one stored session without reading its event log.
|
|
120
|
+
* @param id - the stored session to observe.
|
|
121
|
+
* @param options - optional cancellation.
|
|
122
|
+
* @returns the snapshot (`sizeBytes` carries the physical artifact size), or
|
|
123
|
+
* `undefined` when the session does not exist.
|
|
124
|
+
*/
|
|
125
|
+
stat(id: SessionId, options?: SessionPersistenceStatOptions): Promise<SessionPersistenceSnapshot | undefined>;
|
|
126
|
+
/**
|
|
127
|
+
* List every stored session visible to this process: materialized artifacts
|
|
128
|
+
* plus this process's created-but-unmaterialized sessions.
|
|
129
|
+
* @param options - optional cancellation.
|
|
130
|
+
* @returns one snapshot per session, in no promised order.
|
|
131
|
+
*/
|
|
132
|
+
list(options?: SessionPersistenceListOptions): Promise<readonly SessionPersistenceSnapshot[]>;
|
|
133
|
+
/** Resolve and read one stored log, refusing loudly when the artifact is absent. */
|
|
134
|
+
private requireStoredLog;
|
|
135
|
+
/** Probe the memo and otherwise decode one historical generation under backend cancellation. */
|
|
136
|
+
private loadStoredMigration;
|
|
137
|
+
/** Await shared preparation for one caller and abort it only after its last waiter leaves. */
|
|
138
|
+
private waitForPreparation;
|
|
139
|
+
/** Decode one historical generation without publishing a successor. */
|
|
140
|
+
private prepareStoredMigration;
|
|
141
|
+
/** Publish a prepared historical log before granting write access. */
|
|
142
|
+
private publishStoredMigration;
|
|
143
|
+
/** Translate generation-layer failures into the persistence seam's error vocabulary. */
|
|
144
|
+
private generationFailure;
|
|
145
|
+
/**
|
|
146
|
+
* Read, parse, and validate one stored log as the current logical prefix.
|
|
147
|
+
* @param path - the artifact file to read.
|
|
148
|
+
* @param expectedId - the session identity the artifact must carry.
|
|
149
|
+
* @param signal - optional cancellation for the stat/read/decode work.
|
|
150
|
+
* @returns the validated stored log with any torn-tail truncation point.
|
|
151
|
+
*/
|
|
152
|
+
readStoredLog(path: string, expectedId: SessionId, signal?: AbortSignal): Promise<CurrentStoredLog>;
|
|
153
|
+
/** Decode and memoize one already-stable current physical snapshot. */
|
|
154
|
+
private decodeStoredLog;
|
|
155
|
+
/** Insert one parsed log into the bounded handoff cache. */
|
|
156
|
+
private memoizeStoredLog;
|
|
157
|
+
/**
|
|
158
|
+
* Resolve a session's current-generation log path.
|
|
159
|
+
* @param id - the stored session to locate.
|
|
160
|
+
* @param signal - optional cancellation for the directory scans.
|
|
161
|
+
* @returns the current artifact path, or `undefined` while only a historical generation exists.
|
|
162
|
+
*/
|
|
163
|
+
resolveCurrentLog(id: SessionId, signal?: AbortSignal): Promise<string | undefined>;
|
|
164
|
+
/**
|
|
165
|
+
* Rewrite one active session's current generation with an earlier event prefix.
|
|
166
|
+
* Historical format generations are left in place.
|
|
167
|
+
* @param id - the session whose write handle is active in this backend.
|
|
168
|
+
* @param length - number of events to retain.
|
|
169
|
+
* @returns resolution after the durable rewrite completes.
|
|
170
|
+
*/
|
|
171
|
+
truncate(id: SessionId, length: SessionLogOffsetType): Promise<void>;
|
|
172
|
+
/**
|
|
173
|
+
* Durably append one validated batch; lazily materializes on the first write.
|
|
174
|
+
* @param header - the session's stored header.
|
|
175
|
+
* @param events - the validated contiguous batch, in seq order.
|
|
176
|
+
* @param isMaterialized - whether the session already has a durable artifact.
|
|
177
|
+
* @param inheritedEventCount - the exact fork-inherited prefix length written into a materializing header line.
|
|
178
|
+
*/
|
|
179
|
+
persistBatch(header: SessionHeader, events: readonly SessionEvent[], isMaterialized: boolean, inheritedEventCount: SessionLogOffsetType): Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Materialize a header-only artifact for an explicitly durable empty session.
|
|
182
|
+
* @param header - the session's stored header.
|
|
183
|
+
* @param inheritedEventCount - the exact fork-inherited prefix length written into the header line.
|
|
184
|
+
*/
|
|
185
|
+
persistHeader(header: SessionHeader, inheritedEventCount: SessionLogOffsetType): Promise<void>;
|
|
186
|
+
/**
|
|
187
|
+
* Truncate a torn physical tail durably before this session's first new append.
|
|
188
|
+
* @param header - the session's stored header.
|
|
189
|
+
* @param truncateTo - the byte offset the artifact is truncated to.
|
|
190
|
+
*/
|
|
191
|
+
truncateTornTail(header: SessionHeader, truncateTo: number): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Whether this process still tracks a created-but-unmaterialized session.
|
|
194
|
+
* @param id - the session to test.
|
|
195
|
+
* @returns true while the pending entry exists.
|
|
196
|
+
*/
|
|
197
|
+
hasPendingSession(id: SessionId): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Release one handle's backend bookkeeping on close.
|
|
200
|
+
* @param handle - the closing handle.
|
|
201
|
+
* @param materialized - whether the session reached durable storage.
|
|
202
|
+
*/
|
|
203
|
+
releaseHandle(handle: JsonlSessionHandle, materialized: boolean): void;
|
|
204
|
+
/**
|
|
205
|
+
* Acquire the session directory's kernel write lock; the kernel holds it
|
|
206
|
+
* until the handle's close releases the descriptor, including on process death.
|
|
207
|
+
* @param id - the session the lock guards.
|
|
208
|
+
* @param cwd - header cwd used to derive the directory for a fresh session.
|
|
209
|
+
* @param dir - the resolved directory of an existing artifact, when known.
|
|
210
|
+
* @returns the held lock.
|
|
211
|
+
*/
|
|
212
|
+
private acquireLease;
|
|
213
|
+
/**
|
|
214
|
+
* Acquire the cross-process write lock for a materializing created session,
|
|
215
|
+
* called by its handle immediately before the first log bytes publish.
|
|
216
|
+
* @param header - the session's stored header (its cwd derives the directory).
|
|
217
|
+
* @returns the held lock.
|
|
218
|
+
*/
|
|
219
|
+
acquireWriteLease(header: SessionHeader): Promise<SessionWriteLease>;
|
|
220
|
+
/** Decode complete frames and retain complete JSONL records from a torn final frame. */
|
|
221
|
+
private readZstdPrefix;
|
|
222
|
+
private listArtifacts;
|
|
223
|
+
/** Read and translate one selected generation header without inspecting its body. */
|
|
224
|
+
private readGenerationHeader;
|
|
225
|
+
/** Convert format-catalog string identities to current branded Session metadata. */
|
|
226
|
+
private currentHeader;
|
|
227
|
+
/**
|
|
228
|
+
* Rewrite the current generation with a contiguous event prefix.
|
|
229
|
+
* Predecessor format generations are not moved, overwritten, or deleted.
|
|
230
|
+
* @param header - immutable session metadata stored in the header line.
|
|
231
|
+
* @param inheritedEventCount - exact fork-inherited prefix count.
|
|
232
|
+
* @param events - complete retained event prefix.
|
|
233
|
+
* @returns resolution after the replacement is durable.
|
|
234
|
+
*/
|
|
235
|
+
rewrite(header: SessionHeader, inheritedEventCount: SessionLogOffsetType, events: readonly SessionEvent[]): Promise<void>;
|
|
236
|
+
/** Atomically write the header line + first batch (temp-write, fsync, publish). */
|
|
237
|
+
private materialize;
|
|
238
|
+
private materializePosix;
|
|
239
|
+
private materializeWin32;
|
|
240
|
+
private rejectExistingLog;
|
|
241
|
+
private writeSyncedTempFile;
|
|
242
|
+
/** Encode the header and first batch without combining their frame boundaries. */
|
|
243
|
+
private encodeMaterialization;
|
|
244
|
+
/** Encode one durable append batch in the configured physical representation. */
|
|
245
|
+
private encodeEventBatch;
|
|
246
|
+
/** fsync a POSIX directory so a just-created/renamed entry is crash-durable. */
|
|
247
|
+
private syncDirPosix;
|
|
248
|
+
/**
|
|
249
|
+
* Append and fsync event lines. On a partial write or sync failure, restore the
|
|
250
|
+
* previous size before rethrowing because the unchanged cursor will retry the
|
|
251
|
+
* batch; leaving partial bytes would create duplicate sequence numbers.
|
|
252
|
+
*/
|
|
253
|
+
private appendLines;
|
|
254
|
+
private rollbackAppend;
|
|
255
|
+
/** Truncate the log file to `offset` bytes and fsync (discard the crash tail). */
|
|
256
|
+
private repair;
|
|
257
|
+
/**
|
|
258
|
+
* Read the first newline-terminated line of a file without loading the whole
|
|
259
|
+
* file. Returns undefined if the file is empty or has no complete first line.
|
|
260
|
+
* Reads in bounded chunks so a huge log costs only the header read.
|
|
261
|
+
*/
|
|
262
|
+
private readFirstLine;
|
|
263
|
+
/** Read and validate only the independently compressed header frame. */
|
|
264
|
+
private readFirstZstdLine;
|
|
265
|
+
/** Select the numerically highest canonical generation in one Session directory. */
|
|
266
|
+
private resolveGenerationInDirectory;
|
|
267
|
+
/** Find the unique authoritative generation for an id across project directories. */
|
|
268
|
+
private findLog;
|
|
269
|
+
/** Require an existing configured root to be a readable directory. */
|
|
270
|
+
private assertUsableRoot;
|
|
271
|
+
/** Reject metadata that does not identify the selected physical log. */
|
|
272
|
+
private assertStoredIdentity;
|
|
273
|
+
/** Validate a supported historical header against the selected source path. */
|
|
274
|
+
private validateSourceIdentity;
|
|
275
|
+
/**
|
|
276
|
+
* Whether two path spellings resolve to the same physical file. This admits
|
|
277
|
+
* case aliases on case-insensitive filesystems without weakening identity
|
|
278
|
+
* checks on case-sensitive stores.
|
|
279
|
+
*/
|
|
280
|
+
private sameFile;
|
|
281
|
+
/** The human-readable project directories under the configured root. */
|
|
282
|
+
private listProjectDirs;
|
|
283
|
+
/** List session-owned directories and reject the obsolete flat-file layout. */
|
|
284
|
+
private listSessionDirs;
|
|
285
|
+
/** Reject a root that already belongs to the other physical encoding. */
|
|
286
|
+
private ensureRootEncoding;
|
|
287
|
+
private checkRootEncoding;
|
|
288
|
+
private rejectLegacyFlatArtifact;
|
|
289
|
+
private rejectOppositeArtifact;
|
|
290
|
+
/** Return the highest canonical generation encoded with the other configured suffix. */
|
|
291
|
+
private findOppositeGenerationInDirectory;
|
|
292
|
+
private oppositeCompression;
|
|
293
|
+
private encodingMismatch;
|
|
294
|
+
private legacyLayout;
|
|
295
|
+
private exists;
|
|
296
|
+
private assertLogParentAllowsAbsence;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* One open channel onto a JSONL-stored session: the shared storage-handle
|
|
300
|
+
* scaffolding over this backend's file primitives. Reads re-scan the artifact
|
|
301
|
+
* under the stable-read loop.
|
|
302
|
+
*/
|
|
303
|
+
export default JsonlSessionPersistence;
|
|
304
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process write-ownership lock for one session's artifact directory,
|
|
3
|
+
* held for the whole life of a write handle. The arbiter is the kernel:
|
|
4
|
+
* POSIX takes a non-blocking `flock(2)` via native system support on `session.lock`
|
|
5
|
+
* beside the log, and Windows holds a named kernel semaphore derived from
|
|
6
|
+
* that path — never a file lock or handle, so readers, searches, and
|
|
7
|
+
* directory removal proceed freely while the lock is held. Contention maps
|
|
8
|
+
* to `SessionAlreadyOwnedError`; the kernel releases the lock when the
|
|
9
|
+
* holder's descriptor or last object handle closes, including on any process
|
|
10
|
+
* death, so a crashed holder never blocks a successor. A live but wedged
|
|
11
|
+
* holder keeps the lock until its process exits: there is deliberately no
|
|
12
|
+
* expiry that could expropriate a stalled writer whose resumed appends would
|
|
13
|
+
* tear the log.
|
|
14
|
+
* A POSIX lock names an inode, not a path, so after locking the holder
|
|
15
|
+
* verifies the locked inode is still the file at the lock path and retries
|
|
16
|
+
* otherwise: an unlinked-and-recreated lock file carries a fresh inode, and
|
|
17
|
+
* a lock on the orphaned one proves nothing. Removing a live session's lock
|
|
18
|
+
* file therefore forfeits exclusion on POSIX (nothing in the harness does
|
|
19
|
+
* so); Windows has no lock file at all. Readers never touch the lock.
|
|
20
|
+
* The lock is acquired at write-open of an existing artifact and, for a
|
|
21
|
+
* created session, only right before its first materializing write — an
|
|
22
|
+
* unmaterialized session has no filesystem footprint. Release never removes
|
|
23
|
+
* the POSIX lock file: every acquired lock belongs to a materialized or
|
|
24
|
+
* materializing session, and the surviving file keeps the stable inode later
|
|
25
|
+
* lockers verify against. The browser worker stubs the native flock entry to
|
|
26
|
+
* immediate success: it is single-process, so the in-process write claim
|
|
27
|
+
* already excludes every writer.
|
|
28
|
+
* @module @x1a0f3n9/dsh-session-persistence-jsonl/lease
|
|
29
|
+
*/
|
|
30
|
+
import type { SessionId } from '@x1a0f3n9/dsh-session';
|
|
31
|
+
/** Base name of the kernel lock file inside a session's directory. */
|
|
32
|
+
export declare const LEASE_FILENAME = "session.lock";
|
|
33
|
+
/**
|
|
34
|
+
* One held write lock. Constructed only by {@link SessionWriteLease.acquire};
|
|
35
|
+
* `release` closes the descriptor or handle, which is what releases the lock.
|
|
36
|
+
*/
|
|
37
|
+
export declare class SessionWriteLease {
|
|
38
|
+
private readonly held;
|
|
39
|
+
private released;
|
|
40
|
+
private constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Acquire the session directory's kernel write lock.
|
|
43
|
+
* @param dir - the session's artifact directory (created if absent).
|
|
44
|
+
* @param id - the session the lock guards, for error identities.
|
|
45
|
+
* @returns the held lock.
|
|
46
|
+
* @throws {SessionAlreadyOwnedError} while another holder keeps the lock.
|
|
47
|
+
*/
|
|
48
|
+
static acquire(dir: string, id: SessionId): Promise<SessionWriteLease>;
|
|
49
|
+
/**
|
|
50
|
+
* Release the kernel lock by closing its descriptor or handle. The POSIX
|
|
51
|
+
* lock file is never removed: every acquired lock belongs to a
|
|
52
|
+
* materialized or materializing session, and keeping the file preserves
|
|
53
|
+
* the stable inode later lockers verify against. Idempotent.
|
|
54
|
+
*/
|
|
55
|
+
release(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=lease.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Isolated verification for a staged or competing current JSONL generation. */
|
|
2
|
+
import type { JsonlCompression } from './format.ts';
|
|
3
|
+
import type { JsonlExpectedPrefix, JsonlVerifiedGeneration } from './generation.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Verify one current generation in a fresh Worker Thread.
|
|
6
|
+
* @param path - staged or competing current-generation path.
|
|
7
|
+
* @param compression - configured physical encoding.
|
|
8
|
+
* @param expectedId - Session id expected in the decoded header.
|
|
9
|
+
* @param expectedEventCount - exact logical event count expected after decoding.
|
|
10
|
+
* @param expectedPrefix - verified physical prefix; an append tail may be present and is not validated.
|
|
11
|
+
* @param signal - optional cancellation for scheduler wait and Worker execution.
|
|
12
|
+
* @returns stable physical identity and digest observed by the worker.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyCurrentGenerationInWorker(path: string, compression: JsonlCompression, expectedId: string, expectedEventCount: number, expectedPrefix?: JsonlExpectedPrefix, signal?: AbortSignal): Promise<JsonlVerifiedGeneration>;
|
|
15
|
+
//# sourceMappingURL=migration-verifier.d.ts.map
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSONL provider's session storage runtime: its concrete write/read
|
|
3
|
+
* handle with a per-handle mutation chain and a routed live write-behind
|
|
4
|
+
* buffer, the in-process bookkeeping that enforces one active writer per
|
|
5
|
+
* session id, and the backend's live event routing and teardown. Deliberately
|
|
6
|
+
* provider-local: the persistence seam exposes only the service and handle
|
|
7
|
+
* contracts, and the shared contract suites pin equivalent observable
|
|
8
|
+
* behavior across providers.
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@x1a0f3n9/dsh-session';
|
|
13
|
+
import { SessionPersistenceRevision } from '@x1a0f3n9/dsh-session-persistence';
|
|
14
|
+
import type { SessionAccess, SessionHandle, SessionHandleAppendOptions, SessionHandleFlushOptions, SessionHandleReadOptions, SessionHandleReadResult } from '@x1a0f3n9/dsh-session-persistence';
|
|
15
|
+
import type { SessionWriteLease } from './lease.ts';
|
|
16
|
+
/** Maximum intentional wait before a routed live session batch starts writing. */
|
|
17
|
+
export declare const LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
18
|
+
/** The file-storage primitives the handle drives on its owning service. */
|
|
19
|
+
export interface JsonlHandleStorage {
|
|
20
|
+
/** Append encoded lines; `isMaterialized` selects create-vs-extend publication. */
|
|
21
|
+
persistBatch(header: SessionHeader, events: readonly SessionEvent[], isMaterialized: boolean, inheritedEventCount: SessionLogOffset): Promise<void>;
|
|
22
|
+
/** Materialize the header-only artifact for an explicitly flushed empty session. */
|
|
23
|
+
persistHeader(header: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
|
|
24
|
+
/** Truncate a torn physical tail before the first new append lands. */
|
|
25
|
+
truncateTornTail(header: SessionHeader, truncateTo: number): Promise<void>;
|
|
26
|
+
/** Rewrite the current generation with an earlier contiguous event prefix. */
|
|
27
|
+
rewrite(header: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[]): Promise<void>;
|
|
28
|
+
/** Resolve the current-generation artifact path, or `undefined` when absent. */
|
|
29
|
+
resolveCurrentLog(id: SessionId, signal?: AbortSignal): Promise<string | undefined>;
|
|
30
|
+
/** Read and validate the stored log at `path`, including its established event aliasing state. */
|
|
31
|
+
readStoredLog(path: string, expectedId: SessionId, signal?: AbortSignal): Promise<SessionHandleReadResult>;
|
|
32
|
+
/** Whether the id is still a created-but-unmaterialized session here. */
|
|
33
|
+
hasPendingSession(id: SessionId): boolean;
|
|
34
|
+
/** Acquire the session's cross-process write lock in its artifact directory. */
|
|
35
|
+
acquireWriteLease(header: SessionHeader): Promise<SessionWriteLease>;
|
|
36
|
+
/** Drop the handle's bookkeeping on close. */
|
|
37
|
+
releaseHandle(handle: JsonlSessionHandle, materialized: boolean): void;
|
|
38
|
+
}
|
|
39
|
+
/** Mutable per-handle log state; a write handle is its session's single mutator. */
|
|
40
|
+
export interface StorageHandleState {
|
|
41
|
+
/** The stored next-seq (the logical end this handle knows). */
|
|
42
|
+
cursor: number;
|
|
43
|
+
/** Whether the session has a durable artifact yet. */
|
|
44
|
+
materialized: boolean;
|
|
45
|
+
/** Torn-tail truncation point, consumed by the first new append. */
|
|
46
|
+
tornTruncateTo?: number | undefined;
|
|
47
|
+
/** Complete events recovered from the torn final frame; the first mutation rewrites them durably. */
|
|
48
|
+
recoveredTail?: SessionEvent[] | undefined;
|
|
49
|
+
/** Exact fork-inherited prefix length stored with the log; `0` when unseeded. */
|
|
50
|
+
inheritedEventCount: SessionLogOffset;
|
|
51
|
+
/** The validated stored prefix from a write open, served to reads until the first append. */
|
|
52
|
+
primed?: SessionHandleReadResult | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The JSONL session handle. Mutations serialize on a per-handle promise
|
|
56
|
+
* chain; reads re-scan the artifact on demand and never observe a shorter log
|
|
57
|
+
* than a prior read on this handle. Routed live events buffer in a bounded
|
|
58
|
+
* window and drain through the same chain as explicit appends.
|
|
59
|
+
*/
|
|
60
|
+
export declare class JsonlSessionHandle implements SessionHandle {
|
|
61
|
+
private readonly storage;
|
|
62
|
+
readonly id: SessionId;
|
|
63
|
+
readonly header: SessionHeader;
|
|
64
|
+
readonly access: SessionAccess;
|
|
65
|
+
private readonly state;
|
|
66
|
+
/** The cross-process write lock; a create handle acquires it lazily at first materialization. */
|
|
67
|
+
private lease?;
|
|
68
|
+
private chain;
|
|
69
|
+
private closing;
|
|
70
|
+
private observedLength;
|
|
71
|
+
/** Routed live events awaiting their batching deadline (persistence-owned copies). */
|
|
72
|
+
private buffered;
|
|
73
|
+
private batchTimer;
|
|
74
|
+
/** Set when a drain failed; the automatic timer stays quiet until the next drain. */
|
|
75
|
+
private drainPaused;
|
|
76
|
+
private draining;
|
|
77
|
+
constructor(storage: JsonlHandleStorage, id: SessionId, header: SessionHeader, access: SessionAccess, state: StorageHandleState,
|
|
78
|
+
/** The cross-process write lock; a create handle acquires it lazily at first materialization. */
|
|
79
|
+
lease?: SessionWriteLease | undefined);
|
|
80
|
+
/** Exact fork-inherited prefix length stored with this session's log. */
|
|
81
|
+
get inheritedEventCount(): SessionLogOffset;
|
|
82
|
+
/**
|
|
83
|
+
* Read a slice of the valid contiguous logical log; see the seam contract.
|
|
84
|
+
* @param offset - first logical seq to include (default 0).
|
|
85
|
+
* @param length - maximum events returned (default: the rest).
|
|
86
|
+
* @param options - optional cancellation.
|
|
87
|
+
* @returns a slice carrying the aliasing state established by its producer.
|
|
88
|
+
*/
|
|
89
|
+
read(offset?: number, length?: number, options?: SessionHandleReadOptions): Promise<SessionHandleReadResult>;
|
|
90
|
+
/** Read one slice from the prepared historical prefix retained by this handle. */
|
|
91
|
+
private readPrimed;
|
|
92
|
+
/** Read one current physical generation and enforce this handle's monotonic view. */
|
|
93
|
+
private readCurrent;
|
|
94
|
+
/**
|
|
95
|
+
* Durably append a contiguous batch; see the seam contract.
|
|
96
|
+
* @param events - the contiguous batch in seq order.
|
|
97
|
+
* @param options - optional cancellation observed before the write starts.
|
|
98
|
+
*/
|
|
99
|
+
append(events: readonly SessionEvent[], options?: SessionHandleAppendOptions): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Replace the current generation with an earlier contiguous prefix.
|
|
102
|
+
* @param length - number of events to retain.
|
|
103
|
+
* @returns resolution after the rewritten prefix is durable.
|
|
104
|
+
*/
|
|
105
|
+
truncate(length: SessionLogOffset): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Durability barrier; materializes the artifact when nothing has been
|
|
108
|
+
* appended yet, so an explicitly flushed empty session survives this process.
|
|
109
|
+
* @param options - optional cancellation observed before the barrier starts.
|
|
110
|
+
*/
|
|
111
|
+
flush(options?: SessionHandleFlushOptions): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Release the handle; see the seam contract. Idempotent and uncancellable.
|
|
114
|
+
* A write handle first drains its routed live buffer through the still-open
|
|
115
|
+
* storage, so backend teardown loses nothing regardless of which fiber
|
|
116
|
+
* unwinds first; a drain or lock-release failure still frees the in-process
|
|
117
|
+
* claim, then rejects — both failures together reject as one
|
|
118
|
+
* `AggregateError`.
|
|
119
|
+
* @returns settlement of the release.
|
|
120
|
+
*/
|
|
121
|
+
close(): Promise<void>;
|
|
122
|
+
/** `await using` support: delegates to {@link close}. */
|
|
123
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Buffer one published live session event and arm the bounded batching
|
|
126
|
+
* window when it is idle. The routing installer is the only caller.
|
|
127
|
+
* @param event - the live event, retained as a persistence-owned copy.
|
|
128
|
+
* @param reportBackgroundFailure - observes a deadline-driven drain failure
|
|
129
|
+
* (the events stay buffered; the next {@link drainLive} retries loudly).
|
|
130
|
+
*/
|
|
131
|
+
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void;
|
|
132
|
+
/**
|
|
133
|
+
* Durably drain the routed live buffer through the mutation chain;
|
|
134
|
+
* concurrent callers join one drain, and a failure retains the batch in
|
|
135
|
+
* order so `session/flush` can retry and reject loudly.
|
|
136
|
+
*/
|
|
137
|
+
drainLive(): Promise<void>;
|
|
138
|
+
private drainBuffered;
|
|
139
|
+
/** The shared durable-append body: contiguity, ownership, torn-tail repair, storage write, state advance. */
|
|
140
|
+
private persistContiguous;
|
|
141
|
+
/**
|
|
142
|
+
* Hold the cross-process write lock before this session's first durable
|
|
143
|
+
* write. An open write handle holds it from construction; a create handle
|
|
144
|
+
* acquires it here — immediately before the first log bytes publish — and
|
|
145
|
+
* keeps it through close even when materialization then fails, so a
|
|
146
|
+
* materializing session stays exclusively owned across retries.
|
|
147
|
+
*/
|
|
148
|
+
private ensureLease;
|
|
149
|
+
/** Serialize one operation onto the chain without the closed-handle refusal (drain-from-close). */
|
|
150
|
+
private enqueueChain;
|
|
151
|
+
/** Serialize one public mutating operation onto this handle's chain. */
|
|
152
|
+
private run;
|
|
153
|
+
private assertOpen;
|
|
154
|
+
}
|
|
155
|
+
/** One created-but-unmaterialized session tracked in this process only. */
|
|
156
|
+
export interface PendingSession {
|
|
157
|
+
readonly header: SessionHeader;
|
|
158
|
+
readonly revision: SessionPersistenceRevision;
|
|
159
|
+
/** Exact fork-inherited prefix length supplied at create. */
|
|
160
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The JSONL backend's in-process bookkeeping: the single active writer per
|
|
164
|
+
* session id (doubling as the live event router), the open-handle set the
|
|
165
|
+
* teardown sweep closes, and the created-but-unmaterialized sessions this
|
|
166
|
+
* process can already observe.
|
|
167
|
+
*/
|
|
168
|
+
export declare class JsonlBackendTracker {
|
|
169
|
+
private readonly name;
|
|
170
|
+
/** Every open handle; teardown closes what remains. */
|
|
171
|
+
readonly openHandles: Set<SessionHandle>;
|
|
172
|
+
/** `null` marks a claim whose handle is still being constructed. */
|
|
173
|
+
private readonly writers;
|
|
174
|
+
private readonly pending;
|
|
175
|
+
private counter;
|
|
176
|
+
/** @param name - backend label used in in-memory revision tokens and teardown errors. */
|
|
177
|
+
constructor(name: string);
|
|
178
|
+
/**
|
|
179
|
+
* Claim write ownership and record the created session as pending, making
|
|
180
|
+
* it observable to this process before it materializes. Before
|
|
181
|
+
* materialization this registration is the only guard — session ids do not
|
|
182
|
+
* collide across processes, and no durable artifact exists for another
|
|
183
|
+
* process to open; the handle takes the cross-process lock at its first
|
|
184
|
+
* materializing write.
|
|
185
|
+
* @param header - the validated detached header.
|
|
186
|
+
* @param inheritedEventCount - the exact fork-inherited prefix length.
|
|
187
|
+
* @throws {SessionAlreadyExistsError} when a concurrent create or an open
|
|
188
|
+
* write handle holds the id — for create, the duplicate is the fact.
|
|
189
|
+
*/
|
|
190
|
+
registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void;
|
|
191
|
+
/**
|
|
192
|
+
* Claim write ownership for an existing session.
|
|
193
|
+
* @param id - the session to claim.
|
|
194
|
+
* @throws {SessionAlreadyOwnedError} when an active write handle exists.
|
|
195
|
+
*/
|
|
196
|
+
claimWrite(id: SessionId): void;
|
|
197
|
+
/**
|
|
198
|
+
* Roll a failed write open back.
|
|
199
|
+
* @param id - the session whose claim is dropped.
|
|
200
|
+
*/
|
|
201
|
+
releaseClaim(id: SessionId): void;
|
|
202
|
+
/**
|
|
203
|
+
* The pending entry for a created-but-unmaterialized session, if any.
|
|
204
|
+
* @param id - the session to look up.
|
|
205
|
+
* @returns the pending header and in-memory revision.
|
|
206
|
+
*/
|
|
207
|
+
pendingOf(id: SessionId): PendingSession | undefined;
|
|
208
|
+
/**
|
|
209
|
+
* Whether this process still tracks a created-but-unmaterialized session.
|
|
210
|
+
* @param id - the session to test.
|
|
211
|
+
* @returns true while the pending entry exists.
|
|
212
|
+
*/
|
|
213
|
+
hasPending(id: SessionId): boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Return the active writer for one session, if this process owns it.
|
|
216
|
+
* @param id - the session to resolve.
|
|
217
|
+
* @returns the active writer, or undefined while no writer is open.
|
|
218
|
+
*/
|
|
219
|
+
writerOf(id: SessionId): JsonlSessionHandle | undefined;
|
|
220
|
+
/**
|
|
221
|
+
* Iterate the pending sessions for listing.
|
|
222
|
+
* @returns the pending entries, keyed by session id.
|
|
223
|
+
*/
|
|
224
|
+
pendingEntries(): IterableIterator<[SessionId, PendingSession]>;
|
|
225
|
+
/**
|
|
226
|
+
* Drop a pending entry once the session materialized durably.
|
|
227
|
+
* @param id - the session that reached durable storage.
|
|
228
|
+
*/
|
|
229
|
+
materialized(id: SessionId): void;
|
|
230
|
+
/**
|
|
231
|
+
* Track one open handle for teardown and, for a write handle, bind it as
|
|
232
|
+
* the session's live event route.
|
|
233
|
+
* @param handle - the just-constructed handle.
|
|
234
|
+
* @returns the same handle, for construction-site chaining.
|
|
235
|
+
*/
|
|
236
|
+
adopt(handle: JsonlSessionHandle): JsonlSessionHandle;
|
|
237
|
+
/**
|
|
238
|
+
* Release one handle's bookkeeping on close. A write handle drops its
|
|
239
|
+
* ownership claim; a creator that never materialized leaves nothing behind —
|
|
240
|
+
* the session never existed.
|
|
241
|
+
* @param handle - the closing handle.
|
|
242
|
+
* @param materialized - whether the session reached durable storage.
|
|
243
|
+
*/
|
|
244
|
+
release(handle: JsonlSessionHandle, materialized: boolean): void;
|
|
245
|
+
/**
|
|
246
|
+
* Drain and flush every active write handle — the service-wide durability
|
|
247
|
+
* barrier behind `SessionPersistence.flush`.
|
|
248
|
+
* @throws {AggregateError} naming each session whose flush failed; the
|
|
249
|
+
* remaining handles still flush.
|
|
250
|
+
*/
|
|
251
|
+
flushAll(): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Install the backend's live session routing and teardown. Persistence
|
|
254
|
+
* enforces one active write handle per id, so the listeners route published
|
|
255
|
+
* sessions' events by id; the teardown effect closes every open handle —
|
|
256
|
+
* close drains the routed buffer — and aggregates failures. This provider
|
|
257
|
+
* owns no separate storage connection, so closing handles is the complete
|
|
258
|
+
* teardown. Registrations are effects of the current fiber.
|
|
259
|
+
* @param ctx - the backend's context.
|
|
260
|
+
*/
|
|
261
|
+
install(ctx: Context): void;
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type JsonlGenerationRuntime, type JsonlGenerationRuntimeOverrides } from '../generation.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Create generation operations with deterministic I/O and race seams for tests.
|
|
4
|
+
* @param overrides - deterministic filesystem, platform, and race dependencies.
|
|
5
|
+
* @returns bound generation operations.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createJsonlGenerationTestRuntime(overrides?: JsonlGenerationRuntimeOverrides): JsonlGenerationRuntime;
|
|
8
|
+
//# sourceMappingURL=generation.d.ts.map
|