@gajae-code/coding-agent 0.12.21 → 0.13.0
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/CHANGELOG.md +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -83,6 +83,19 @@ function validateRangeReadBounds(start: number, length: number): void {
|
|
|
83
83
|
if (length > SESSION_RANGE_READ_MAX_BYTES) throw new RangeError("Session range read exceeds the bounded maximum");
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function normalizeBufferedWriterCapacity(value: number | undefined): number {
|
|
87
|
+
const capacity = value ?? SESSION_STORAGE_BUFFERED_WRITER_DEFAULT_BYTES;
|
|
88
|
+
if (
|
|
89
|
+
!Number.isSafeInteger(capacity) ||
|
|
90
|
+
capacity < SESSION_STORAGE_BUFFERED_WRITER_MIN_BYTES ||
|
|
91
|
+
capacity > SESSION_STORAGE_BUFFERED_WRITER_MAX_BYTES
|
|
92
|
+
)
|
|
93
|
+
throw new RangeError(
|
|
94
|
+
`Buffered writer capacity must be between ${SESSION_STORAGE_BUFFERED_WRITER_MIN_BYTES} and ${SESSION_STORAGE_BUFFERED_WRITER_MAX_BYTES} bytes`,
|
|
95
|
+
);
|
|
96
|
+
return capacity;
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
|
|
87
100
|
return {
|
|
88
101
|
dev: stats.dev,
|
|
@@ -135,6 +148,23 @@ export class SessionStorageWriterRetryableCloseError extends Error {
|
|
|
135
148
|
export interface SessionStorageWriterCloseAdapter {
|
|
136
149
|
close(fd: number): void;
|
|
137
150
|
}
|
|
151
|
+
/** Bounded buffered-writer capacity limits. */
|
|
152
|
+
export const SESSION_STORAGE_BUFFERED_WRITER_MIN_BYTES = 64 * 1024;
|
|
153
|
+
export const SESSION_STORAGE_BUFFERED_WRITER_MAX_BYTES = 512 * 1024;
|
|
154
|
+
export const SESSION_STORAGE_BUFFERED_WRITER_DEFAULT_BYTES = 256 * 1024;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Counters for a synchronous buffered sidecar writer. `bytesWritten` and
|
|
158
|
+
* `writeCalls` count operations against the backend, not calls accepted into
|
|
159
|
+
* the in-process buffer.
|
|
160
|
+
*/
|
|
161
|
+
export interface SessionStorageBufferedWriterInstrumentation {
|
|
162
|
+
readonly bytesSubmitted: number;
|
|
163
|
+
readonly bytesWritten: number;
|
|
164
|
+
readonly writeCalls: number;
|
|
165
|
+
readonly flushCalls: number;
|
|
166
|
+
readonly bufferedBytes: number;
|
|
167
|
+
}
|
|
138
168
|
|
|
139
169
|
/** Options for opening a {@link SessionStorageWriter}. */
|
|
140
170
|
export interface SessionStorageWriterOpenOptions {
|
|
@@ -144,6 +174,8 @@ export interface SessionStorageWriterOpenOptions {
|
|
|
144
174
|
closeAdapter?: SessionStorageWriterCloseAdapter;
|
|
145
175
|
/** Opaque authority for default-computed managed destinations only. */
|
|
146
176
|
securityContext?: SessionStorageSecurityContext;
|
|
177
|
+
/** Enable bounded synchronous buffering for disposable sidecar writes. */
|
|
178
|
+
bufferSize?: number;
|
|
147
179
|
}
|
|
148
180
|
|
|
149
181
|
/**
|
|
@@ -215,6 +247,20 @@ export interface SessionStorageWriter {
|
|
|
215
247
|
/** Stored error for non-success close states (`close_failed_retryable`/`close_unknown`). */
|
|
216
248
|
getCloseError(): Error | undefined;
|
|
217
249
|
}
|
|
250
|
+
|
|
251
|
+
/** Synchronous byte-oriented writer with bounded buffering for disposable sidecars. */
|
|
252
|
+
export interface SessionStorageBufferedWriter extends SessionStorageWriter {
|
|
253
|
+
/** Append already-serialized bytes; the caller's view may be reused on return. */
|
|
254
|
+
writeBytesSync(bytes: Uint8Array): void;
|
|
255
|
+
/** Flush pending bytes to the backend without synchronizing them to stable storage. */
|
|
256
|
+
flushSync(): void;
|
|
257
|
+
/** Flush pending bytes, then synchronize the backend. */
|
|
258
|
+
fsyncSync(): void;
|
|
259
|
+
/** Flush pending bytes, then close the backend descriptor. */
|
|
260
|
+
closeSync(): void;
|
|
261
|
+
/** Snapshot backend-write counters and current pending capacity. */
|
|
262
|
+
getInstrumentation(): SessionStorageBufferedWriterInstrumentation;
|
|
263
|
+
}
|
|
218
264
|
// =============================================================================
|
|
219
265
|
// Staged streaming writer contract (two-pass fork publication, immutable destinations)
|
|
220
266
|
// =============================================================================
|
|
@@ -261,6 +307,10 @@ export interface StagedStreamingWriter {
|
|
|
261
307
|
publishNoReplace(): void;
|
|
262
308
|
}
|
|
263
309
|
|
|
310
|
+
export interface SessionStorageExclusiveLock {
|
|
311
|
+
releaseSync(): void;
|
|
312
|
+
}
|
|
313
|
+
|
|
264
314
|
export interface SessionStorage {
|
|
265
315
|
ensureDirSync(dir: string): void;
|
|
266
316
|
existsSync(path: string): boolean;
|
|
@@ -303,12 +353,19 @@ export interface SessionStorage {
|
|
|
303
353
|
*/
|
|
304
354
|
deleteSessionVerified?(target: VerifiedSessionDeleteTarget): Promise<VerifiedSessionDeleteResult>;
|
|
305
355
|
openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter;
|
|
356
|
+
/** Open a bounded synchronous byte-oriented writer for disposable sidecars. */
|
|
357
|
+
openBufferedWriter?(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageBufferedWriter;
|
|
306
358
|
/** Bounded recorded-length read with descriptor identity validation (additive). */
|
|
307
359
|
readRangeSync?(path: string, start: number, length: number): SessionStorageRangeSnapshot;
|
|
308
360
|
/** Async bounded recorded-length read with descriptor identity validation (additive). */
|
|
309
361
|
readRange?(path: string, start: number, length: number): Promise<SessionStorageRangeSnapshot>;
|
|
310
362
|
/** Open a staged streaming writer for one immutable one-shot destination (additive). */
|
|
311
363
|
openStagedWriter?(path: string, options?: SessionStorageWriterOpenOptions): StagedStreamingWriter;
|
|
364
|
+
/** Acquire an owner-bound exclusive lock; returns undefined while another owner holds it. */
|
|
365
|
+
acquireExclusiveLockSync?(
|
|
366
|
+
path: string,
|
|
367
|
+
options?: { securityContext?: SessionStorageSecurityContext },
|
|
368
|
+
): SessionStorageExclusiveLock | undefined;
|
|
312
369
|
}
|
|
313
370
|
|
|
314
371
|
// =============================================================================
|
|
@@ -608,6 +665,29 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
608
665
|
): void {
|
|
609
666
|
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
610
667
|
throw new Error("Invalid managed session security context");
|
|
668
|
+
if (securityContext) {
|
|
669
|
+
const root = fs.lstatSync(securityContext.rootAuthority.canonicalPath, { bigint: true });
|
|
670
|
+
if (
|
|
671
|
+
!root.isDirectory() ||
|
|
672
|
+
root.isSymbolicLink() ||
|
|
673
|
+
root.dev !== securityContext.rootAuthority.dev ||
|
|
674
|
+
root.ino !== securityContext.rootAuthority.ino
|
|
675
|
+
)
|
|
676
|
+
throw new Error("Managed writer root authority changed");
|
|
677
|
+
if (securityContext.retainedAuthority) {
|
|
678
|
+
const relative = path.relative(securityContext.sessionDir, pathname).split(path.sep).join("/");
|
|
679
|
+
const retained = securityContext.retainedAuthority.stat(relative);
|
|
680
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
681
|
+
if (
|
|
682
|
+
!retained.ok ||
|
|
683
|
+
!retained.identity ||
|
|
684
|
+
retained.identity.dev !== opened.dev.toString() ||
|
|
685
|
+
retained.identity.ino !== opened.ino.toString() ||
|
|
686
|
+
retained.identity.nlink !== opened.nlink.toString()
|
|
687
|
+
)
|
|
688
|
+
throw new Error("Managed writer descriptor escaped retained authority");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
611
691
|
if (process.platform !== "linux" || !securityContext) {
|
|
612
692
|
if (operation === "apply") {
|
|
613
693
|
const applied = validateNativeSecurityResult(
|
|
@@ -670,8 +750,9 @@ function fsyncDirectorySync(pathname: string): void {
|
|
|
670
750
|
function unlinkOwnedStagedSync(stagingPath: string, expected: { dev: bigint; ino: bigint }): void {
|
|
671
751
|
try {
|
|
672
752
|
const named = fs.lstatSync(stagingPath, { bigint: true });
|
|
673
|
-
if (named.dev !== expected.dev || named.ino !== expected.ino) return;
|
|
674
|
-
fs.
|
|
753
|
+
if (!named.isFile() || named.isSymbolicLink() || named.dev !== expected.dev || named.ino !== expected.ino) return;
|
|
754
|
+
const sha256 = createHash("sha256").update(fs.readFileSync(stagingPath)).digest("hex");
|
|
755
|
+
exactRemoveSessionStorageLockPath(stagingPath, named, sha256, named.nlink > 1n);
|
|
675
756
|
} catch {
|
|
676
757
|
// ENOENT means the name was already consumed or removed; cleanup is best-effort.
|
|
677
758
|
return;
|
|
@@ -786,7 +867,11 @@ function createFileCommitMarkerCheckedSync(
|
|
|
786
867
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
787
868
|
stagedIdentity = { dev: opened.dev, ino: opened.ino };
|
|
788
869
|
let offset = 0;
|
|
789
|
-
while (offset < bytes.byteLength)
|
|
870
|
+
while (offset < bytes.byteLength) {
|
|
871
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
872
|
+
if (written === 0) throw new Error("Short write");
|
|
873
|
+
offset += written;
|
|
874
|
+
}
|
|
790
875
|
fs.fsyncSync(fd);
|
|
791
876
|
secureOwnerOnlyFileDescriptor(tempPath, fd, "verify", securityContext);
|
|
792
877
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
@@ -855,7 +940,11 @@ function replaceFileCommitMarkerCheckedSync(
|
|
|
855
940
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
856
941
|
stagedIdentity = { dev: opened.dev, ino: opened.ino };
|
|
857
942
|
let offset = 0;
|
|
858
|
-
while (offset < bytes.byteLength)
|
|
943
|
+
while (offset < bytes.byteLength) {
|
|
944
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
945
|
+
if (written === 0) throw new Error("Short write");
|
|
946
|
+
offset += written;
|
|
947
|
+
}
|
|
859
948
|
fs.fsyncSync(fd);
|
|
860
949
|
secureOwnerOnlyFileDescriptor(tempPath, fd, "verify", securityContext);
|
|
861
950
|
staged = fs.fstatSync(fd, { bigint: true });
|
|
@@ -926,7 +1015,7 @@ const writerRegistry = new FinalizationRegistry<number>(fd => {
|
|
|
926
1015
|
}
|
|
927
1016
|
});
|
|
928
1017
|
|
|
929
|
-
class FileSessionStorageWriter implements
|
|
1018
|
+
class FileSessionStorageWriter implements SessionStorageBufferedWriter {
|
|
930
1019
|
#fd: number;
|
|
931
1020
|
#path: string;
|
|
932
1021
|
|
|
@@ -937,10 +1026,21 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
937
1026
|
#closeAdapter: SessionStorageWriterCloseAdapter;
|
|
938
1027
|
#securityContext: SessionStorageSecurityContext;
|
|
939
1028
|
|
|
1029
|
+
#bufferSize: number | undefined;
|
|
1030
|
+
#buffer: Buffer | undefined;
|
|
1031
|
+
#bufferedBytes = 0;
|
|
1032
|
+
#bytesSubmitted = 0;
|
|
1033
|
+
#bytesWritten = 0;
|
|
1034
|
+
#writeCalls = 0;
|
|
1035
|
+
#flushCalls = 0;
|
|
1036
|
+
|
|
940
1037
|
constructor(fpath: string, options?: SessionStorageWriterOpenOptions) {
|
|
941
1038
|
this.#onError = options?.onError;
|
|
942
1039
|
this.#closeAdapter = options?.closeAdapter ?? defaultCloseAdapter;
|
|
943
1040
|
this.#securityContext = options?.securityContext;
|
|
1041
|
+
this.#bufferSize =
|
|
1042
|
+
options?.bufferSize === undefined ? undefined : normalizeBufferedWriterCapacity(options.bufferSize);
|
|
1043
|
+
this.#buffer = this.#bufferSize === undefined ? undefined : Buffer.allocUnsafe(this.#bufferSize);
|
|
944
1044
|
const flags = options?.flags ?? "a";
|
|
945
1045
|
const dir = path.dirname(fpath);
|
|
946
1046
|
assertNoReparsePath(dir);
|
|
@@ -987,47 +1087,97 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
987
1087
|
}
|
|
988
1088
|
}
|
|
989
1089
|
|
|
990
|
-
|
|
1090
|
+
#assertOpen(): void {
|
|
991
1091
|
if (this.#closeState !== "open") throw this.#nonOpenWriteError();
|
|
992
1092
|
if (this.#error) throw this.#error;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
#asBuffer(bytes: Uint8Array): Buffer {
|
|
1096
|
+
if (Buffer.isBuffer(bytes)) return bytes;
|
|
1097
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
#writeToKernel(bytes: Uint8Array): void {
|
|
1101
|
+
const buffer = this.#asBuffer(bytes);
|
|
1102
|
+
let offset = 0;
|
|
1103
|
+
while (offset < buffer.byteLength) {
|
|
1104
|
+
this.#writeCalls++;
|
|
1105
|
+
const written = fs.writeSync(this.#fd, buffer, offset, buffer.byteLength - offset);
|
|
1106
|
+
if (written === 0) throw new Error("Short write");
|
|
1107
|
+
offset += written;
|
|
1108
|
+
this.#bytesWritten += written;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
#flushPending(): void {
|
|
1113
|
+
this.#flushCalls++;
|
|
1114
|
+
if (!this.#buffer || this.#bufferedBytes === 0) return;
|
|
1115
|
+
this.#writeToKernel(this.#buffer.subarray(0, this.#bufferedBytes));
|
|
1116
|
+
this.#bufferedBytes = 0;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
#appendBytes(bytes: Uint8Array): void {
|
|
1120
|
+
if (bytes.byteLength === 0) return;
|
|
1121
|
+
if (!this.#buffer || !this.#bufferSize) {
|
|
1122
|
+
this.#writeToKernel(bytes);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const source = this.#asBuffer(bytes);
|
|
1126
|
+
if (source.byteLength >= this.#bufferSize) {
|
|
1127
|
+
this.#flushPending();
|
|
1128
|
+
this.#writeToKernel(source);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (this.#bufferedBytes + source.byteLength > this.#bufferSize) this.#flushPending();
|
|
1132
|
+
source.copy(this.#buffer, this.#bufferedBytes);
|
|
1133
|
+
this.#bufferedBytes += source.byteLength;
|
|
1134
|
+
if (this.#bufferedBytes === this.#bufferSize) this.#flushPending();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
writeBytesSync(bytes: Uint8Array): void {
|
|
1138
|
+
this.#assertOpen();
|
|
993
1139
|
try {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
while (offset < buf.length) {
|
|
997
|
-
const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset);
|
|
998
|
-
if (written === 0) {
|
|
999
|
-
throw new Error("Short write");
|
|
1000
|
-
}
|
|
1001
|
-
offset += written;
|
|
1002
|
-
}
|
|
1140
|
+
this.#appendBytes(bytes);
|
|
1141
|
+
this.#bytesSubmitted += bytes.byteLength;
|
|
1003
1142
|
} catch (err) {
|
|
1004
1143
|
throw this.#recordError(err);
|
|
1005
1144
|
}
|
|
1006
1145
|
}
|
|
1007
1146
|
|
|
1147
|
+
writeLineSync(line: string): void {
|
|
1148
|
+
this.writeBytesSync(Buffer.from(line, "utf-8"));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1008
1151
|
async writeLine(line: string): Promise<void> {
|
|
1009
1152
|
this.writeLineSync(line);
|
|
1010
1153
|
}
|
|
1011
1154
|
|
|
1155
|
+
flushSync(): void {
|
|
1156
|
+
this.#assertOpen();
|
|
1157
|
+
try {
|
|
1158
|
+
this.#flushPending();
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
throw this.#recordError(err);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1012
1164
|
async flush(): Promise<void> {
|
|
1013
|
-
|
|
1014
|
-
if (this.#error) throw this.#error;
|
|
1015
|
-
// OS buffers are flushed on fsync, nothing to do here
|
|
1165
|
+
this.flushSync();
|
|
1016
1166
|
}
|
|
1017
1167
|
|
|
1018
1168
|
fsyncSync(): void {
|
|
1019
|
-
|
|
1020
|
-
if (this.#error) throw this.#error;
|
|
1169
|
+
this.#assertOpen();
|
|
1021
1170
|
try {
|
|
1171
|
+
this.#flushPending();
|
|
1022
1172
|
fs.fsyncSync(this.#fd);
|
|
1023
1173
|
secureOwnerOnlyFileDescriptor(this.#path, this.#fd, "verify", this.#securityContext);
|
|
1024
1174
|
} catch (err) {
|
|
1025
1175
|
throw this.#recordError(err);
|
|
1026
1176
|
}
|
|
1027
1177
|
}
|
|
1178
|
+
|
|
1028
1179
|
statSync(): SessionStorageStat {
|
|
1029
|
-
|
|
1030
|
-
if (this.#error) throw this.#error;
|
|
1180
|
+
this.#assertOpen();
|
|
1031
1181
|
return statFromNode(fs.fstatSync(this.#fd, { bigint: true }));
|
|
1032
1182
|
}
|
|
1033
1183
|
|
|
@@ -1041,6 +1191,15 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
1041
1191
|
// Dispatched close already threw: outcome is uncertain. Never dispatch OS close
|
|
1042
1192
|
// for this numeric fd again; surface the stored non-quiescent error.
|
|
1043
1193
|
if (this.#closeState === "close_unknown") throw this.#closeError!;
|
|
1194
|
+
|
|
1195
|
+
let flushError: Error | undefined;
|
|
1196
|
+
if (!this.#error) {
|
|
1197
|
+
try {
|
|
1198
|
+
this.#flushPending();
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
flushError = this.#recordError(err);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1044
1203
|
// State is "open" or "close_failed_retryable": a close may be dispatched.
|
|
1045
1204
|
try {
|
|
1046
1205
|
secureOwnerOnlyFileDescriptor(this.#path, this.#fd, "verify", this.#securityContext);
|
|
@@ -1072,7 +1231,9 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
1072
1231
|
// Successful underlying close confirms closed.
|
|
1073
1232
|
this.#closeState = "closed";
|
|
1074
1233
|
this.#closeError = undefined;
|
|
1234
|
+
this.#bufferedBytes = 0;
|
|
1075
1235
|
writerRegistry.unregister(this);
|
|
1236
|
+
if (flushError) throw flushError;
|
|
1076
1237
|
}
|
|
1077
1238
|
|
|
1078
1239
|
async close(): Promise<void> {
|
|
@@ -1092,6 +1253,16 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
1092
1253
|
getCloseError(): Error | undefined {
|
|
1093
1254
|
return this.#closeError;
|
|
1094
1255
|
}
|
|
1256
|
+
|
|
1257
|
+
getInstrumentation(): SessionStorageBufferedWriterInstrumentation {
|
|
1258
|
+
return {
|
|
1259
|
+
bytesSubmitted: this.#bytesSubmitted,
|
|
1260
|
+
bytesWritten: this.#bytesWritten,
|
|
1261
|
+
writeCalls: this.#writeCalls,
|
|
1262
|
+
flushCalls: this.#flushCalls,
|
|
1263
|
+
bufferedBytes: this.#bufferedBytes,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1095
1266
|
}
|
|
1096
1267
|
/**
|
|
1097
1268
|
* File-backend staged streaming writer: streams lines to a sibling staging file and
|
|
@@ -1149,13 +1320,14 @@ class FileStagedStreamingWriter implements StagedStreamingWriter {
|
|
|
1149
1320
|
this.#assertOpen();
|
|
1150
1321
|
if (bytes.byteLength > STAGED_WRITER_LINE_MAX_BYTES)
|
|
1151
1322
|
throw new RangeError("Staged line exceeds the bounded maximum");
|
|
1152
|
-
const line = Buffer.concat([Buffer.from(bytes), newlineBuffer]);
|
|
1153
1323
|
try {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1324
|
+
for (const chunk of [bytes, newlineBuffer]) {
|
|
1325
|
+
let written = 0;
|
|
1326
|
+
while (written < chunk.byteLength) {
|
|
1327
|
+
const count = fs.writeSync(this.#fd, chunk, written, chunk.byteLength - written);
|
|
1328
|
+
if (count === 0) throw new Error("Short write");
|
|
1329
|
+
written += count;
|
|
1330
|
+
}
|
|
1159
1331
|
}
|
|
1160
1332
|
this.#lineCount++;
|
|
1161
1333
|
} catch (err) {
|
|
@@ -1381,7 +1553,348 @@ class FileStagedStreamingWriter implements StagedStreamingWriter {
|
|
|
1381
1553
|
}
|
|
1382
1554
|
}
|
|
1383
1555
|
|
|
1556
|
+
type SessionStorageLockOwner = {
|
|
1557
|
+
pid: number;
|
|
1558
|
+
incarnation?: string;
|
|
1559
|
+
token: string;
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
type SessionStorageLockOwnerProbe =
|
|
1563
|
+
| { kind: "absent" }
|
|
1564
|
+
| { kind: "live"; incarnation?: string }
|
|
1565
|
+
| { kind: "unverifiable" };
|
|
1566
|
+
|
|
1567
|
+
function probeSessionStorageLockOwner(pid: number): SessionStorageLockOwnerProbe {
|
|
1568
|
+
try {
|
|
1569
|
+
const owner = nativeSessionStorage().Process.fromPid(pid) as { incarnation?: unknown } | null;
|
|
1570
|
+
if (!owner) return { kind: "absent" };
|
|
1571
|
+
return {
|
|
1572
|
+
kind: "live",
|
|
1573
|
+
...(typeof owner.incarnation === "string" && owner.incarnation.length > 0
|
|
1574
|
+
? { incarnation: owner.incarnation }
|
|
1575
|
+
: {}),
|
|
1576
|
+
};
|
|
1577
|
+
} catch {
|
|
1578
|
+
return { kind: "unverifiable" };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function parseSessionStorageLockOwner(bytes: Buffer): SessionStorageLockOwner | undefined {
|
|
1583
|
+
const text = bytes.toString("utf8").trim();
|
|
1584
|
+
try {
|
|
1585
|
+
const value = JSON.parse(text) as Partial<SessionStorageLockOwner>;
|
|
1586
|
+
if (
|
|
1587
|
+
!Number.isSafeInteger(value.pid) ||
|
|
1588
|
+
(value.pid ?? 0) <= 0 ||
|
|
1589
|
+
typeof value.token !== "string" ||
|
|
1590
|
+
value.token.length === 0 ||
|
|
1591
|
+
(value.incarnation !== undefined && (typeof value.incarnation !== "string" || value.incarnation.length === 0))
|
|
1592
|
+
)
|
|
1593
|
+
return undefined;
|
|
1594
|
+
return value as SessionStorageLockOwner;
|
|
1595
|
+
} catch {
|
|
1596
|
+
const legacy = /^(\d+):(.+)$/.exec(text);
|
|
1597
|
+
if (!legacy) return undefined;
|
|
1598
|
+
const pid = Number.parseInt(legacy[1]!, 10);
|
|
1599
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
1600
|
+
return { pid, token: legacy[2]! };
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function exactRemoveSessionStorageLockPath(
|
|
1605
|
+
lockPath: string,
|
|
1606
|
+
expected: fs.BigIntStats,
|
|
1607
|
+
sha256: string,
|
|
1608
|
+
allowHardLink = false,
|
|
1609
|
+
): boolean {
|
|
1610
|
+
try {
|
|
1611
|
+
const parent = fs.lstatSync(path.dirname(lockPath), { bigint: true });
|
|
1612
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) return false;
|
|
1613
|
+
const result = nativeSessionStorage().exactUnlink(lockPath, {
|
|
1614
|
+
dev: expected.dev,
|
|
1615
|
+
ino: expected.ino,
|
|
1616
|
+
nlink: expected.nlink,
|
|
1617
|
+
parentDev: parent.dev,
|
|
1618
|
+
parentIno: parent.ino,
|
|
1619
|
+
size: expected.size,
|
|
1620
|
+
mtimeNs: expected.mtimeNs,
|
|
1621
|
+
sha256,
|
|
1622
|
+
quarantineName: `${path.basename(lockPath)}.reap-${randomUUID()}`,
|
|
1623
|
+
allowHardLink,
|
|
1624
|
+
});
|
|
1625
|
+
return result.ok || (result.code === "cleanup_pending" && typeof result.detachedPath === "string");
|
|
1626
|
+
} catch {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function exactRemoveOwnedSessionStorageLockPath(
|
|
1632
|
+
lockPath: string,
|
|
1633
|
+
expected: Pick<fs.BigIntStats, "dev" | "ino">,
|
|
1634
|
+
): boolean {
|
|
1635
|
+
let fd: number | undefined;
|
|
1636
|
+
try {
|
|
1637
|
+
fd = fs.openSync(lockPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
1638
|
+
const current = fs.fstatSync(fd, { bigint: true });
|
|
1639
|
+
if (
|
|
1640
|
+
!current.isFile() ||
|
|
1641
|
+
current.dev !== expected.dev ||
|
|
1642
|
+
current.ino !== expected.ino ||
|
|
1643
|
+
current.nlink < 1n ||
|
|
1644
|
+
current.nlink > 2n ||
|
|
1645
|
+
current.size > 4096n
|
|
1646
|
+
)
|
|
1647
|
+
return false;
|
|
1648
|
+
const bytes = Buffer.allocUnsafe(Number(current.size));
|
|
1649
|
+
let offset = 0;
|
|
1650
|
+
while (offset < bytes.byteLength) {
|
|
1651
|
+
const read = fs.readSync(fd, bytes, offset, bytes.byteLength - offset, offset);
|
|
1652
|
+
if (read === 0) return false;
|
|
1653
|
+
offset += read;
|
|
1654
|
+
}
|
|
1655
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
1656
|
+
fs.closeSync(fd);
|
|
1657
|
+
fd = undefined;
|
|
1658
|
+
return exactRemoveSessionStorageLockPath(lockPath, current, sha256);
|
|
1659
|
+
} catch {
|
|
1660
|
+
return false;
|
|
1661
|
+
} finally {
|
|
1662
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function reclaimStaleSessionStorageLockSync(lockPath: string): boolean {
|
|
1667
|
+
let fd: number | undefined;
|
|
1668
|
+
let expected: fs.BigIntStats | undefined;
|
|
1669
|
+
let owner: SessionStorageLockOwner | undefined;
|
|
1670
|
+
let sha256: string | undefined;
|
|
1671
|
+
try {
|
|
1672
|
+
fd = fs.openSync(lockPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
1673
|
+
expected = fs.fstatSync(fd, { bigint: true });
|
|
1674
|
+
if (
|
|
1675
|
+
!expected.isFile() ||
|
|
1676
|
+
expected.nlink < 1n ||
|
|
1677
|
+
expected.nlink > 2n ||
|
|
1678
|
+
expected.size <= 0n ||
|
|
1679
|
+
expected.size > 4096n
|
|
1680
|
+
)
|
|
1681
|
+
return false;
|
|
1682
|
+
const bytes = Buffer.allocUnsafe(Number(expected.size));
|
|
1683
|
+
let offset = 0;
|
|
1684
|
+
while (offset < bytes.byteLength) {
|
|
1685
|
+
const read = fs.readSync(fd, bytes, offset, bytes.byteLength - offset, offset);
|
|
1686
|
+
if (read === 0) return false;
|
|
1687
|
+
offset += read;
|
|
1688
|
+
}
|
|
1689
|
+
owner = parseSessionStorageLockOwner(bytes);
|
|
1690
|
+
sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
1693
|
+
return false;
|
|
1694
|
+
} finally {
|
|
1695
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1696
|
+
}
|
|
1697
|
+
if (!owner || !expected || !sha256) return false;
|
|
1698
|
+
const probe = probeSessionStorageLockOwner(owner.pid);
|
|
1699
|
+
const stale =
|
|
1700
|
+
probe.kind === "absent" ||
|
|
1701
|
+
(probe.kind === "live" &&
|
|
1702
|
+
owner.incarnation !== undefined &&
|
|
1703
|
+
probe.incarnation !== undefined &&
|
|
1704
|
+
probe.incarnation !== owner.incarnation);
|
|
1705
|
+
if (!stale) return false;
|
|
1706
|
+
let lockExpected = expected;
|
|
1707
|
+
const stagedPath = expected.nlink === 2n ? `${lockPath}.${owner.token}.owner.tmp` : undefined;
|
|
1708
|
+
if (stagedPath) {
|
|
1709
|
+
try {
|
|
1710
|
+
const staged = fs.lstatSync(stagedPath, { bigint: true });
|
|
1711
|
+
if (staged.dev !== expected.dev || staged.ino !== expected.ino || staged.isSymbolicLink()) return false;
|
|
1712
|
+
unlinkOwnedStagedSync(stagedPath, expected);
|
|
1713
|
+
if (fs.existsSync(stagedPath)) return false;
|
|
1714
|
+
lockExpected = fs.lstatSync(lockPath, { bigint: true });
|
|
1715
|
+
if (
|
|
1716
|
+
lockExpected.dev !== expected.dev ||
|
|
1717
|
+
lockExpected.ino !== expected.ino ||
|
|
1718
|
+
lockExpected.nlink !== 1n ||
|
|
1719
|
+
lockExpected.size !== expected.size ||
|
|
1720
|
+
lockExpected.mtimeNs !== expected.mtimeNs ||
|
|
1721
|
+
lockExpected.isSymbolicLink()
|
|
1722
|
+
)
|
|
1723
|
+
return false;
|
|
1724
|
+
} catch {
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return exactRemoveSessionStorageLockPath(lockPath, lockExpected, sha256);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1384
1731
|
export class FileSessionStorage implements SessionStorage {
|
|
1732
|
+
acquireExclusiveLockSync(
|
|
1733
|
+
lockPath: string,
|
|
1734
|
+
options?: { securityContext?: SessionStorageSecurityContext },
|
|
1735
|
+
): SessionStorageExclusiveLock | undefined {
|
|
1736
|
+
const dir = path.dirname(lockPath);
|
|
1737
|
+
this.ensureDirSync(dir);
|
|
1738
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1739
|
+
const token = randomUUID();
|
|
1740
|
+
const stagedPath = `${lockPath}.${token}.owner.tmp`;
|
|
1741
|
+
let fd: number | undefined;
|
|
1742
|
+
let stagedIdentity: fs.BigIntStats | undefined;
|
|
1743
|
+
let ownerDigest: string | undefined;
|
|
1744
|
+
let published = false;
|
|
1745
|
+
try {
|
|
1746
|
+
fd = fs.openSync(
|
|
1747
|
+
stagedPath,
|
|
1748
|
+
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW ?? 0),
|
|
1749
|
+
0o600,
|
|
1750
|
+
);
|
|
1751
|
+
secureOwnerOnlyFileDescriptor(stagedPath, fd, "apply", options?.securityContext);
|
|
1752
|
+
stagedIdentity = fs.fstatSync(fd, { bigint: true });
|
|
1753
|
+
const probe = probeSessionStorageLockOwner(process.pid);
|
|
1754
|
+
const owner: SessionStorageLockOwner = {
|
|
1755
|
+
pid: process.pid,
|
|
1756
|
+
...(probe.kind === "live" && probe.incarnation ? { incarnation: probe.incarnation } : {}),
|
|
1757
|
+
token,
|
|
1758
|
+
};
|
|
1759
|
+
const bytes = Buffer.from(`${JSON.stringify(owner)}\n`, "utf8");
|
|
1760
|
+
ownerDigest = createHash("sha256").update(bytes).digest("hex");
|
|
1761
|
+
let offset = 0;
|
|
1762
|
+
while (offset < bytes.byteLength) {
|
|
1763
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
1764
|
+
if (written === 0) throw new Error("Short write");
|
|
1765
|
+
offset += written;
|
|
1766
|
+
}
|
|
1767
|
+
fs.fsyncSync(fd);
|
|
1768
|
+
secureOwnerOnlyFileDescriptor(stagedPath, fd, "verify", options?.securityContext);
|
|
1769
|
+
stagedIdentity = fs.fstatSync(fd, { bigint: true });
|
|
1770
|
+
let outcome = classifyNativePublishOutcome(
|
|
1771
|
+
nativeSessionStorage().renameNoReplacePath(stagedPath, lockPath),
|
|
1772
|
+
);
|
|
1773
|
+
let linkPublished = false;
|
|
1774
|
+
if (renameFlagsUnsupported(outcome)) {
|
|
1775
|
+
outcome = classifyNativePublishOutcome(nativeSessionStorage().linkNoReplacePath(stagedPath, lockPath));
|
|
1776
|
+
linkPublished = outcome.ok;
|
|
1777
|
+
}
|
|
1778
|
+
if (!outcome.ok) {
|
|
1779
|
+
fs.closeSync(fd);
|
|
1780
|
+
fd = undefined;
|
|
1781
|
+
exactRemoveSessionStorageLockPath(stagedPath, stagedIdentity, ownerDigest);
|
|
1782
|
+
if (outcome.reason === "destination_exists") {
|
|
1783
|
+
if (attempt === 0 && reclaimStaleSessionStorageLockSync(lockPath)) continue;
|
|
1784
|
+
return undefined;
|
|
1785
|
+
}
|
|
1786
|
+
throw new Error(`exclusive_lock_publish_rejected:${outcome.reason}`);
|
|
1787
|
+
}
|
|
1788
|
+
published = true;
|
|
1789
|
+
fs.closeSync(fd);
|
|
1790
|
+
fd = undefined;
|
|
1791
|
+
if (linkPublished) {
|
|
1792
|
+
const stagedNamed = fs.lstatSync(stagedPath, { bigint: true });
|
|
1793
|
+
if (
|
|
1794
|
+
stagedNamed.dev !== stagedIdentity.dev ||
|
|
1795
|
+
stagedNamed.ino !== stagedIdentity.ino ||
|
|
1796
|
+
stagedNamed.isSymbolicLink()
|
|
1797
|
+
)
|
|
1798
|
+
throw new Error("exclusive_lock_staging_identity_changed");
|
|
1799
|
+
unlinkOwnedStagedSync(stagedPath, stagedIdentity);
|
|
1800
|
+
if (fs.existsSync(stagedPath)) throw new Error("exclusive_lock_staging_cleanup_failed");
|
|
1801
|
+
}
|
|
1802
|
+
const publishedFd = fs.openSync(lockPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
1803
|
+
let expected: fs.BigIntStats;
|
|
1804
|
+
try {
|
|
1805
|
+
secureOwnerOnlyFileDescriptor(lockPath, publishedFd, "verify", options?.securityContext);
|
|
1806
|
+
expected = fs.fstatSync(publishedFd, { bigint: true });
|
|
1807
|
+
if (
|
|
1808
|
+
!expected.isFile() ||
|
|
1809
|
+
expected.isSymbolicLink() ||
|
|
1810
|
+
expected.dev !== stagedIdentity.dev ||
|
|
1811
|
+
expected.ino !== stagedIdentity.ino ||
|
|
1812
|
+
expected.nlink !== 1n ||
|
|
1813
|
+
expected.size !== BigInt(bytes.byteLength)
|
|
1814
|
+
)
|
|
1815
|
+
throw new Error("exclusive_lock_identity_changed");
|
|
1816
|
+
const installed = Buffer.allocUnsafe(bytes.byteLength);
|
|
1817
|
+
let readOffset = 0;
|
|
1818
|
+
while (readOffset < installed.byteLength) {
|
|
1819
|
+
const read = fs.readSync(
|
|
1820
|
+
publishedFd,
|
|
1821
|
+
installed,
|
|
1822
|
+
readOffset,
|
|
1823
|
+
installed.byteLength - readOffset,
|
|
1824
|
+
readOffset,
|
|
1825
|
+
);
|
|
1826
|
+
if (read === 0) throw new Error("exclusive_lock_short_read");
|
|
1827
|
+
readOffset += read;
|
|
1828
|
+
}
|
|
1829
|
+
if (createHash("sha256").update(installed).digest("hex") !== ownerDigest)
|
|
1830
|
+
throw new Error("exclusive_lock_content_changed");
|
|
1831
|
+
} finally {
|
|
1832
|
+
fs.closeSync(publishedFd);
|
|
1833
|
+
}
|
|
1834
|
+
fsyncDirectorySync(dir);
|
|
1835
|
+
let released = false;
|
|
1836
|
+
return {
|
|
1837
|
+
releaseSync: () => {
|
|
1838
|
+
if (released) return;
|
|
1839
|
+
if (!exactRemoveSessionStorageLockPath(lockPath, expected, ownerDigest!)) {
|
|
1840
|
+
try {
|
|
1841
|
+
const current = fs.lstatSync(lockPath, { bigint: true });
|
|
1842
|
+
if (current.dev === expected.dev && current.ino === expected.ino) {
|
|
1843
|
+
// The inode matches, but exactRemove refused — either the
|
|
1844
|
+
// content changed (replacement reused the inode) or cleanup
|
|
1845
|
+
// genuinely failed on the original. Re-verify the content
|
|
1846
|
+
// digest to distinguish: a replacement whose sha256 no longer
|
|
1847
|
+
// matches is not ours and must not be unlinked.
|
|
1848
|
+
let fd: number | undefined;
|
|
1849
|
+
try {
|
|
1850
|
+
fd = fs.openSync(lockPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
1851
|
+
const bytes = Buffer.allocUnsafe(Number(current.size));
|
|
1852
|
+
let offset = 0;
|
|
1853
|
+
while (offset < bytes.byteLength) {
|
|
1854
|
+
const read = fs.readSync(fd, bytes, offset, bytes.byteLength - offset, offset);
|
|
1855
|
+
if (read === 0) break;
|
|
1856
|
+
offset += read;
|
|
1857
|
+
}
|
|
1858
|
+
const currentDigest = createHash("sha256")
|
|
1859
|
+
.update(bytes.subarray(0, offset))
|
|
1860
|
+
.digest("hex");
|
|
1861
|
+
if (currentDigest === ownerDigest!) throw new Error("exclusive_lock_release_failed");
|
|
1862
|
+
} finally {
|
|
1863
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
released = true;
|
|
1871
|
+
},
|
|
1872
|
+
};
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1875
|
+
if (published && stagedIdentity && ownerDigest) {
|
|
1876
|
+
try {
|
|
1877
|
+
const named = fs.lstatSync(lockPath, { bigint: true });
|
|
1878
|
+
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino)
|
|
1879
|
+
exactRemoveSessionStorageLockPath(lockPath, named, ownerDigest);
|
|
1880
|
+
} catch {
|
|
1881
|
+
// The published name is absent or belongs to another owner.
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (stagedIdentity && ownerDigest) {
|
|
1885
|
+
try {
|
|
1886
|
+
const named = fs.lstatSync(stagedPath, { bigint: true });
|
|
1887
|
+
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino)
|
|
1888
|
+
exactRemoveOwnedSessionStorageLockPath(stagedPath, stagedIdentity);
|
|
1889
|
+
} catch {
|
|
1890
|
+
// The staged name was consumed or removed.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
throw error;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return undefined;
|
|
1897
|
+
}
|
|
1385
1898
|
ensureDirSync(dir: string): void {
|
|
1386
1899
|
if (!fs.existsSync(dir)) {
|
|
1387
1900
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1624,6 +2137,12 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1624
2137
|
openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter {
|
|
1625
2138
|
return new FileSessionStorageWriter(path, options);
|
|
1626
2139
|
}
|
|
2140
|
+
openBufferedWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageBufferedWriter {
|
|
2141
|
+
return new FileSessionStorageWriter(path, {
|
|
2142
|
+
...options,
|
|
2143
|
+
bufferSize: options?.bufferSize ?? SESSION_STORAGE_BUFFERED_WRITER_DEFAULT_BYTES,
|
|
2144
|
+
}) as SessionStorageBufferedWriter;
|
|
2145
|
+
}
|
|
1627
2146
|
|
|
1628
2147
|
openStagedWriter(path: string, options?: SessionStorageWriterOpenOptions): StagedStreamingWriter {
|
|
1629
2148
|
return new FileStagedStreamingWriter(path, options);
|
|
@@ -2435,7 +2954,7 @@ class MemoryStagedStreamingWriter implements StagedStreamingWriter {
|
|
|
2435
2954
|
}
|
|
2436
2955
|
}
|
|
2437
2956
|
|
|
2438
|
-
class MemorySessionStorageWriter implements
|
|
2957
|
+
class MemorySessionStorageWriter implements SessionStorageBufferedWriter {
|
|
2439
2958
|
#storage: MemorySessionStorage;
|
|
2440
2959
|
#path: string;
|
|
2441
2960
|
#closeState: SessionStorageWriterCloseState = "open";
|
|
@@ -2446,12 +2965,22 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
2446
2965
|
#closeAdapter: SessionStorageWriterCloseAdapter | undefined;
|
|
2447
2966
|
#bytes: Buffer;
|
|
2448
2967
|
#length = 0;
|
|
2968
|
+
#bufferSize: number | undefined;
|
|
2969
|
+
#buffer: Buffer | undefined;
|
|
2970
|
+
#bufferedBytes = 0;
|
|
2971
|
+
#bytesSubmitted = 0;
|
|
2972
|
+
#bytesWritten = 0;
|
|
2973
|
+
#writeCalls = 0;
|
|
2974
|
+
#flushCalls = 0;
|
|
2449
2975
|
|
|
2450
2976
|
constructor(storage: MemorySessionStorage, path: string, options?: SessionStorageWriterOpenOptions) {
|
|
2451
2977
|
this.#storage = storage;
|
|
2452
2978
|
this.#path = path;
|
|
2453
2979
|
this.#onError = options?.onError;
|
|
2454
2980
|
this.#closeAdapter = options?.closeAdapter;
|
|
2981
|
+
this.#bufferSize =
|
|
2982
|
+
options?.bufferSize === undefined ? undefined : normalizeBufferedWriterCapacity(options.bufferSize);
|
|
2983
|
+
this.#buffer = this.#bufferSize === undefined ? undefined : Buffer.allocUnsafe(this.#bufferSize);
|
|
2455
2984
|
const existing =
|
|
2456
2985
|
options?.flags === "w" || !storage.existsSync(path)
|
|
2457
2986
|
? Buffer.alloc(0)
|
|
@@ -2469,40 +2998,127 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
2469
2998
|
return error;
|
|
2470
2999
|
}
|
|
2471
3000
|
|
|
2472
|
-
|
|
2473
|
-
|
|
3001
|
+
#nonOpenWriteError(): Error {
|
|
3002
|
+
switch (this.#closeState) {
|
|
3003
|
+
case "closed":
|
|
3004
|
+
return new Error("Writer closed");
|
|
3005
|
+
case "close_unknown":
|
|
3006
|
+
return this.#closeError ?? new Error("Writer close outcome is unknown; descriptor quarantined");
|
|
3007
|
+
case "close_failed_retryable":
|
|
3008
|
+
return this.#closeError ?? new Error("Writer close failed before dispatch (retryable); writes rejected");
|
|
3009
|
+
default:
|
|
3010
|
+
return new Error("Writer closed");
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
#assertOpen(): void {
|
|
3015
|
+
if (this.#closeState !== "open") throw this.#nonOpenWriteError();
|
|
2474
3016
|
if (this.#error) throw this.#error;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
#asBuffer(bytes: Uint8Array): Buffer {
|
|
3020
|
+
if (Buffer.isBuffer(bytes)) return bytes;
|
|
3021
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
#appendToBytes(bytes: Uint8Array): void {
|
|
3025
|
+
const source = this.#asBuffer(bytes);
|
|
3026
|
+
const nextLength = this.#length + source.byteLength;
|
|
3027
|
+
if (nextLength > this.#bytes.byteLength) {
|
|
3028
|
+
const expanded = Buffer.allocUnsafe(Math.max(nextLength, this.#bytes.byteLength * 2));
|
|
3029
|
+
this.#bytes.copy(expanded, 0, 0, this.#length);
|
|
3030
|
+
this.#bytes = expanded;
|
|
3031
|
+
}
|
|
3032
|
+
source.copy(this.#bytes, this.#length);
|
|
3033
|
+
this.#length = nextLength;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
#writeCurrent(bytesWritten: number): void {
|
|
3037
|
+
this.#writeCalls++;
|
|
3038
|
+
this.#storage.writeBytesOwnedSync(this.#path, Buffer.from(this.#bytes.subarray(0, this.#length)));
|
|
3039
|
+
this.#bytesWritten += bytesWritten;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
#flushPending(): void {
|
|
3043
|
+
this.#flushCalls++;
|
|
3044
|
+
if (!this.#buffer || this.#bufferedBytes === 0) return;
|
|
3045
|
+
const pendingBytes = this.#bufferedBytes;
|
|
3046
|
+
const nextLength = this.#length + pendingBytes;
|
|
3047
|
+
if (nextLength > this.#bytes.byteLength) {
|
|
3048
|
+
const expanded = Buffer.allocUnsafe(Math.max(nextLength, this.#bytes.byteLength * 2));
|
|
3049
|
+
this.#bytes.copy(expanded, 0, 0, this.#length);
|
|
3050
|
+
this.#bytes = expanded;
|
|
3051
|
+
}
|
|
3052
|
+
this.#buffer.copy(this.#bytes, this.#length, 0, pendingBytes);
|
|
3053
|
+
this.#writeCalls++;
|
|
3054
|
+
this.#storage.writeBytesOwnedSync(this.#path, Buffer.from(this.#bytes.subarray(0, nextLength)));
|
|
3055
|
+
this.#length = nextLength;
|
|
3056
|
+
this.#bufferedBytes = 0;
|
|
3057
|
+
this.#bytesWritten += pendingBytes;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
#appendBytes(bytes: Uint8Array): void {
|
|
3061
|
+
if (bytes.byteLength === 0) return;
|
|
3062
|
+
if (!this.#buffer || !this.#bufferSize) {
|
|
3063
|
+
this.#appendToBytes(bytes);
|
|
3064
|
+
this.#writeCurrent(bytes.byteLength);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
const source = this.#asBuffer(bytes);
|
|
3068
|
+
if (source.byteLength >= this.#bufferSize) {
|
|
3069
|
+
this.#flushPending();
|
|
3070
|
+
this.#appendToBytes(source);
|
|
3071
|
+
this.#writeCurrent(source.byteLength);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (this.#bufferedBytes + source.byteLength > this.#bufferSize) this.#flushPending();
|
|
3075
|
+
source.copy(this.#buffer, this.#bufferedBytes);
|
|
3076
|
+
this.#bufferedBytes += source.byteLength;
|
|
3077
|
+
if (this.#bufferedBytes === this.#bufferSize) this.#flushPending();
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
writeBytesSync(bytes: Uint8Array): void {
|
|
3081
|
+
this.#assertOpen();
|
|
2475
3082
|
try {
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
if (nextLength > this.#bytes.byteLength) {
|
|
2479
|
-
const expanded = Buffer.allocUnsafe(Math.max(nextLength, this.#bytes.byteLength * 2));
|
|
2480
|
-
this.#bytes.copy(expanded, 0, 0, this.#length);
|
|
2481
|
-
this.#bytes = expanded;
|
|
2482
|
-
}
|
|
2483
|
-
bytes.copy(this.#bytes, this.#length);
|
|
2484
|
-
this.#length = nextLength;
|
|
2485
|
-
this.#storage.writeBytesOwnedSync(this.#path, this.#bytes.subarray(0, this.#length));
|
|
3083
|
+
this.#appendBytes(bytes);
|
|
3084
|
+
this.#bytesSubmitted += bytes.byteLength;
|
|
2486
3085
|
} catch (err) {
|
|
2487
3086
|
throw this.#recordError(err);
|
|
2488
3087
|
}
|
|
2489
3088
|
}
|
|
2490
3089
|
|
|
3090
|
+
writeLineSync(line: string): void {
|
|
3091
|
+
this.writeBytesSync(Buffer.from(line, "utf8"));
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2491
3094
|
async writeLine(line: string): Promise<void> {
|
|
2492
3095
|
this.writeLineSync(line);
|
|
2493
3096
|
}
|
|
2494
3097
|
|
|
3098
|
+
flushSync(): void {
|
|
3099
|
+
this.#assertOpen();
|
|
3100
|
+
try {
|
|
3101
|
+
this.#flushPending();
|
|
3102
|
+
} catch (err) {
|
|
3103
|
+
throw this.#recordError(err);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2495
3107
|
async flush(): Promise<void> {
|
|
2496
|
-
|
|
2497
|
-
if (this.#error) throw this.#error;
|
|
3108
|
+
this.flushSync();
|
|
2498
3109
|
}
|
|
3110
|
+
|
|
2499
3111
|
fsyncSync(): void {
|
|
2500
|
-
|
|
2501
|
-
|
|
3112
|
+
this.#assertOpen();
|
|
3113
|
+
try {
|
|
3114
|
+
this.#flushPending();
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
throw this.#recordError(err);
|
|
3117
|
+
}
|
|
2502
3118
|
}
|
|
3119
|
+
|
|
2503
3120
|
statSync(): SessionStorageStat {
|
|
2504
|
-
|
|
2505
|
-
if (this.#error) throw this.#error;
|
|
3121
|
+
this.#assertOpen();
|
|
2506
3122
|
return this.#storage.statSync(this.#path);
|
|
2507
3123
|
}
|
|
2508
3124
|
|
|
@@ -2517,6 +3133,15 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
2517
3133
|
// succeeds. The sentinel fd (-1) signals "no real descriptor".
|
|
2518
3134
|
if (this.#closeState === "closed") return;
|
|
2519
3135
|
if (this.#closeState === "close_unknown") throw this.#closeError!;
|
|
3136
|
+
|
|
3137
|
+
let flushError: Error | undefined;
|
|
3138
|
+
if (!this.#error) {
|
|
3139
|
+
try {
|
|
3140
|
+
this.#flushPending();
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
flushError = this.#recordError(err);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2520
3145
|
if (this.#closeAdapter) {
|
|
2521
3146
|
try {
|
|
2522
3147
|
this.#closeAdapter.close(-1);
|
|
@@ -2533,6 +3158,8 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
2533
3158
|
}
|
|
2534
3159
|
this.#closeState = "closed";
|
|
2535
3160
|
this.#closeError = undefined;
|
|
3161
|
+
this.#bufferedBytes = 0;
|
|
3162
|
+
if (flushError) throw flushError;
|
|
2536
3163
|
}
|
|
2537
3164
|
|
|
2538
3165
|
async close(): Promise<void> {
|
|
@@ -2550,11 +3177,41 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
2550
3177
|
getCloseError(): Error | undefined {
|
|
2551
3178
|
return this.#closeError;
|
|
2552
3179
|
}
|
|
3180
|
+
|
|
3181
|
+
getInstrumentation(): SessionStorageBufferedWriterInstrumentation {
|
|
3182
|
+
return {
|
|
3183
|
+
bytesSubmitted: this.#bytesSubmitted,
|
|
3184
|
+
bytesWritten: this.#bytesWritten,
|
|
3185
|
+
writeCalls: this.#writeCalls,
|
|
3186
|
+
flushCalls: this.#flushCalls,
|
|
3187
|
+
bufferedBytes: this.#bufferedBytes,
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
2553
3190
|
}
|
|
2554
3191
|
|
|
2555
3192
|
export class MemorySessionStorage implements SessionStorage {
|
|
2556
3193
|
#files = new Map<string, { content: Buffer; mtimeMs: number; ino: bigint }>();
|
|
2557
3194
|
#nextInode = 1n;
|
|
3195
|
+
acquireExclusiveLockSync(
|
|
3196
|
+
lockPath: string,
|
|
3197
|
+
_options?: { securityContext?: SessionStorageSecurityContext },
|
|
3198
|
+
): SessionStorageExclusiveLock | undefined {
|
|
3199
|
+
if (this.#files.has(lockPath)) return undefined;
|
|
3200
|
+
const ino = this.#nextInode++;
|
|
3201
|
+
this.#files.set(lockPath, {
|
|
3202
|
+
content: Buffer.from(`${process.pid}:${randomUUID()}\n`, "utf8"),
|
|
3203
|
+
mtimeMs: Date.now(),
|
|
3204
|
+
ino,
|
|
3205
|
+
});
|
|
3206
|
+
let released = false;
|
|
3207
|
+
return {
|
|
3208
|
+
releaseSync: () => {
|
|
3209
|
+
if (released) return;
|
|
3210
|
+
released = true;
|
|
3211
|
+
if (this.#files.get(lockPath)?.ino === ino) this.#files.delete(lockPath);
|
|
3212
|
+
},
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
2558
3215
|
|
|
2559
3216
|
#statFor(entry: { content: Buffer; mtimeMs: number; ino: bigint }): SessionStorageStat {
|
|
2560
3217
|
return {
|
|
@@ -2719,8 +3376,12 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
2719
3376
|
|
|
2720
3377
|
deleteSessionWithArtifacts(sessionPath: string): Promise<void> {
|
|
2721
3378
|
this.#files.delete(sessionPath);
|
|
3379
|
+
const artifactPrefix = sessionPath.endsWith(".jsonl") ? `${sessionPath.slice(0, -6)}/` : `${sessionPath}/`;
|
|
2722
3380
|
for (const candidate of [...this.#files.keys()]) {
|
|
2723
|
-
if (
|
|
3381
|
+
if (
|
|
3382
|
+
(candidate.startsWith(`${sessionPath}.spill.`) || candidate.startsWith(artifactPrefix)) &&
|
|
3383
|
+
isDerivedSessionMemoryFile(candidate)
|
|
3384
|
+
)
|
|
2724
3385
|
this.#files.delete(candidate);
|
|
2725
3386
|
}
|
|
2726
3387
|
return Promise.resolve();
|
|
@@ -2792,6 +3453,12 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
2792
3453
|
openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter {
|
|
2793
3454
|
return new MemorySessionStorageWriter(this, path, options);
|
|
2794
3455
|
}
|
|
3456
|
+
openBufferedWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageBufferedWriter {
|
|
3457
|
+
return new MemorySessionStorageWriter(this, path, {
|
|
3458
|
+
...options,
|
|
3459
|
+
bufferSize: options?.bufferSize ?? SESSION_STORAGE_BUFFERED_WRITER_DEFAULT_BYTES,
|
|
3460
|
+
}) as SessionStorageBufferedWriter;
|
|
3461
|
+
}
|
|
2795
3462
|
|
|
2796
3463
|
openStagedWriter(path: string): StagedStreamingWriter {
|
|
2797
3464
|
return new MemoryStagedStreamingWriter(this, path);
|