sliftutils 1.7.32 → 1.7.33
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/builders/extensionBuild.ts +1 -1
- package/bundler/bundleEntry.ts +1 -3
- package/bundler/bundler.ts +2 -4
- package/bundler/sourceMaps.ts +1 -2
- package/examplestorage/browser.tsx +1 -2
- package/examplestorage/exampleserver.ts +1 -2
- package/index.d.ts +105 -100
- package/misc/dist/environment.ts.cache +2 -2
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/dist/zip.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/dns.ts.cache +7 -16
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/https/dns.d.ts +1 -4
- package/misc/https/dns.ts +5 -14
- package/misc/https/hostServer.d.ts +2 -0
- package/misc/https/hostServer.ts +6 -7
- package/misc/https/httpsCerts.ts +4 -10
- package/misc/openrouter.ts +1 -2
- package/package.json +1 -1
- package/render-utils/FullscreenModal.tsx +1 -4
- package/render-utils/GenericFormat.tsx +2 -8
- package/render-utils/Input.tsx +5 -13
- package/render-utils/InputLabel.tsx +4 -6
- package/render-utils/InputPicker.tsx +1 -2
- package/render-utils/URLParam.ts +2 -3
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/render-utils/mobxTyped.ts +2 -3
- package/render-utils/niceStringify.ts +2 -4
- package/storage/ArchivesDisk.ts +4 -14
- package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
- package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
- package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
- package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
- package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
- package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
- package/storage/BulkDatabase2/blockCache.ts +12 -28
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
- package/storage/BulkDatabase2/mergeLock.ts +7 -26
- package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
- package/storage/BulkDatabase2/streamLog.ts +14 -40
- package/storage/BulkDatabase2/syncClient.ts +8 -24
- package/storage/FileFolderAPI.tsx +28 -75
- package/storage/IArchives.d.ts +3 -3
- package/storage/IArchives.ts +21 -50
- package/storage/IStorage.ts +2 -4
- package/storage/PendingManager.tsx +2 -3
- package/storage/TransactionStorage.ts +11 -24
- package/storage/backblaze.ts +12 -43
- package/storage/dist/ArchivesDisk.ts.cache +7 -17
- package/storage/dist/FileFolderAPI.tsx.cache +25 -63
- package/storage/dist/IArchives.ts.cache +10 -20
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/PendingManager.tsx.cache +5 -6
- package/storage/dist/TransactionStorage.ts.cache +14 -27
- package/storage/dist/backblaze.ts.cache +15 -46
- package/storage/dist/embeddingFormats.ts.cache +11 -24
- package/storage/dist/fileSystemPointer.ts.cache +4 -7
- package/storage/dist/remoteFileStorage.ts.cache +17 -36
- package/storage/embeddingFormats.ts +11 -27
- package/storage/fileSystemPointer.ts +2 -7
- package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
- package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
- package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
- package/storage/proxydatabase/transactionSet.ts +2 -5
- package/storage/remoteFileServer.ts +20 -48
- package/storage/remoteFileStorage.ts +14 -33
- package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
- package/storage/remoteStorage/ArchivesRemote.ts +11 -26
- package/storage/remoteStorage/ArchivesUrl.ts +2 -4
- package/storage/remoteStorage/accessPage.tsx +1 -5
- package/storage/remoteStorage/blobStore.d.ts +5 -13
- package/storage/remoteStorage/blobStore.ts +52 -130
- package/storage/remoteStorage/createArchives.d.ts +9 -13
- package/storage/remoteStorage/createArchives.ts +43 -187
- package/storage/remoteStorage/deployTakeover.d.ts +18 -32
- package/storage/remoteStorage/deployTakeover.ts +70 -384
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
- package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +48 -189
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
- package/storage/remoteStorage/dist/storageClientController.ts.cache +4 -6
- package/storage/remoteStorage/dist/storageController.ts.cache +11 -73
- package/storage/remoteStorage/dist/storageServerState.ts.cache +265 -178
- package/storage/remoteStorage/grantAccessCli.ts +1 -3
- package/storage/remoteStorage/intermediateSources.d.ts +21 -0
- package/storage/remoteStorage/intermediateSources.ts +117 -0
- package/storage/remoteStorage/remoteConfig.ts +33 -20
- package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
- package/storage/remoteStorage/sourceWrapper.ts +19 -41
- package/storage/remoteStorage/storageClientController.ts +1 -3
- package/storage/remoteStorage/storageController.d.ts +4 -5
- package/storage/remoteStorage/storageController.ts +10 -81
- package/storage/remoteStorage/storageServer.ts +20 -34
- package/storage/remoteStorage/storageServerState.d.ts +28 -5
- package/storage/remoteStorage/storageServerState.ts +271 -207
|
@@ -39,10 +39,7 @@ const USE_INDEXED_DB = false;
|
|
|
39
39
|
// How often a worker rechecks the pointer IndexedDB for a granted handle when none is available yet.
|
|
40
40
|
const WORKER_POLL_INTERVAL_MS = 60 * 1000;
|
|
41
41
|
|
|
42
|
-
// These mirror the subset of the native FileSystemFileHandle / FileSystemDirectoryHandle API we use, so
|
|
43
|
-
// the native browser handles, the Node handles, and the remote handles are all interchangeable — and
|
|
44
|
-
// code written against the native handle (e.g. a recursive walk over `handle.entries()`) works on any of
|
|
45
|
-
// them. kind/name and entries() are part of that contract.
|
|
42
|
+
// These mirror the subset of the native FileSystemFileHandle / FileSystemDirectoryHandle API we use, so the native browser handles, the Node handles, and the remote handles are all interchangeable — and code written against the native handle (e.g. a recursive walk over `handle.entries()`) works on any of them. kind/name and entries() are part of that contract.
|
|
46
43
|
export type FileWrapper = {
|
|
47
44
|
readonly kind: "file";
|
|
48
45
|
readonly name: string;
|
|
@@ -59,19 +56,15 @@ export type FileWrapper = {
|
|
|
59
56
|
write(value: Buffer): Promise<void>;
|
|
60
57
|
close(): Promise<void>;
|
|
61
58
|
}>;
|
|
62
|
-
// Returns a URL for the file's bytes, usable in <video>/<img>/fetch. Optional — the native browser
|
|
63
|
-
// FileSystemFileHandle has no such method, so prefer the getFileURL() helper, which falls back to a
|
|
64
|
-
// blob: URL via createObjectURL for native handles. Always release it with disposeFileURL when done.
|
|
59
|
+
// Returns a URL for the file's bytes, usable in <video>/<img>/fetch. Optional — the native browser FileSystemFileHandle has no such method, so prefer the getFileURL() helper, which falls back to a blob: URL via createObjectURL for native handles. Always release it with disposeFileURL when done.
|
|
65
60
|
getURL?(): Promise<string>;
|
|
66
61
|
};
|
|
67
62
|
export type DirectoryWrapper = {
|
|
68
63
|
readonly kind: "directory";
|
|
69
64
|
readonly name: string;
|
|
70
|
-
// Full path from the storage root, for diagnostics/logging (the native handle doesn't expose paths,
|
|
71
|
-
// so it's optional). e.g. "bulkDatabases2/myCollection".
|
|
65
|
+
// Full path from the storage root, for diagnostics/logging (the native handle doesn't expose paths, so it's optional). e.g. "bulkDatabases2/myCollection".
|
|
72
66
|
readonly fullPath?: string;
|
|
73
|
-
// True when this storage is served over the network (a remote server) rather than a local disk, so
|
|
74
|
-
// callers can adapt for the higher latency/cost (local/native handles leave it undefined = false).
|
|
67
|
+
// True when this storage is served over the network (a remote server) rather than a local disk, so callers can adapt for the higher latency/cost (local/native handles leave it undefined = false).
|
|
75
68
|
readonly isRemote?: boolean;
|
|
76
69
|
removeEntry(key: string, options?: { recursive?: boolean }): Promise<void>;
|
|
77
70
|
getFileHandle(key: string, options?: { create?: boolean }): Promise<FileWrapper>;
|
|
@@ -107,8 +100,7 @@ export function setStorageRootOverride(handle: FileSystemDirectoryHandle | undef
|
|
|
107
100
|
}
|
|
108
101
|
|
|
109
102
|
// ---- remote (server) storage config ----
|
|
110
|
-
// Instead of a local folder, the user can point at a remoteFileServer.js instance (URL + password).
|
|
111
|
-
// When configured, getFileStorageNested2 serves everything from that server. Persisted in localStorage.
|
|
103
|
+
// Instead of a local folder, the user can point at a remoteFileServer.js instance (URL + password). When configured, getFileStorageNested2 serves everything from that server. Persisted in localStorage.
|
|
112
104
|
type RemoteConfig = { url: string; password: string };
|
|
113
105
|
function remoteConfigKey() { return getFileAPIKey() + ":remote"; }
|
|
114
106
|
function loadRemoteConfig(): RemoteConfig | undefined {
|
|
@@ -122,8 +114,7 @@ function loadRemoteConfig(): RemoteConfig | undefined {
|
|
|
122
114
|
}
|
|
123
115
|
function saveRemoteConfig(c: RemoteConfig) { localStorage.setItem(remoteConfigKey(), JSON.stringify(c)); }
|
|
124
116
|
|
|
125
|
-
// The server is always HTTPS on the filehoster's default port, so the user only needs to type the host
|
|
126
|
-
// (e.g. "65.109.93.113"). We strip any scheme/path they include and default the port if omitted.
|
|
117
|
+
// The server is always HTTPS on the filehoster's default port, so the user only needs to type the host (e.g. "65.109.93.113"). We strip any scheme/path they include and default the port if omitted.
|
|
127
118
|
const DEFAULT_REMOTE_PORT = 8787; // matches remoteFileServer.js's default
|
|
128
119
|
function normalizeServerUrl(raw: string): string {
|
|
129
120
|
let s = raw.trim().replace(/^\w+:\/\//, "").replace(/\/.*$/, "");
|
|
@@ -160,11 +151,7 @@ class DirectoryPrompter extends preact.Component {
|
|
|
160
151
|
}
|
|
161
152
|
}
|
|
162
153
|
|
|
163
|
-
// "Connect to a server" option for the directory prompt: collapses to a button, expands to address +
|
|
164
|
-
// password fields. On connect it ACTUALLY connects (testRemoteConnection); only on success does it
|
|
165
|
-
// persist the config and call onConnected, so the caller (getDirectoryHandle) resolves with a working
|
|
166
|
-
// remote handle. Failures are shown to the user (and logged, without the password), never swallowed.
|
|
167
|
-
// `initial` pre-fills + expands the form (used to retry a remembered server that stopped working).
|
|
154
|
+
// "Connect to a server" option for the directory prompt: collapses to a button, expands to address + password fields. On connect it ACTUALLY connects (testRemoteConnection); only on success does it persist the config and call onConnected, so the caller (getDirectoryHandle) resolves with a working remote handle. Failures are shown to the user (and logged, without the password), never swallowed. `initial` pre-fills + expands the form (used to retry a remembered server that stopped working).
|
|
168
155
|
@observer
|
|
169
156
|
class ServerConnectForm extends preact.Component<{ onConnected: (config: RemoteConfig) => void; initial?: RemoteConfig }> {
|
|
170
157
|
private obs = observable({
|
|
@@ -193,8 +180,7 @@ class ServerConnectForm extends preact.Component<{ onConnected: (config: RemoteC
|
|
|
193
180
|
s.error = "The server rejected that password.";
|
|
194
181
|
console.error("Remote connect: unauthorized for", url);
|
|
195
182
|
} else {
|
|
196
|
-
// Got nothing back — usually the self-signed certificate isn't trusted yet, but show the
|
|
197
|
-
// actual error too so a wrong address / down server / CORS issue is diagnosable.
|
|
183
|
+
// Got nothing back — usually the self-signed certificate isn't trusted yet, but show the actual error too so a wrong address / down server / CORS issue is diagnosable.
|
|
198
184
|
s.needsCert = true;
|
|
199
185
|
s.error = "Couldn't reach the server: " + result.error;
|
|
200
186
|
console.error("Remote connect: unreachable", url, "-", result.error);
|
|
@@ -343,9 +329,7 @@ export class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
|
343
329
|
// Create the file
|
|
344
330
|
await fs.promises.writeFile(filePath, Buffer.alloc(0));
|
|
345
331
|
} else if (!exists) {
|
|
346
|
-
// Tag as ENOENT so readWithRetry treats it as a genuinely-missing file (return undefined now)
|
|
347
|
-
// rather than a transient read failure to retry 6× with backoff — missing files are normal
|
|
348
|
-
// (a concurrent merge deletes a file mid-read), and retrying them is catastrophically slow.
|
|
332
|
+
// Tag as ENOENT so readWithRetry treats it as a genuinely-missing file (return undefined now) rather than a transient read failure to retry 6× with backoff — missing files are normal (a concurrent merge deletes a file mid-read), and retrying them is catastrophically slow.
|
|
349
333
|
const err = new Error(`File not found: ${filePath}`) as Error & { code?: string };
|
|
350
334
|
err.code = "ENOENT";
|
|
351
335
|
throw err;
|
|
@@ -387,9 +371,7 @@ export class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
|
387
371
|
}
|
|
388
372
|
|
|
389
373
|
|
|
390
|
-
// When set, getDirectoryHandle skips every prompt + remote check and serves from the browser's Origin
|
|
391
|
-
// Private File System (OPFS). One named subfolder is "current" per fileAPIKey; pickPrivateFolder /
|
|
392
|
-
// resetStorageLocation switch which one. Set this in app startup BEFORE the first getDirectoryHandle.
|
|
374
|
+
// When set, getDirectoryHandle skips every prompt + remote check and serves from the browser's Origin Private File System (OPFS). One named subfolder is "current" per fileAPIKey; pickPrivateFolder / resetStorageLocation switch which one. Set this in app startup BEFORE the first getDirectoryHandle.
|
|
393
375
|
let opfsEnabled = false;
|
|
394
376
|
const OPFS_FOLDERS_DIR = "folders";
|
|
395
377
|
const OPFS_DEFAULT_FOLDER = "default";
|
|
@@ -401,14 +383,12 @@ function setCurrentOpfsFolder(name: string): void {
|
|
|
401
383
|
localStorage.setItem(opfsCurrentKey(), name);
|
|
402
384
|
}
|
|
403
385
|
|
|
404
|
-
// Switches getDirectoryHandle to the Origin Private File System. No directory picker, no permission
|
|
405
|
-
// prompt, no remote server check. Persists nothing — call on every app start before reading storage.
|
|
386
|
+
// Switches getDirectoryHandle to the Origin Private File System. No directory picker, no permission prompt, no remote server check. Persists nothing — call on every app start before reading storage.
|
|
406
387
|
export function usePrivateFileSystem(): void {
|
|
407
388
|
opfsEnabled = true;
|
|
408
389
|
}
|
|
409
390
|
|
|
410
|
-
// Whether the OPFS branch is in effect (so other callers can adapt — e.g. skip the "find data subdir"
|
|
411
|
-
// hack in getFileStorageNested2 since OPFS is always a clean root we own end-to-end).
|
|
391
|
+
// Whether the OPFS branch is in effect (so other callers can adapt — e.g. skip the "find data subdir" hack in getFileStorageNested2 since OPFS is always a clean root we own end-to-end).
|
|
412
392
|
export function isPrivateFileSystemActive(): boolean {
|
|
413
393
|
return opfsEnabled;
|
|
414
394
|
}
|
|
@@ -431,9 +411,7 @@ async function getCurrentOpfsHandle(): Promise<DirectoryWrapper> {
|
|
|
431
411
|
return folder as unknown as DirectoryWrapper;
|
|
432
412
|
}
|
|
433
413
|
|
|
434
|
-
// All previously-used OPFS subfolders under this fileAPIKey, sorted alphabetically (timestamp-named
|
|
435
|
-
// auto-generated folders therefore sort chronologically). Use this to surface a list, then call
|
|
436
|
-
// pickPrivateFolder(name) to switch.
|
|
414
|
+
// All previously-used OPFS subfolders under this fileAPIKey, sorted alphabetically (timestamp-named auto-generated folders therefore sort chronologically). Use this to surface a list, then call pickPrivateFolder(name) to switch.
|
|
437
415
|
export async function listPrivateFolders(): Promise<string[]> {
|
|
438
416
|
if (!navigator.storage?.getDirectory) return [];
|
|
439
417
|
let folders: FileSystemDirectoryHandle;
|
|
@@ -445,17 +423,13 @@ export async function listPrivateFolders(): Promise<string[]> {
|
|
|
445
423
|
return names.sort();
|
|
446
424
|
}
|
|
447
425
|
|
|
448
|
-
// Switch to a specific OPFS subfolder and reload so getDirectoryHandle re-resolves to it. The folder
|
|
449
|
-
// is created if it doesn't exist yet.
|
|
426
|
+
// Switch to a specific OPFS subfolder and reload so getDirectoryHandle re-resolves to it. The folder is created if it doesn't exist yet.
|
|
450
427
|
export function pickPrivateFolder(name: string): void {
|
|
451
428
|
setCurrentOpfsFolder(name);
|
|
452
429
|
window.location.reload();
|
|
453
430
|
}
|
|
454
431
|
|
|
455
|
-
// Returns the directory handle to use — local (Node / picked folder) OR a remote server, both as the
|
|
456
|
-
// same DirectoryWrapper, so callers don't know or care which it is. A remembered server is VERIFIED
|
|
457
|
-
// (we actually connect) before use; if it no longer works we re-prompt, just like a local folder whose
|
|
458
|
-
// permission was lost. Blocks until ready (or the user dismisses, which rejects).
|
|
432
|
+
// Returns the directory handle to use — local (Node / picked folder) OR a remote server, both as the same DirectoryWrapper, so callers don't know or care which it is. A remembered server is VERIFIED (we actually connect) before use; if it no longer works we re-prompt, just like a local folder whose permission was lost. Blocks until ready (or the user dismisses, which rejects).
|
|
459
433
|
export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Promise<DirectoryWrapper> {
|
|
460
434
|
// An injected root (e.g. a Web Worker handed the handle by its owning window)
|
|
461
435
|
// short-circuits the whole interactive resolution — no picker, no DOM, no
|
|
@@ -463,12 +437,7 @@ export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Prom
|
|
|
463
437
|
if (storageRootOverride) {
|
|
464
438
|
return storageRootOverride as unknown as DirectoryWrapper;
|
|
465
439
|
}
|
|
466
|
-
// A worker can't show the picker (no DOM / no user activation) and can't read localStorage, but
|
|
467
|
-
// it CAN open the pointer IndexedDB the main-thread picker persists to. Poll it for a handle
|
|
468
|
-
// whose permission is already granted; the owning window may also postMessage a handle in
|
|
469
|
-
// mid-poll (checked each iteration). This has to come before the isNode branch: typesafecss's
|
|
470
|
-
// isNode() returns true in workers (window is undefined), so a worker would otherwise fall into
|
|
471
|
-
// the Node branch and try to use fs/path.
|
|
440
|
+
// A worker can't show the picker (no DOM / no user activation) and can't read localStorage, but it CAN open the pointer IndexedDB the main-thread picker persists to. Poll it for a handle whose permission is already granted; the owning window may also postMessage a handle in mid-poll (checked each iteration). This has to come before the isNode branch: typesafecss's isNode() returns true in workers (window is undefined), so a worker would otherwise fall into the Node branch and try to use fs/path.
|
|
472
441
|
if ("WorkerGlobalScope" in globalThis) {
|
|
473
442
|
while (true) {
|
|
474
443
|
if (storageRootOverride) return storageRootOverride as unknown as DirectoryWrapper;
|
|
@@ -512,8 +481,7 @@ export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Prom
|
|
|
512
481
|
}
|
|
513
482
|
};
|
|
514
483
|
const onConnected = (config: RemoteConfig) => { saveRemoteConfig(config); resolveHandle(getRemoteHandle(config)); };
|
|
515
|
-
// The three options, rendered fresh each time. If a saved server just failed, pre-fill + expand
|
|
516
|
-
// the connect form so the user can retry or fix it.
|
|
484
|
+
// The three options, rendered fresh each time. If a saved server just failed, pre-fill + expand the connect form so the user can retry or fix it.
|
|
517
485
|
const renderOptions = () => (
|
|
518
486
|
<>
|
|
519
487
|
<button className={css.fontSize(40).pad2(80, 40)} onClick={pickLocal}>Pick Data Directory</button>
|
|
@@ -523,8 +491,7 @@ export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Prom
|
|
|
523
491
|
</>
|
|
524
492
|
);
|
|
525
493
|
|
|
526
|
-
// A previously-picked local folder: try to restore it (may need a click). Skipped when a saved
|
|
527
|
-
// server failed — that user wants the server, so go straight to the (pre-filled) prompt.
|
|
494
|
+
// A previously-picked local folder: try to restore it (may need a click). Skipped when a saved server failed — that user wants the server, so go straight to the (pre-filled) prompt.
|
|
528
495
|
const storedId = !savedRemote && localStorage.getItem(getFileAPIKey());
|
|
529
496
|
if (storedId) {
|
|
530
497
|
let doneLoad = false;
|
|
@@ -582,8 +549,7 @@ export const getFileStorageNested2 = cache(async function getFileStorage(pathStr
|
|
|
582
549
|
base = new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
|
|
583
550
|
} else {
|
|
584
551
|
base = await getDirectoryHandle();
|
|
585
|
-
// Skip the "find data subdir" hack under OPFS — that's for picked directories where the user
|
|
586
|
-
// might have selected a folder with unrelated files; OPFS is a clean root we own end-to-end.
|
|
552
|
+
// Skip the "find data subdir" hack under OPFS — that's for picked directories where the user might have selected a folder with unrelated files; OPFS is a clean root we own end-to-end.
|
|
587
553
|
if (!opfsEnabled) {
|
|
588
554
|
let dirs: string[] = [];
|
|
589
555
|
let alls: string[] = [];
|
|
@@ -614,8 +580,7 @@ export const getFileStorage = lazy(async function getFileStorage(): Promise<File
|
|
|
614
580
|
});
|
|
615
581
|
export function resetStorageLocation() {
|
|
616
582
|
if (opfsEnabled) {
|
|
617
|
-
// Don't delete previous folder — listPrivateFolders / pickPrivateFolder still need it. Just point
|
|
618
|
-
// "current" at a fresh timestamp-named one so the next reload starts clean.
|
|
583
|
+
// Don't delete previous folder — listPrivateFolders / pickPrivateFolder still need it. Just point "current" at a fresh timestamp-named one so the next reload starts clean.
|
|
619
584
|
setCurrentOpfsFolder(new Date().toISOString().replace(/[:.]/g, "-"));
|
|
620
585
|
window.location.reload();
|
|
621
586
|
return;
|
|
@@ -634,8 +599,7 @@ export type NestedFileStorage = {
|
|
|
634
599
|
|
|
635
600
|
export type FileStorage = IStorageRaw & {
|
|
636
601
|
folder: NestedFileStorage;
|
|
637
|
-
// Mirrors DirectoryWrapper.isRemote: true when this storage is served over the network. Lets callers
|
|
638
|
-
// (e.g. BulkDatabase2, which skips automatic compaction over the network by default) adapt.
|
|
602
|
+
// Mirrors DirectoryWrapper.isRemote: true when this storage is served over the network. Lets callers (e.g. BulkDatabase2, which skips automatic compaction over the network by default) adapt.
|
|
639
603
|
isRemote?: boolean;
|
|
640
604
|
};
|
|
641
605
|
|
|
@@ -671,8 +635,7 @@ async function fixedGetFileHandle(config: {
|
|
|
671
635
|
return await config.handle.getFileHandle(config.key, { create: true });
|
|
672
636
|
}
|
|
673
637
|
|
|
674
|
-
// A file that genuinely does not exist. We must NOT retry these, otherwise reading many missing
|
|
675
|
-
// files (a common pattern) gets catastrophically slow.
|
|
638
|
+
// A file that genuinely does not exist. We must NOT retry these, otherwise reading many missing files (a common pattern) gets catastrophically slow.
|
|
676
639
|
function isMissingError(error: unknown): boolean {
|
|
677
640
|
let name = (error as { name?: string })?.name;
|
|
678
641
|
let code = (error as { code?: string })?.code;
|
|
@@ -681,10 +644,7 @@ function isMissingError(error: unknown): boolean {
|
|
|
681
644
|
|
|
682
645
|
const MAX_READ_RETRIES = 6;
|
|
683
646
|
|
|
684
|
-
// The browser File System Access API can transiently fail reads under heavy load (it appears to
|
|
685
|
-
// throttle when a page issues many reads at once). Those failures surface as errors OTHER than
|
|
686
|
-
// "not found", so we retry them with backoff. A real missing file (NotFoundError / ENOENT) returns
|
|
687
|
-
// undefined immediately with no retry.
|
|
647
|
+
// The browser File System Access API can transiently fail reads under heavy load (it appears to throttle when a page issues many reads at once). Those failures surface as errors OTHER than "not found", so we retry them with backoff. A real missing file (NotFoundError / ENOENT) returns undefined immediately with no retry.
|
|
688
648
|
async function readWithRetry<T>(label: string, key: string, read: () => Promise<T>): Promise<T | undefined> {
|
|
689
649
|
let backoff = 25;
|
|
690
650
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -724,8 +684,7 @@ function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
|
724
684
|
const file = await handle.getFileHandle(key);
|
|
725
685
|
const fileContent = await file.getFile();
|
|
726
686
|
const arrayBuffer = await fileContent.arrayBuffer();
|
|
727
|
-
// Under load the FS Access API can resolve with a truncated buffer and no error.
|
|
728
|
-
// Treat a short read as transient so readWithRetry retries it.
|
|
687
|
+
// Under load the FS Access API can resolve with a truncated buffer and no error. Treat a short read as transient so readWithRetry retries it.
|
|
729
688
|
if (arrayBuffer.byteLength !== fileContent.size) {
|
|
730
689
|
throw Object.assign(new Error(`Short read: got ${arrayBuffer.byteLength} of ${fileContent.size} bytes`), { name: "ShortReadError" });
|
|
731
690
|
}
|
|
@@ -749,8 +708,7 @@ function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
|
749
708
|
|
|
750
709
|
async append(key: string, value: Buffer): Promise<void> {
|
|
751
710
|
await appendQueue(key)(async () => {
|
|
752
|
-
// NOTE: Interesting point. Chrome doesn't optimize this to be an append, and instead
|
|
753
|
-
// rewrites the entire file.
|
|
711
|
+
// NOTE: Interesting point. Chrome doesn't optimize this to be an append, and instead rewrites the entire file.
|
|
754
712
|
const file = await fixedGetFileHandle({ handle, key, create: true });
|
|
755
713
|
const writable = await file.createWritable({ keepExistingData: true });
|
|
756
714
|
let offset = (await file.getFile()).size;
|
|
@@ -837,10 +795,7 @@ export function wrapHandle(handle: DirectoryWrapper): FileStorage {
|
|
|
837
795
|
};
|
|
838
796
|
}
|
|
839
797
|
|
|
840
|
-
// Returns a URL for a file's bytes, ready to drop into <video>/<img>/fetch. A native (local) file becomes
|
|
841
|
-
// an in-memory blob: URL; a remote file becomes an https URL into the server's range-capable /media
|
|
842
|
-
// endpoint (auth token in the query, since a media element can't send headers). Both support HTTP range /
|
|
843
|
-
// seeking. ALWAYS hand the result to disposeFileURL when finished — for blob: URLs that frees memory.
|
|
798
|
+
// Returns a URL for a file's bytes, ready to drop into <video>/<img>/fetch. A native (local) file becomes an in-memory blob: URL; a remote file becomes an https URL into the server's range-capable /media endpoint (auth token in the query, since a media element can't send headers). Both support HTTP range / seeking. ALWAYS hand the result to disposeFileURL when finished — for blob: URLs that frees memory.
|
|
844
799
|
export async function getFileURL(file: FileWrapper): Promise<string> {
|
|
845
800
|
if (file.getURL) return file.getURL();
|
|
846
801
|
// Native FileSystemFileHandle (or any Blob-backed file): a blob: URL over the File itself.
|
|
@@ -848,16 +803,14 @@ export async function getFileURL(file: FileWrapper): Promise<string> {
|
|
|
848
803
|
return URL.createObjectURL(f as unknown as Blob);
|
|
849
804
|
}
|
|
850
805
|
|
|
851
|
-
// Releases a URL from getFileURL. blob: URLs leak until the document is gone unless revoked; https/file:
|
|
852
|
-
// URLs need no cleanup, so this is a no-op for them.
|
|
806
|
+
// Releases a URL from getFileURL. blob: URLs leak until the document is gone unless revoked; https/file: URLs need no cleanup, so this is a no-op for them.
|
|
853
807
|
export function disposeFileURL(url: string): void {
|
|
854
808
|
if (url.startsWith("blob:")) {
|
|
855
809
|
try { URL.revokeObjectURL(url); } catch { /* not in a browser, or already revoked */ }
|
|
856
810
|
}
|
|
857
811
|
}
|
|
858
812
|
|
|
859
|
-
// A StorageFactory backed by a remote server (path -> FileStorage), for code that injects its own
|
|
860
|
-
// storage into BulkDatabase2 rather than going through the directory prompt.
|
|
813
|
+
// A StorageFactory backed by a remote server (path -> FileStorage), for code that injects its own storage into BulkDatabase2 rather than going through the directory prompt.
|
|
861
814
|
export function getRemoteFileStorageFactory(url: string, password: string, options?: RemoteOptions): (pathStr: string) => Promise<FileStorage> {
|
|
862
815
|
const root = getRemoteDirectoryHandle(url, password, options);
|
|
863
816
|
return async (pathStr: string) => {
|
package/storage/IArchives.d.ts
CHANGED
|
@@ -28,11 +28,12 @@ export type CommonConfig = {
|
|
|
28
28
|
validWindow: [number, number];
|
|
29
29
|
/** Sharding: the fraction of the key space this source handles, as [start, end) over [0, 1) (keys are routed by getRoute in remoteConfig.ts). Defaults to FULL_ROUTE (unsharded). At every point in time the sources' routes must fully cover [0, 1), or some keys could never be read. */
|
|
30
30
|
route?: [number, number];
|
|
31
|
+
/** Set on entries injected into the in-memory config by an overlay (a deploy switchover's alternate-port window). Never written to disk: resolveIntermediateSources strips these and rejoins the windows around them, which is also how a client tells whether an update is a real configuration change or just an overlay. */
|
|
32
|
+
intermediate?: boolean;
|
|
31
33
|
};
|
|
32
34
|
export type HostedConfig = CommonConfig & {
|
|
33
35
|
type: "remote";
|
|
34
36
|
url: string;
|
|
35
|
-
accountName?: string;
|
|
36
37
|
public?: boolean;
|
|
37
38
|
fast?: boolean;
|
|
38
39
|
writeDelay?: number;
|
|
@@ -104,8 +105,7 @@ export type ArchivesSyncStatus = {
|
|
|
104
105
|
};
|
|
105
106
|
export interface IArchives {
|
|
106
107
|
getDebugName(): string;
|
|
107
|
-
/** Whether writes would be accepted (credentials exist, the account trusts this machine, etc).
|
|
108
|
-
* Checked without writing anything. */
|
|
108
|
+
/** Whether writes would be accepted (credentials exist, the account trusts this machine, etc). Checked without writing anything. */
|
|
109
109
|
hasWriteAccess(): Promise<boolean>;
|
|
110
110
|
get(fileName: string, config?: {
|
|
111
111
|
range?: {
|
package/storage/IArchives.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
|
-
// The important operations of an archive bucket (extracted from ArchivesBackblaze), so other
|
|
4
|
-
// backends (e.g. our own remote storage server) can be used interchangeably.
|
|
3
|
+
// The important operations of an archive bucket (extracted from ArchivesBackblaze), so other backends (e.g. our own remote storage server) can be used interchangeably.
|
|
5
4
|
|
|
6
|
-
// A write may not be stamped more than this far in the future, or clock skew between machines
|
|
7
|
-
// would let a bad timestamp block writes for a long time.
|
|
5
|
+
// A write may not be stamped more than this far in the future, or clock skew between machines would let a bad timestamp block writes for a long time.
|
|
8
6
|
export const MAX_LAST_MODIFIED_FUTURE = 15 * 60 * 1000;
|
|
9
7
|
|
|
10
|
-
// How long browsers may cache files from immutable buckets (the Cache-Control max-age), shared by
|
|
11
|
-
// every hosting path (backblaze bucket settings and our own storage server's HTTP route)
|
|
8
|
+
// How long browsers may cache files from immutable buckets (the Cache-Control max-age), shared by every hosting path (backblaze bucket settings and our own storage server's HTTP route)
|
|
12
9
|
export const IMMUTABLE_CACHE_TIME = 86400 * 1000;
|
|
13
10
|
export function assertValidLastModified(lastModified: number): void {
|
|
14
11
|
let max = Date.now() + MAX_LAST_MODIFIED_FUTURE;
|
|
@@ -46,6 +43,8 @@ export type CommonConfig = {
|
|
|
46
43
|
validWindow: [number, number];
|
|
47
44
|
/** Sharding: the fraction of the key space this source handles, as [start, end) over [0, 1) (keys are routed by getRoute in remoteConfig.ts). Defaults to FULL_ROUTE (unsharded). At every point in time the sources' routes must fully cover [0, 1), or some keys could never be read. */
|
|
48
45
|
route?: [number, number];
|
|
46
|
+
/** Set on entries injected into the in-memory config by an overlay (a deploy switchover's alternate-port window). Never written to disk: resolveIntermediateSources strips these and rejoins the windows around them, which is also how a client tells whether an update is a real configuration change or just an overlay. */
|
|
47
|
+
intermediate?: boolean;
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
export type HostedConfig = CommonConfig & {
|
|
@@ -56,16 +55,12 @@ export type HostedConfig = CommonConfig & {
|
|
|
56
55
|
url: string;
|
|
57
56
|
|
|
58
57
|
// NOTE: Authentication is handled by cert.ts, via having your machine trusted to access this account.
|
|
59
|
-
accountName?: string;
|
|
60
58
|
|
|
61
59
|
public?: boolean;
|
|
62
|
-
// Fast mode: the server acknowledges writes once they are in memory, flushing to disk after
|
|
63
|
-
// writeDelay (default 5 minutes) and coalescing writes to the same file. A server crash loses
|
|
64
|
-
// writes that haven't flushed yet.
|
|
60
|
+
// Fast mode: the server acknowledges writes once they are in memory, flushing to disk after writeDelay (default 5 minutes) and coalescing writes to the same file. A server crash loses writes that haven't flushed yet.
|
|
65
61
|
fast?: boolean;
|
|
66
62
|
writeDelay?: number;
|
|
67
|
-
// The bucket is served straight from the server's disk, with no index — so no fast writes and
|
|
68
|
-
// no getChangesAfter/getSyncStatus.
|
|
63
|
+
// The bucket is served straight from the server's disk, with no index — so no fast writes and no getChangesAfter/getSyncStatus.
|
|
69
64
|
rawDisk?: boolean;
|
|
70
65
|
// Writes to paths that already exist are disallowed (deletes still work).
|
|
71
66
|
immutable?: boolean;
|
|
@@ -81,9 +76,7 @@ export type BackblazeConfig = CommonConfig & {
|
|
|
81
76
|
// NOTE: This isn't enforced on the backblaze level, so this is just a client-side guarantee. This can change how we cache files.
|
|
82
77
|
// - Backblaze does support immutability. However, apparently, once we enable it on a bucket, we can't disable it, which is really bad, as it means if our code could ever enable it and we accidentally enable it on an important bucket, we essentially just bricked that bucket. So we should never write any code that ever tries to use backblaze to make things immutable.
|
|
83
78
|
immutable?: boolean;
|
|
84
|
-
// CORS origins allowed to consume the bucket's files in a browser. Not a security boundary
|
|
85
|
-
// (access is gated by the API key / signed URLs, neither of which rides in cookies) - it only
|
|
86
|
-
// controls which sites' in-page JavaScript can read responses. Defaults to any HTTPS origin.
|
|
79
|
+
// CORS origins allowed to consume the bucket's files in a browser. Not a security boundary (access is gated by the API key / signed URLs, neither of which rides in cookies) - it only controls which sites' in-page JavaScript can read responses. Defaults to any HTTPS origin.
|
|
87
80
|
allowedOrigins?: string[];
|
|
88
81
|
|
|
89
82
|
// NOTE: We will access the api key from getSecret, see backblaze.ts for the specific keys.
|
|
@@ -95,16 +88,12 @@ export const FULL_VALID_WINDOW: [number, number] = [0, Number.MAX_SAFE_INTEGER];
|
|
|
95
88
|
|
|
96
89
|
|
|
97
90
|
|
|
98
|
-
// createTime is a misnomer kept for compatibility — it is really the LAST-WRITE time, same as
|
|
99
|
-
// getInfo's writeTime. Neither Backblaze nor our remote storage tracks a distinct creation date:
|
|
100
|
-
// each write stamps a fresh timestamp on the current version, so both fields are just "when the
|
|
101
|
-
// bytes served by get() were most recently written".
|
|
91
|
+
// createTime is a misnomer kept for compatibility — it is really the LAST-WRITE time, same as getInfo's writeTime. Neither Backblaze nor our remote storage tracks a distinct creation date: each write stamps a fresh timestamp on the current version, so both fields are just "when the bytes served by get() were most recently written".
|
|
102
92
|
export type ArchiveFileInfo = { path: string; createTime: number; size: number };
|
|
103
93
|
|
|
104
94
|
// An in-progress background synchronization task (see ArchivesConfig.syncing)
|
|
105
95
|
export type SyncActivity = {
|
|
106
|
-
// A metadata scan is a single listing call, so it has no incremental progress - just that it's
|
|
107
|
-
// running and since when. A full sync knows its exact file/byte progress.
|
|
96
|
+
// A metadata scan is a single listing call, so it has no incremental progress - just that it's running and since when. A full sync knows its exact file/byte progress.
|
|
108
97
|
type: "metadataScan" | "fullSync";
|
|
109
98
|
sourceDebugName: string;
|
|
110
99
|
startTime: number;
|
|
@@ -117,14 +106,11 @@ export type SyncActivity = {
|
|
|
117
106
|
export type ArchivesConfig = {
|
|
118
107
|
// Whether getChangesAfter is implemented (fast change polling, instead of full rescans)
|
|
119
108
|
supportsChangesAfter?: boolean;
|
|
120
|
-
// The bucket's full routing config (ROUTING_FILE). Absent for sources that don't have one (a
|
|
121
|
-
// bare disk source, or a bucket that doesn't exist yet).
|
|
109
|
+
// The bucket's full routing config (ROUTING_FILE). Absent for sources that don't have one (a bare disk source, or a bucket that doesn't exist yet).
|
|
122
110
|
remoteConfig?: RemoteConfig;
|
|
123
|
-
// Live index totals (tombstones excluded), kept up to date in memory on every mutation and
|
|
124
|
-
// recomputed on load - so any drift heals on restart
|
|
111
|
+
// Live index totals (tombstones excluded), kept up to date in memory on every mutation and recomputed on load - so any drift heals on restart
|
|
125
112
|
index?: { fileCount: number; byteCount: number };
|
|
126
|
-
// The same totals broken down by which source currently holds each file's bytes (the first
|
|
127
|
-
// entry is the server's own disk)
|
|
113
|
+
// The same totals broken down by which source currently holds each file's bytes (the first entry is the server's own disk)
|
|
128
114
|
indexSources?: { debugName: string; fileCount: number; byteCount: number }[];
|
|
129
115
|
// The server's configured readerDiskLimit, when it runs as a bounded read cache
|
|
130
116
|
readerDiskLimit?: number;
|
|
@@ -135,41 +121,27 @@ export type ArchivesConfig = {
|
|
|
135
121
|
// A synchronization source of a BlobStore (which synchronizes an index + local cache from them)
|
|
136
122
|
export type ArchivesSource = {
|
|
137
123
|
source: IArchives;
|
|
138
|
-
// From the source's CommonConfig. Values with write times outside the window are ignored when
|
|
139
|
-
// scanning, and once its end is past (see windowAcceptsWrites) the source stops
|
|
140
|
-
// receiving writes entirely - still scanned (it holds the authoritative data for its window),
|
|
141
|
-
// it just stops growing.
|
|
124
|
+
// From the source's CommonConfig. Values with write times outside the window are ignored when scanning, and once its end is past (see windowAcceptsWrites) the source stops receiving writes entirely - still scanned (it holds the authoritative data for its window), it just stops growing.
|
|
142
125
|
validWindow: [number, number];
|
|
143
|
-
// From the source's CommonConfig (intersected with the owning store's own route): only keys
|
|
144
|
-
// routing into [start, end) are accepted from this source's scans and sent to it in
|
|
145
|
-
// writes/reconciliation. The routing file is exempt - config flows everywhere. Absent = all keys.
|
|
126
|
+
// From the source's CommonConfig (intersected with the owning store's own route): only keys routing into [start, end) are accepted from this source's scans and sent to it in writes/reconciliation. The routing file is exempt - config flows everywhere. Absent = all keys.
|
|
146
127
|
route?: [number, number];
|
|
147
128
|
// From the source's CommonConfig; see there.
|
|
148
129
|
noFullSync?: boolean;
|
|
149
|
-
// Stable identity of the underlying endpoint (its config with windows/routes stripped) - how
|
|
150
|
-
// BlobStore.updateSources recognizes a source across config changes so it can update it in
|
|
151
|
-
// place instead of removing and re-adding it
|
|
130
|
+
// Stable identity of the underlying endpoint (its config with windows/routes stripped) - how BlobStore.updateSources recognizes a source across config changes so it can update it in place instead of removing and re-adding it
|
|
152
131
|
identity?: string;
|
|
153
132
|
};
|
|
154
133
|
|
|
155
|
-
// Error marker a server includes when a freshly-stamped write reaches it outside its valid windows
|
|
156
|
-
// (the client resolved its target, then time crossed a window boundary before the write landed).
|
|
157
|
-
// Clients detect this marker and re-resolve the currently-valid source, retrying ONCE - boundaries
|
|
158
|
-
// are far apart, so hitting it twice in one attempt means something is actually wrong.
|
|
134
|
+
// Error marker a server includes when a freshly-stamped write reaches it outside its valid windows (the client resolved its target, then time crossed a window boundary before the write landed). Clients detect this marker and re-resolve the currently-valid source, retrying ONCE - boundaries are far apart, so hitting it twice in one attempt means something is actually wrong.
|
|
159
135
|
export const STORAGE_WRONG_VALID_WINDOW = "REMOTE_STORAGE_WRONG_VALID_WINDOW_a7c1f04e";
|
|
160
136
|
|
|
161
|
-
// Error marker a server includes when a freshly-stamped write's key routes outside the shards this
|
|
162
|
-
// server handles (the client's config disagrees with the server's - clients re-resolve once)
|
|
137
|
+
// Error marker a server includes when a freshly-stamped write's key routes outside the shards this server handles (the client's config disagrees with the server's - clients re-resolve once)
|
|
163
138
|
export const STORAGE_WRONG_ROUTE = "REMOTE_STORAGE_WRONG_ROUTE_c94d2e17";
|
|
164
139
|
|
|
165
140
|
export const FULL_ROUTE: [number, number] = [0, 1];
|
|
166
141
|
|
|
167
|
-
// A key containing this sentinel doesn't have a fixed shard: setVariableShard picks the (lowest
|
|
168
|
-
// latency, up) write shard, appends "_<value in the shard's route>" directly after the sentinel,
|
|
169
|
-
// and returns the materialized key. getRoute treats that suffix as a complete route override.
|
|
142
|
+
// A key containing this sentinel doesn't have a fixed shard: setVariableShard picks the (lowest latency, up) write shard, appends "_<value in the shard's route>" directly after the sentinel, and returns the materialized key. getRoute treats that suffix as a complete route override.
|
|
170
143
|
export const VARIABLE_SHARD = "VARIABLE_SHARD_f0234jfah08fgyhfgyssdds83nmp";
|
|
171
|
-
// No grace past the end: a window boundary is a hard handoff (clients retry a rejected write
|
|
172
|
-
// against the newly-valid source, so leniency here would only desynchronize the handoff)
|
|
144
|
+
// No grace past the end: a window boundary is a hard handoff (clients retry a rejected write against the newly-valid source, so leniency here would only desynchronize the handoff)
|
|
173
145
|
export function windowAcceptsWrites(validWindow: [number, number] | undefined): boolean {
|
|
174
146
|
if (!validWindow) return true;
|
|
175
147
|
return validWindow[1] > Date.now();
|
|
@@ -194,8 +166,7 @@ export type ArchivesSyncStatus = {
|
|
|
194
166
|
|
|
195
167
|
export interface IArchives {
|
|
196
168
|
getDebugName(): string;
|
|
197
|
-
/** Whether writes would be accepted (credentials exist, the account trusts this machine, etc).
|
|
198
|
-
* Checked without writing anything. */
|
|
169
|
+
/** Whether writes would be accepted (credentials exist, the account trusts this machine, etc). Checked without writing anything. */
|
|
199
170
|
hasWriteAccess(): Promise<boolean>;
|
|
200
171
|
get(fileName: string, config?: { range?: { start: number; end: number } }): Promise<Buffer | undefined>;
|
|
201
172
|
get2(fileName: string, config?: { range?: { start: number; end: number } }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined>;
|
package/storage/IStorage.ts
CHANGED
|
@@ -25,12 +25,10 @@ export type IStorage<T> = {
|
|
|
25
25
|
// Allows watching for when the storage detects and underlying changes, and resyncs all of it's data (which might
|
|
26
26
|
watchResync?: (callback: () => void) => void;
|
|
27
27
|
};
|
|
28
|
-
// NOTE: In the file system some characters are disallowed, and some characters do special things
|
|
29
|
-
// (/ makes a folder). And there are even more rules, such as lengths per folder, etc, etc.
|
|
28
|
+
// NOTE: In the file system some characters are disallowed, and some characters do special things (/ makes a folder). And there are even more rules, such as lengths per folder, etc, etc.
|
|
30
29
|
export type IStorageRaw = {
|
|
31
30
|
get(key: string): Promise<Buffer | undefined>;
|
|
32
|
-
// Reads bytes in the range [start, end) (end is exclusive, clamped to the file size).
|
|
33
|
-
// Returns undefined if the file doesn't exist.
|
|
31
|
+
// Reads bytes in the range [start, end) (end is exclusive, clamped to the file size). Returns undefined if the file doesn't exist.
|
|
34
32
|
getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined>;
|
|
35
33
|
// May or may not be efficient in the underlying storage
|
|
36
34
|
append(key: string, value: Buffer): Promise<void>;
|
|
@@ -22,8 +22,7 @@ export function hasPending(): boolean {
|
|
|
22
22
|
return Object.keys(watchState.pending).length > 0;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// NOTE: This not only prevents render overload, but also means any pending that are < this
|
|
26
|
-
// delay don't show up (which is useful to reduce unnecessary pending messages).
|
|
25
|
+
// NOTE: This not only prevents render overload, but also means any pending that are < this delay don't show up (which is useful to reduce unnecessary pending messages).
|
|
27
26
|
const setPendingBase = throttleFunction(500, function setPendingBase() {
|
|
28
27
|
for (let [group, message] of pendingCache) {
|
|
29
28
|
|
|
@@ -38,7 +37,7 @@ const setPendingBase = throttleFunction(500, function setPendingBase() {
|
|
|
38
37
|
}
|
|
39
38
|
delete watchState.pending[group];
|
|
40
39
|
} else {
|
|
41
|
-
//console.log("setPending", group, message);
|
|
40
|
+
// console.log("setPending", group, message);
|
|
42
41
|
if (!(group in watchState.pending)) {
|
|
43
42
|
pendingLastSets.set(group, Date.now());
|
|
44
43
|
}
|
|
@@ -13,27 +13,20 @@ import { runInfinitePoll } from "socket-function/src/batching";
|
|
|
13
13
|
// - Zip individual large values
|
|
14
14
|
// - Stores a transaction log
|
|
15
15
|
// - Transaction log has a header, which is JSON, for things such as "zipped"
|
|
16
|
-
// - Transaction log uses length prefixed values, with a special 8 bytes to denote the end,
|
|
17
|
-
//
|
|
18
|
-
// - When transaction log is iterated on, if the bytes at the end (using the length prefix)
|
|
19
|
-
// don't match the end bytes OR the start bytes are wrong, we skip the start bytes,
|
|
20
|
-
// and iterating until we find new start bytes that match our special bytes. Then we try
|
|
21
|
-
// to read these values.
|
|
16
|
+
// - Transaction log uses length prefixed values, with a special 8 bytes to denote the end, and 8 for the start.
|
|
17
|
+
// - When transaction log is iterated on, if the bytes at the end (using the length prefix) don't match the end bytes OR the start bytes are wrong, we skip the start bytes, and iterating until we find new start bytes that match our special bytes. Then we try to read these values.
|
|
22
18
|
// - Each transaction entry has a byte for flags, one bit which denotes if the value is zipped or not.
|
|
23
19
|
// - Compresses the log after there are 3X entries than keys (and > 100)
|
|
24
20
|
// - Both dedupe keys, and zip
|
|
25
21
|
// - Assumes all files in rawStorage ending with .tx are transaction logs
|
|
26
22
|
// - Names files like `${generation}.tx`
|
|
27
|
-
// - When compressing, we increment the generation, and write to a new file, and delete
|
|
28
|
-
// any generations that are older than the new one
|
|
23
|
+
// - When compressing, we increment the generation, and write to a new file, and delete any generations that are older than the new one
|
|
29
24
|
// - On reading, we read from all generation files that exist, in case some are corrupted
|
|
30
25
|
// - On load, loads in all transaction logs, and stores all values in a Map<string, Buffer>
|
|
31
26
|
// - On writes immediately updates the in memory Map, and then writes to the transaction log
|
|
32
27
|
// - Caches the last transaction file name in memory
|
|
33
|
-
// - Makes sure all file system writes (but not Map updates) are done with fileLockSection,
|
|
34
|
-
//
|
|
35
|
-
// - Buffers pending appends in memory, so they can written all at once (after the first one
|
|
36
|
-
// is blocking in fileLockSection).
|
|
28
|
+
// - Makes sure all file system writes (but not Map updates) are done with fileLockSection, so they never overlap.
|
|
29
|
+
// - Buffers pending appends in memory, so they can written all at once (after the first one is blocking in fileLockSection).
|
|
37
30
|
|
|
38
31
|
UPDATE now we use chunks, because append is too slow.
|
|
39
32
|
|
|
@@ -93,9 +86,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
93
86
|
private rawStorage: IStorageRaw,
|
|
94
87
|
private debugName: string,
|
|
95
88
|
private writeDelay = WRITE_DELAY,
|
|
96
|
-
// When set, we poll the underlying storage and reload if another process/tab writes new
|
|
97
|
-
// chunk files. Off by default, as it costs a periodic disk scan and most collections are
|
|
98
|
-
// only written by a single process.
|
|
89
|
+
// When set, we poll the underlying storage and reload if another process/tab writes new chunk files. Off by default, as it costs a periodic disk scan and most collections are only written by a single process.
|
|
99
90
|
private resyncFromDisk = false
|
|
100
91
|
) {
|
|
101
92
|
TransactionStorage.allStorage.push(this);
|
|
@@ -241,7 +232,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
241
232
|
private updatePendingAppends = throttleFunction(100, async () => {
|
|
242
233
|
let appendCount = this.pendingAppends.length + this.extraAppends;
|
|
243
234
|
let group = `Transaction (${this.debugName})`;
|
|
244
|
-
//console.log(`Update pending appends ${group}: ${appendCount}`);
|
|
235
|
+
// console.log(`Update pending appends ${group}: ${appendCount}`);
|
|
245
236
|
if (!appendCount) {
|
|
246
237
|
setPending(group, "");
|
|
247
238
|
return;
|
|
@@ -322,7 +313,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
322
313
|
const fullFile = await this.rawStorage.get(filename);
|
|
323
314
|
if (!fullFile) return [];
|
|
324
315
|
if (fullFile.length < 4) {
|
|
325
|
-
//console.error(`Transaction in ${this.debugName} file ${filename} is too small, skipping`);
|
|
316
|
+
// console.error(`Transaction in ${this.debugName} file ${filename} is too small, skipping`);
|
|
326
317
|
return [];
|
|
327
318
|
}
|
|
328
319
|
size.value += fullFile.length;
|
|
@@ -549,8 +540,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
549
540
|
existingDiskEntries = existingDiskEntries.filter(x => x.endsWith(CHUNK_EXT));
|
|
550
541
|
let compressNow = force || (
|
|
551
542
|
this.entryCount > 100 && this.entryCount > this.cache.size * 3
|
|
552
|
-
// NOTE: This compress check breaks down if we only have very large values, but... those
|
|
553
|
-
// don't work ANYWAYS (it is better to use one file per value instead).
|
|
543
|
+
// NOTE: This compress check breaks down if we only have very large values, but... those don't work ANYWAYS (it is better to use one file per value instead).
|
|
554
544
|
// - Maybe we should throw, or at least warn, on sets of value > 1MB,
|
|
555
545
|
// at which point they should just use a file per value
|
|
556
546
|
|| existingDiskEntries.length > Math.max(10, Math.ceil(this.entryCount / 1000))
|
|
@@ -559,8 +549,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
559
549
|
if (!compressNow) return;
|
|
560
550
|
console.log(`Compressing ${this.debugName} transaction log, ${this.entryCount} entries, ${this.cache.size} keys`);
|
|
561
551
|
|
|
562
|
-
// Load off disk, in case there are other writes. We still race with them, but at least
|
|
563
|
-
// this reduces the race condition considerably
|
|
552
|
+
// Load off disk, in case there are other writes. We still race with them, but at least this reduces the race condition considerably
|
|
564
553
|
existingDiskEntries = await this.loadAllTransactions();
|
|
565
554
|
|
|
566
555
|
this.entryCount = this.cache.size;
|
|
@@ -598,9 +587,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
598
587
|
}
|
|
599
588
|
}
|
|
600
589
|
|
|
601
|
-
// This is the ONLY time we can delete old files, as we know for sure the new file has all of our data.
|
|
602
|
-
// Any future readers won't know this, unless they write it themselves (or unless they audit it against
|
|
603
|
-
// the other generations, which is annoying).
|
|
590
|
+
// This is the ONLY time we can delete old files, as we know for sure the new file has all of our data. Any future readers won't know this, unless they write it themselves (or unless they audit it against the other generations, which is annoying).
|
|
604
591
|
for (const file of existingDiskEntries) {
|
|
605
592
|
await this.rawStorage.remove(file);
|
|
606
593
|
this.diskFiles.delete(file);
|