@ixo/oracle-runtime-workers 0.13.0 → 0.15.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/README.md +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
package/src/sqlite/do-vfs.ts
CHANGED
|
@@ -101,6 +101,7 @@ import {
|
|
|
101
101
|
bitmapHas,
|
|
102
102
|
FULL_MASK,
|
|
103
103
|
PageTier,
|
|
104
|
+
PageTierError,
|
|
104
105
|
resolvePageTierOptions,
|
|
105
106
|
SEGMENT_BYTES,
|
|
106
107
|
SEGMENT_CHUNKS,
|
|
@@ -109,6 +110,7 @@ import {
|
|
|
109
110
|
type PageTierOptions,
|
|
110
111
|
type PageTierStats,
|
|
111
112
|
type SegmentEntry,
|
|
113
|
+
type SweepStep,
|
|
112
114
|
} from './page-tier';
|
|
113
115
|
|
|
114
116
|
export { CHUNK_SIZE, PAGES_PER_CHUNK, VFS_PAGE_SIZE } from './chunk-layout';
|
|
@@ -142,6 +144,13 @@ export const IMPORT_FILE_SUFFIX = '.importing';
|
|
|
142
144
|
|
|
143
145
|
const SQLITE_HEADER_MAGIC = 'SQLite format 3\0';
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Times a cold-miss resolve re-fetches a segment whose map entry changed
|
|
149
|
+
* while its GET was in flight. One eviction pass rewrites a segment once,
|
|
150
|
+
* so a second change in a row means something keeps rewriting it.
|
|
151
|
+
*/
|
|
152
|
+
const SEGMENT_REFETCH_LIMIT = 4;
|
|
153
|
+
|
|
145
154
|
export interface DoVfsOptions {
|
|
146
155
|
/**
|
|
147
156
|
* Max clean pages held in the LRU cache (dirty pages are never evicted).
|
|
@@ -166,7 +175,39 @@ export interface TierFlushResult {
|
|
|
166
175
|
/** Hot rows / bytes after the pass. */
|
|
167
176
|
hotRows: number;
|
|
168
177
|
hotBytes: number;
|
|
169
|
-
skipped?:
|
|
178
|
+
skipped?: TierSkip;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type TierSkip =
|
|
182
|
+
| 'no-tier'
|
|
183
|
+
| 'snapshot-open'
|
|
184
|
+
| 'write-transaction'
|
|
185
|
+
| 'in-progress';
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Runs a synchronous step while nothing else uses the file's connection
|
|
189
|
+
* (see `DoVfs.tierFlush`); resolves with the step's result.
|
|
190
|
+
*/
|
|
191
|
+
export type Exclusive = <T>(step: () => T) => Promise<T>;
|
|
192
|
+
|
|
193
|
+
/** For direct VFS callers that issue no statements concurrently (tests). */
|
|
194
|
+
const runNow: Exclusive = async (step) => step();
|
|
195
|
+
|
|
196
|
+
/** What one eviction pass set out to do. */
|
|
197
|
+
interface TierPassPlan {
|
|
198
|
+
/** The open file (its flushes are tracked in `tierWrittenSince`), if open. */
|
|
199
|
+
file: PersistentFile | undefined;
|
|
200
|
+
totalChunks: number;
|
|
201
|
+
/** Hot rows when the pass planned. */
|
|
202
|
+
hotRows: number;
|
|
203
|
+
/** Eviction candidates (chunk numbers) per segment. */
|
|
204
|
+
bySegment: Map<number, number[]>;
|
|
205
|
+
/** Segments this pass rewrites (capped). */
|
|
206
|
+
todo: number[];
|
|
207
|
+
/** Segments with candidates, rewritten by this pass or a later one. */
|
|
208
|
+
segments: number;
|
|
209
|
+
/** `DoVfs.layoutEpoch` when the pass planned. */
|
|
210
|
+
epoch: number;
|
|
170
211
|
}
|
|
171
212
|
|
|
172
213
|
export interface TierStatus extends PageTierStats {
|
|
@@ -445,8 +486,28 @@ export class DoVfs implements SQLiteVFS {
|
|
|
445
486
|
private pinnedBytes = 0;
|
|
446
487
|
/** Statement/transaction retries the wrapper ran for cold misses (reported in `tierStatus`). */
|
|
447
488
|
missRetries = 0;
|
|
448
|
-
/**
|
|
449
|
-
|
|
489
|
+
/**
|
|
490
|
+
* The tier pass in flight (one at a time per VFS), as an ownership token:
|
|
491
|
+
* only the pass that set it clears it, so a pass of an instance replaced
|
|
492
|
+
* by `attach()` cannot clear the flag of a newer one.
|
|
493
|
+
*/
|
|
494
|
+
private tierPass: object | null = null;
|
|
495
|
+
/**
|
|
496
|
+
* The orphan sweep in flight (token as above). A sweep deletes every
|
|
497
|
+
* object the map does not reference — which includes one a pass uploaded
|
|
498
|
+
* but has not committed yet — so no pass starts while it runs and it
|
|
499
|
+
* never runs while another pass is in flight.
|
|
500
|
+
*/
|
|
501
|
+
private tierSweep: object | null = null;
|
|
502
|
+
/** `exportFile`/`fileChecksum` reads in flight: no R2 object is deleted meanwhile. */
|
|
503
|
+
private windowReaders = 0;
|
|
504
|
+
/**
|
|
505
|
+
* Bumped by everything that rewrites chunk rows or the tier map other
|
|
506
|
+
* than a pass's own commit and an ordinary flush: a truncation, import,
|
|
507
|
+
* rename, delete, materialize, re-attach. A pass that sees it move
|
|
508
|
+
* between reading a segment and committing it gives up (see `tierFlush`).
|
|
509
|
+
*/
|
|
510
|
+
private layoutEpoch = 0;
|
|
450
511
|
|
|
451
512
|
constructor(
|
|
452
513
|
readonly name: string,
|
|
@@ -509,7 +570,10 @@ export class DoVfs implements SQLiteVFS {
|
|
|
509
570
|
this.misses.clear();
|
|
510
571
|
this.pinned.clear();
|
|
511
572
|
this.pinnedBytes = 0;
|
|
512
|
-
this.
|
|
573
|
+
this.tierPass = null;
|
|
574
|
+
this.tierSweep = null;
|
|
575
|
+
this.windowReaders = 0;
|
|
576
|
+
this.layoutEpoch++;
|
|
513
577
|
this.lastFlushError = undefined;
|
|
514
578
|
this.tier?.attach(storage);
|
|
515
579
|
}
|
|
@@ -876,12 +940,21 @@ export class DoVfs implements SQLiteVFS {
|
|
|
876
940
|
const out = new Uint8Array(size);
|
|
877
941
|
const totalChunks = Math.ceil(size / CHUNK_SIZE);
|
|
878
942
|
const segments = this.tier ? this.tier.segmentsOf(name) : new Map();
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
start
|
|
884
|
-
|
|
943
|
+
this.windowReaders++;
|
|
944
|
+
try {
|
|
945
|
+
for (
|
|
946
|
+
let start = 0;
|
|
947
|
+
start < totalChunks;
|
|
948
|
+
start += SNAPSHOT_WINDOW_CHUNKS
|
|
949
|
+
) {
|
|
950
|
+
const end = Math.min(totalChunks, start + SNAPSHOT_WINDOW_CHUNKS);
|
|
951
|
+
out.set(
|
|
952
|
+
await this.readWindow(name, size, start, end, undefined, segments),
|
|
953
|
+
start * CHUNK_SIZE,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
} finally {
|
|
957
|
+
this.windowReaders--;
|
|
885
958
|
}
|
|
886
959
|
return out;
|
|
887
960
|
}
|
|
@@ -935,6 +1008,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
935
1008
|
}
|
|
936
1009
|
if (rows.length > 0) this.upsertChunks(name, rows);
|
|
937
1010
|
});
|
|
1011
|
+
this.layoutEpoch++;
|
|
938
1012
|
this.stats.storageWrites++;
|
|
939
1013
|
}
|
|
940
1014
|
|
|
@@ -970,11 +1044,20 @@ export class DoVfs implements SQLiteVFS {
|
|
|
970
1044
|
const hash = createHash('sha256');
|
|
971
1045
|
const totalChunks = Math.ceil(size / CHUNK_SIZE);
|
|
972
1046
|
const segments = this.tier ? this.tier.segmentsOf(name) : new Map();
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1047
|
+
this.windowReaders++;
|
|
1048
|
+
try {
|
|
1049
|
+
for (
|
|
1050
|
+
let start = 0;
|
|
1051
|
+
start < totalChunks;
|
|
1052
|
+
start += SNAPSHOT_WINDOW_CHUNKS
|
|
1053
|
+
) {
|
|
1054
|
+
const end = Math.min(totalChunks, start + SNAPSHOT_WINDOW_CHUNKS);
|
|
1055
|
+
hash.update(
|
|
1056
|
+
await this.readWindow(name, size, start, end, undefined, segments),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
this.windowReaders--;
|
|
978
1061
|
}
|
|
979
1062
|
return hash.digest('hex');
|
|
980
1063
|
}
|
|
@@ -1233,6 +1316,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1233
1316
|
this.storage.sql.exec('DELETE FROM vfs2_files WHERE file = ?', staging);
|
|
1234
1317
|
this.stats.rowsWritten += chunkno + 1;
|
|
1235
1318
|
});
|
|
1319
|
+
this.layoutEpoch++;
|
|
1236
1320
|
this.stats.storageWrites++;
|
|
1237
1321
|
return total;
|
|
1238
1322
|
} catch (error) {
|
|
@@ -1280,6 +1364,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1280
1364
|
this.storage.sql.exec('DELETE FROM vfs2_files WHERE file = ?', from);
|
|
1281
1365
|
this.stats.rowsWritten += Math.ceil(meta.size / CHUNK_SIZE) + 1;
|
|
1282
1366
|
});
|
|
1367
|
+
this.layoutEpoch++;
|
|
1283
1368
|
this.stats.storageWrites++;
|
|
1284
1369
|
}
|
|
1285
1370
|
|
|
@@ -1450,12 +1535,16 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1450
1535
|
/**
|
|
1451
1536
|
* The chunk holding `chunkno`, from (in order) the dirty set, the clean
|
|
1452
1537
|
* cache, then storage. Returns a buffer that must be treated as read-only
|
|
1453
|
-
* unless it came from `dirty` — writers copy before mutating.
|
|
1454
|
-
* `
|
|
1538
|
+
* unless it came from `dirty` — writers copy before mutating. Only the
|
|
1539
|
+
* pages in `wanted` are guaranteed valid: a partial row (or partial dirty
|
|
1540
|
+
* chunk) whose mask covers them is returned as is. Throws
|
|
1541
|
+
* `ColdChunkMiss` when a wanted page lives only in R2 (see the file
|
|
1542
|
+
* header).
|
|
1455
1543
|
*/
|
|
1456
1544
|
private loadChunk(
|
|
1457
1545
|
file: PersistentFile,
|
|
1458
1546
|
chunkno: number,
|
|
1547
|
+
wanted: number = FULL_MASK,
|
|
1459
1548
|
): Uint8Array | undefined {
|
|
1460
1549
|
this.touch(file, chunkno);
|
|
1461
1550
|
const dirty = file.dirty.get(chunkno);
|
|
@@ -1477,12 +1566,18 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1477
1566
|
file.dirtyMask.set(chunkno, FULL_MASK);
|
|
1478
1567
|
return dirty;
|
|
1479
1568
|
}
|
|
1569
|
+
if ((wanted & ~mask) === 0) return dirty;
|
|
1480
1570
|
throw new ColdChunkMiss(file.name, chunkno);
|
|
1481
1571
|
}
|
|
1482
1572
|
const local = this.lookupLocal(file, chunkno);
|
|
1483
1573
|
if (local !== undefined && local.mask === FULL_MASK) return local.data;
|
|
1484
|
-
if (this.tier !== null && this.tier.slotInR2(file.name, chunkno))
|
|
1574
|
+
if (this.tier !== null && this.tier.slotInR2(file.name, chunkno)) {
|
|
1575
|
+
// A partial row holding every wanted page serves the read. It is not
|
|
1576
|
+
// cached: the clean cache only holds whole chunks.
|
|
1577
|
+
if (local !== undefined && (wanted & ~local.mask) === 0)
|
|
1578
|
+
return local.data;
|
|
1485
1579
|
throw new ColdChunkMiss(file.name, chunkno);
|
|
1580
|
+
}
|
|
1486
1581
|
if (local === undefined) return undefined; // sparse
|
|
1487
1582
|
// A partial row whose R2 slot is gone (truncated away): the rest is zeros.
|
|
1488
1583
|
this.cache.set(file.name, chunkno, local.data);
|
|
@@ -1530,7 +1625,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1530
1625
|
const chunkno = Math.floor(pos / CHUNK_SIZE);
|
|
1531
1626
|
const inChunk = pos - chunkno * CHUNK_SIZE;
|
|
1532
1627
|
const n = Math.min(CHUNK_SIZE - inChunk, available - done);
|
|
1533
|
-
const chunk = this.loadChunk(file, chunkno);
|
|
1628
|
+
const chunk = this.loadChunk(file, chunkno, pagesOf(inChunk, n));
|
|
1534
1629
|
if (chunk === undefined) {
|
|
1535
1630
|
pData.fill(0, done, done + n); // sparse region
|
|
1536
1631
|
} else {
|
|
@@ -1701,6 +1796,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1701
1796
|
this.stats.rowsWritten += 1;
|
|
1702
1797
|
});
|
|
1703
1798
|
file.gen += 1;
|
|
1799
|
+
if (truncateTo !== null) this.layoutEpoch++;
|
|
1704
1800
|
this.stats.storageWrites++;
|
|
1705
1801
|
this.stats.flushes++;
|
|
1706
1802
|
const pins = this.pinned.get(file.name);
|
|
@@ -1824,6 +1920,7 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1824
1920
|
this.storage.sql.exec('DELETE FROM vfs2_files WHERE file = ?', name);
|
|
1825
1921
|
this.tier?.dropFile(name);
|
|
1826
1922
|
});
|
|
1923
|
+
this.layoutEpoch++;
|
|
1827
1924
|
this.stats.storageWrites++;
|
|
1828
1925
|
}
|
|
1829
1926
|
|
|
@@ -1883,8 +1980,10 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1883
1980
|
for (let chunkno = first; chunkno <= last && !wanted; chunkno++)
|
|
1884
1981
|
wanted = !pins.has(chunkno) && tier.slotInR2(name, chunkno);
|
|
1885
1982
|
if (!wanted) continue;
|
|
1886
|
-
const seg = await
|
|
1983
|
+
const seg = await this.currentSegment(tier, name, segno);
|
|
1887
1984
|
fetched++;
|
|
1985
|
+
// Synchronous from here on: the map, the hot rows read below and
|
|
1986
|
+
// `seg` all describe the same moment.
|
|
1888
1987
|
const rows = this.storage.sql
|
|
1889
1988
|
.exec<ChunkRow>(
|
|
1890
1989
|
'SELECT chunkno, data, mask FROM vfs2_chunks WHERE file = ? AND chunkno >= ? AND chunkno <= ?',
|
|
@@ -1933,6 +2032,46 @@ export class DoVfs implements SQLiteVFS {
|
|
|
1933
2032
|
return fetched;
|
|
1934
2033
|
}
|
|
1935
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* The bytes of segment `segno` of `name` as the map references it when
|
|
2037
|
+
* this returns, or null when the map has no entry for it. An eviction
|
|
2038
|
+
* pass may rewrite the segment while the GET is in flight — a new
|
|
2039
|
+
* generation, and the hot rows it absorbed deleted — so a response for
|
|
2040
|
+
* a generation the map no longer points at is discarded and the current
|
|
2041
|
+
* one fetched (a pass committing in between can also delete the old
|
|
2042
|
+
* object, which then reads as missing).
|
|
2043
|
+
*/
|
|
2044
|
+
private async currentSegment(
|
|
2045
|
+
tier: PageTier,
|
|
2046
|
+
name: string,
|
|
2047
|
+
segno: number,
|
|
2048
|
+
): Promise<Uint8Array | null> {
|
|
2049
|
+
for (let attempt = 0; ; attempt++) {
|
|
2050
|
+
const mapped = tier.segmentsOf(name).get(segno);
|
|
2051
|
+
if (mapped === undefined) return null;
|
|
2052
|
+
const entry: SegmentEntry = { gen: mapped.gen, mask: mapped.mask };
|
|
2053
|
+
let bytes: Uint8Array | null;
|
|
2054
|
+
try {
|
|
2055
|
+
bytes = await tier.getSegmentAt(name, segno, entry);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
if (
|
|
2058
|
+
!(error instanceof PageTierError) ||
|
|
2059
|
+
sameSegmentEntry(tier.segmentsOf(name).get(segno), entry) ||
|
|
2060
|
+
attempt >= SEGMENT_REFETCH_LIMIT
|
|
2061
|
+
)
|
|
2062
|
+
throw error;
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (sameSegmentEntry(tier.segmentsOf(name).get(segno), entry))
|
|
2066
|
+
return bytes;
|
|
2067
|
+
if (attempt >= SEGMENT_REFETCH_LIMIT) {
|
|
2068
|
+
throw new DoVfsError(
|
|
2069
|
+
`segment ${segno} of '${name}' kept changing while it was fetched`,
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1936
2075
|
/**
|
|
1937
2076
|
* The retried statement/transaction is done: hand the pinned chunks to
|
|
1938
2077
|
* the LRU (the most recently used survive) and free the pin budget.
|
|
@@ -2004,12 +2143,25 @@ export class DoVfs implements SQLiteVFS {
|
|
|
2004
2143
|
/**
|
|
2005
2144
|
* One eviction pass over `name` (see page-tier.ts): rows nobody touched
|
|
2006
2145
|
* for `evictAfterPeriods` periods are written into their R2 segments and
|
|
2007
|
-
* deleted.
|
|
2008
|
-
*
|
|
2146
|
+
* deleted. Skips while a snapshot is open or a write transaction holds
|
|
2147
|
+
* the file. `force` evicts every clean row regardless of recency (tests,
|
|
2148
|
+
* ops).
|
|
2149
|
+
*
|
|
2150
|
+
* `exclusive` runs a synchronous step with no transaction or statement
|
|
2151
|
+
* of the file's connection in flight (`DoSqliteDatabase` passes its
|
|
2152
|
+
* connection mutex). The pass takes it to plan, to read a segment's rows
|
|
2153
|
+
* and to commit the segment — never across its R2 GET/PUT — so turns and
|
|
2154
|
+
* cold-miss resolves interleave with the uploads. A commit re-checks what
|
|
2155
|
+
* those may have changed meanwhile: rows a flush rewrote stay hot
|
|
2156
|
+
* (`tierWrittenSince`); a moved segment entry, a truncation, import,
|
|
2157
|
+
* rename or delete of the file (`layoutEpoch`), a re-opened file or a
|
|
2158
|
+
* snapshot opened in between end the pass without committing that
|
|
2159
|
+
* segment (its upload is queued for deletion; the next pass starts over).
|
|
2009
2160
|
*/
|
|
2010
2161
|
async tierFlush(
|
|
2011
2162
|
name: string,
|
|
2012
2163
|
opts: { force?: boolean; maxSegments?: number } = {},
|
|
2164
|
+
exclusive: Exclusive = runNow,
|
|
2013
2165
|
): Promise<TierFlushResult> {
|
|
2014
2166
|
const tier = this.tier;
|
|
2015
2167
|
const none = (skipped: TierFlushResult['skipped']): TierFlushResult => ({
|
|
@@ -2021,15 +2173,60 @@ export class DoVfs implements SQLiteVFS {
|
|
|
2021
2173
|
skipped,
|
|
2022
2174
|
});
|
|
2023
2175
|
if (tier === null) return none('no-tier');
|
|
2024
|
-
if (this.
|
|
2025
|
-
|
|
2176
|
+
if (this.tierPass !== null || this.tierSweep !== null)
|
|
2177
|
+
return none('in-progress');
|
|
2178
|
+
const pass = {};
|
|
2179
|
+
this.tierPass = pass;
|
|
2180
|
+
let plan: TierPassPlan | undefined;
|
|
2181
|
+
try {
|
|
2182
|
+
const start = await exclusive(() => this.planTierPass(tier, name, opts));
|
|
2183
|
+
if ('skipped' in start) return none(start.skipped);
|
|
2184
|
+
plan = start;
|
|
2185
|
+
let evicted = 0;
|
|
2186
|
+
let rewritten = 0;
|
|
2187
|
+
for (const segno of plan.todo) {
|
|
2188
|
+
const outcome = await this.evictSegment(
|
|
2189
|
+
tier,
|
|
2190
|
+
name,
|
|
2191
|
+
segno,
|
|
2192
|
+
plan,
|
|
2193
|
+
exclusive,
|
|
2194
|
+
);
|
|
2195
|
+
if (outcome === null) break;
|
|
2196
|
+
evicted += outcome;
|
|
2197
|
+
rewritten++;
|
|
2198
|
+
}
|
|
2199
|
+
tier.stats.evictedChunks += evicted;
|
|
2200
|
+
tier.stats.evictionPasses++;
|
|
2201
|
+
await this.runTierMaintenance(tier, 'if-due', true);
|
|
2202
|
+
const hotRows = Math.max(0, plan.hotRows - evicted);
|
|
2203
|
+
return {
|
|
2204
|
+
evictedChunks: evicted,
|
|
2205
|
+
segmentsRewritten: rewritten,
|
|
2206
|
+
remaining: plan.segments - rewritten,
|
|
2207
|
+
hotRows,
|
|
2208
|
+
hotBytes: hotRows * CHUNK_SIZE,
|
|
2209
|
+
};
|
|
2210
|
+
} finally {
|
|
2211
|
+
if (this.tierPass === pass) this.tierPass = null;
|
|
2212
|
+
if (plan?.file !== undefined) plan.file.tierWrittenSince = null;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/** What a pass will evict (synchronous; runs under `exclusive`). */
|
|
2217
|
+
private planTierPass(
|
|
2218
|
+
tier: PageTier,
|
|
2219
|
+
name: string,
|
|
2220
|
+
opts: { force?: boolean; maxSegments?: number },
|
|
2221
|
+
): TierPassPlan | { skipped: TierSkip } {
|
|
2222
|
+
if (this.snapshots.has(name)) return { skipped: 'snapshot-open' };
|
|
2026
2223
|
const file = this.persistentByName.get(name);
|
|
2027
2224
|
if (file !== undefined && file.lock >= SQLITE_LOCK_EXCLUSIVE)
|
|
2028
|
-
return
|
|
2225
|
+
return { skipped: 'write-transaction' };
|
|
2029
2226
|
this.ensureSchema();
|
|
2030
2227
|
if (file !== undefined) this.flush(file);
|
|
2031
2228
|
const size = file?.size ?? this.readStoredMeta(name)?.size;
|
|
2032
|
-
if (size === undefined) return
|
|
2229
|
+
if (size === undefined) return { skipped: 'no-tier' };
|
|
2033
2230
|
const totalChunks = Math.ceil(size / CHUNK_SIZE);
|
|
2034
2231
|
if (file !== undefined) this.persistTouches(file, tier);
|
|
2035
2232
|
const recent = tier.recentBitmap(
|
|
@@ -2058,110 +2255,141 @@ export class DoVfs implements SQLiteVFS {
|
|
|
2058
2255
|
if (list) list.push(row.chunkno);
|
|
2059
2256
|
else bySegment.set(segno, [row.chunkno]);
|
|
2060
2257
|
}
|
|
2061
|
-
const
|
|
2258
|
+
const segments = Array.from(bySegment.keys()).sort((a, b) => a - b);
|
|
2062
2259
|
const cap = opts.maxSegments ?? tier.options.maxSegmentsPerPass;
|
|
2063
|
-
const todo = plan.slice(0, cap);
|
|
2064
|
-
let evicted = 0;
|
|
2065
|
-
this.tierPassInFlight = true;
|
|
2066
2260
|
if (file !== undefined) file.tierWrittenSince = new Set();
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
);
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
`DELETE FROM vfs2_chunks WHERE file = ? AND chunkno IN (${evict.map(() => '?').join(', ')})`,
|
|
2137
|
-
name,
|
|
2138
|
-
...evict,
|
|
2139
|
-
);
|
|
2140
|
-
this.stats.rowsWritten += evict.length;
|
|
2141
|
-
}
|
|
2142
|
-
});
|
|
2143
|
-
this.stats.storageWrites++;
|
|
2144
|
-
if (entry !== undefined) {
|
|
2145
|
-
const oldKey = tier.key(name, segno, entry.gen);
|
|
2146
|
-
if (oldKey !== newKey) tier.queueDelete(oldKey);
|
|
2261
|
+
return {
|
|
2262
|
+
file,
|
|
2263
|
+
totalChunks,
|
|
2264
|
+
hotRows: hot.length,
|
|
2265
|
+
bySegment,
|
|
2266
|
+
todo: segments.slice(0, cap),
|
|
2267
|
+
segments: segments.length,
|
|
2268
|
+
epoch: this.layoutEpoch,
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/** Whether nothing a pass cannot account for happened to `name` since it planned. */
|
|
2273
|
+
private tierPassStillValid(name: string, plan: TierPassPlan): boolean {
|
|
2274
|
+
return (
|
|
2275
|
+
this.layoutEpoch === plan.epoch &&
|
|
2276
|
+
!this.snapshots.has(name) &&
|
|
2277
|
+
this.persistentByName.get(name) === plan.file &&
|
|
2278
|
+
(plan.file === undefined || plan.file.lock < SQLITE_LOCK_EXCLUSIVE)
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/**
|
|
2283
|
+
* Rewrite segment `segno` from its hot rows plus its previous object,
|
|
2284
|
+
* then (under `exclusive`) point the map at the new object and delete the
|
|
2285
|
+
* evicted rows in one storage transaction. Returns the chunks evicted, or
|
|
2286
|
+
* null when the file changed under the pass (nothing committed).
|
|
2287
|
+
*/
|
|
2288
|
+
private async evictSegment(
|
|
2289
|
+
tier: PageTier,
|
|
2290
|
+
name: string,
|
|
2291
|
+
segno: number,
|
|
2292
|
+
plan: TierPassPlan,
|
|
2293
|
+
exclusive: Exclusive,
|
|
2294
|
+
): Promise<number | null> {
|
|
2295
|
+
const candidates = plan.bySegment.get(segno) ?? [];
|
|
2296
|
+
const first = segno * SEGMENT_CHUNKS;
|
|
2297
|
+
const last = Math.min(plan.totalChunks, first + SEGMENT_CHUNKS) - 1;
|
|
2298
|
+
const read = await exclusive(() => {
|
|
2299
|
+
if (!this.tierPassStillValid(name, plan)) return null;
|
|
2300
|
+
const mapped = tier.segmentsOf(name).get(segno);
|
|
2301
|
+
const rows = this.storage.sql
|
|
2302
|
+
.exec<ChunkRow>(
|
|
2303
|
+
'SELECT chunkno, data, mask FROM vfs2_chunks WHERE file = ? AND chunkno >= ? AND chunkno <= ?',
|
|
2304
|
+
name,
|
|
2305
|
+
first,
|
|
2306
|
+
last,
|
|
2307
|
+
)
|
|
2308
|
+
.toArray();
|
|
2309
|
+
this.stats.storageReads++;
|
|
2310
|
+
this.stats.rowsRead += rows.length;
|
|
2311
|
+
const byChunk = new Map<number, ChunkRow>();
|
|
2312
|
+
for (const row of rows) byChunk.set(row.chunkno, row);
|
|
2313
|
+
const entry: SegmentEntry | undefined =
|
|
2314
|
+
mapped === undefined
|
|
2315
|
+
? undefined
|
|
2316
|
+
: { gen: mapped.gen, mask: mapped.mask };
|
|
2317
|
+
return { entry, byChunk };
|
|
2318
|
+
});
|
|
2319
|
+
if (read === null) return null;
|
|
2320
|
+
const { entry, byChunk } = read;
|
|
2321
|
+
// The old object is needed for every slot a full hot row does not cover.
|
|
2322
|
+
let needOld = false;
|
|
2323
|
+
if (entry !== undefined && entry.mask !== 0) {
|
|
2324
|
+
for (let chunkno = first; chunkno <= last; chunkno++) {
|
|
2325
|
+
const row = byChunk.get(chunkno);
|
|
2326
|
+
const inR2 = (entry.mask & (1 << slotOf(chunkno))) !== 0;
|
|
2327
|
+
if (inR2 && (row === undefined || row.mask !== FULL_MASK)) {
|
|
2328
|
+
needOld = true;
|
|
2329
|
+
break;
|
|
2147
2330
|
}
|
|
2148
|
-
evicted += evict.length;
|
|
2149
2331
|
}
|
|
2150
|
-
} finally {
|
|
2151
|
-
this.tierPassInFlight = false;
|
|
2152
|
-
if (file !== undefined) file.tierWrittenSince = null;
|
|
2153
2332
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2333
|
+
let old: Uint8Array | null = null;
|
|
2334
|
+
if (needOld && entry !== undefined) {
|
|
2335
|
+
try {
|
|
2336
|
+
old = await tier.getSegmentAt(name, segno, entry);
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
// An import, truncate or wipe meanwhile may have deleted it.
|
|
2339
|
+
if (!this.tierPassStillValid(name, plan)) return null;
|
|
2340
|
+
throw error;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
const seg = new Uint8Array(SEGMENT_BYTES);
|
|
2344
|
+
let mask = 0;
|
|
2345
|
+
for (let chunkno = first; chunkno <= last; chunkno++) {
|
|
2346
|
+
const slot = slotOf(chunkno);
|
|
2347
|
+
const at = slot * CHUNK_SIZE;
|
|
2348
|
+
const row = byChunk.get(chunkno);
|
|
2349
|
+
const inR2 = entry !== undefined && (entry.mask & (1 << slot)) !== 0;
|
|
2350
|
+
if (inR2 && old !== null) seg.set(old.subarray(at, at + CHUNK_SIZE), at);
|
|
2351
|
+
if (row !== undefined) {
|
|
2352
|
+
placeMasked(seg, at, CHUNK_SIZE, new Uint8Array(row.data), row.mask);
|
|
2353
|
+
mask |= 1 << slot;
|
|
2354
|
+
} else if (inR2) {
|
|
2355
|
+
mask |= 1 << slot;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
const gen = tier.allocateGen(name);
|
|
2359
|
+
const newKey = await tier.putSegment(name, segno, gen, seg);
|
|
2360
|
+
return exclusive(() => {
|
|
2361
|
+
if (
|
|
2362
|
+
!this.tierPassStillValid(name, plan) ||
|
|
2363
|
+
!sameSegmentEntry(tier.segmentsOf(name).get(segno), entry)
|
|
2364
|
+
) {
|
|
2365
|
+
tier.queueDelete(newKey);
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
// Rows a flush wrote while the object was in flight are newer than
|
|
2369
|
+
// what was uploaded: keep them hot.
|
|
2370
|
+
const written = plan.file?.tierWrittenSince;
|
|
2371
|
+
const evict = candidates.filter(
|
|
2372
|
+
(chunkno) =>
|
|
2373
|
+
written === null || written === undefined || !written.has(chunkno),
|
|
2374
|
+
);
|
|
2375
|
+
this.storage.transactionSync(() => {
|
|
2376
|
+
tier.setSegment(name, segno, { gen, mask });
|
|
2377
|
+
if (evict.length > 0) {
|
|
2378
|
+
this.storage.sql.exec(
|
|
2379
|
+
`DELETE FROM vfs2_chunks WHERE file = ? AND chunkno IN (${evict.map(() => '?').join(', ')})`,
|
|
2380
|
+
name,
|
|
2381
|
+
...evict,
|
|
2382
|
+
);
|
|
2383
|
+
this.stats.rowsWritten += evict.length;
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
this.stats.storageWrites++;
|
|
2387
|
+
if (entry !== undefined) {
|
|
2388
|
+
const oldKey = tier.key(name, segno, entry.gen);
|
|
2389
|
+
if (oldKey !== newKey) tier.queueDelete(oldKey);
|
|
2390
|
+
}
|
|
2391
|
+
return evict.length;
|
|
2392
|
+
});
|
|
2165
2393
|
}
|
|
2166
2394
|
|
|
2167
2395
|
/**
|
|
@@ -2223,17 +2451,58 @@ export class DoVfs implements SQLiteVFS {
|
|
|
2223
2451
|
chunks += upserts.length;
|
|
2224
2452
|
}
|
|
2225
2453
|
this.storage.transactionSync(() => tier.dropFile(name));
|
|
2454
|
+
this.layoutEpoch++;
|
|
2226
2455
|
this.cache.dropFile(name);
|
|
2227
|
-
|
|
2456
|
+
await this.runTierMaintenance(tier, 'if-due');
|
|
2228
2457
|
return { chunks };
|
|
2229
2458
|
}
|
|
2230
2459
|
|
|
2231
|
-
/**
|
|
2460
|
+
/**
|
|
2461
|
+
* Delete queued R2 objects, plus an orphan sweep: the whole prefix now
|
|
2462
|
+
* with `sweep`, else one bounded step when one is due (see
|
|
2463
|
+
* `PageTier.sweepDue`).
|
|
2464
|
+
*/
|
|
2232
2465
|
async tierMaintenance(
|
|
2233
2466
|
opts: { sweep?: boolean } = {},
|
|
2234
2467
|
): Promise<{ deleted: number }> {
|
|
2235
|
-
if (this.tier === null
|
|
2236
|
-
return this.
|
|
2468
|
+
if (this.tier === null) return { deleted: 0 };
|
|
2469
|
+
return this.runTierMaintenance(
|
|
2470
|
+
this.tier,
|
|
2471
|
+
opts.sweep === true ? 'full' : 'if-due',
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* The one way into `PageTier.maintenance`. Deletes nothing while a
|
|
2477
|
+
* snapshot or a whole-file read may still fetch an object the map has
|
|
2478
|
+
* moved away from. A sweep needs the tier to itself: never while a pass
|
|
2479
|
+
* other than the caller's own (`byOwnPass`) has uploads in flight, and
|
|
2480
|
+
* no pass starts while it runs.
|
|
2481
|
+
*/
|
|
2482
|
+
private async runTierMaintenance(
|
|
2483
|
+
tier: PageTier,
|
|
2484
|
+
sweep: 'if-due' | 'full',
|
|
2485
|
+
byOwnPass = false,
|
|
2486
|
+
): Promise<{ deleted: number }> {
|
|
2487
|
+
if (this.snapshots.size > 0 || this.windowReaders > 0)
|
|
2488
|
+
return { deleted: 0 };
|
|
2489
|
+
const alone =
|
|
2490
|
+
(byOwnPass || this.tierPass === null) && this.tierSweep === null;
|
|
2491
|
+
const step: SweepStep = !alone
|
|
2492
|
+
? 'none'
|
|
2493
|
+
: sweep === 'full'
|
|
2494
|
+
? 'full'
|
|
2495
|
+
: tier.sweepDue()
|
|
2496
|
+
? 'step'
|
|
2497
|
+
: 'none';
|
|
2498
|
+
if (step === 'none') return tier.maintenance();
|
|
2499
|
+
const token = {};
|
|
2500
|
+
this.tierSweep = token;
|
|
2501
|
+
try {
|
|
2502
|
+
return await tier.maintenance({ sweep: step });
|
|
2503
|
+
} finally {
|
|
2504
|
+
if (this.tierSweep === token) this.tierSweep = null;
|
|
2505
|
+
}
|
|
2237
2506
|
}
|
|
2238
2507
|
|
|
2239
2508
|
/** Delete every R2 object of this VFS (the working copy is being wiped). */
|
|
@@ -2244,6 +2513,24 @@ export class DoVfs implements SQLiteVFS {
|
|
|
2244
2513
|
}
|
|
2245
2514
|
}
|
|
2246
2515
|
|
|
2516
|
+
/** Mask of the pages of a chunk that bytes `[from, from + length)` of it touch. */
|
|
2517
|
+
function pagesOf(from: number, length: number): number {
|
|
2518
|
+
const first = Math.floor(from / VFS_PAGE_SIZE);
|
|
2519
|
+
const last = Math.floor((from + Math.max(1, length) - 1) / VFS_PAGE_SIZE);
|
|
2520
|
+
let mask = 0;
|
|
2521
|
+
for (let page = first; page <= last; page++) mask |= 1 << page;
|
|
2522
|
+
return mask;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/** Whether two map entries of a segment (absent = no object) are the same. */
|
|
2526
|
+
function sameSegmentEntry(
|
|
2527
|
+
a: SegmentEntry | undefined,
|
|
2528
|
+
b: SegmentEntry | undefined,
|
|
2529
|
+
): boolean {
|
|
2530
|
+
if (a === undefined || b === undefined) return a === b;
|
|
2531
|
+
return a.gen === b.gen && a.mask === b.mask;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2247
2534
|
/** Copy the pages of `mask` from `src` into `dst[offset..offset+n)` (page-aligned buffers). */
|
|
2248
2535
|
function placeMasked(
|
|
2249
2536
|
dst: Uint8Array,
|