@peers-app/peers-sdk 0.30.0 → 0.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/contracts/contract-resolvers.js +1 -0
  2. package/dist/contracts/index.d.ts +1 -0
  3. package/dist/contracts/index.js +1 -0
  4. package/dist/contracts/system/file-storage-contract.d.ts +170 -0
  5. package/dist/contracts/system/file-storage-contract.js +123 -0
  6. package/dist/contracts/system/files-contract.d.ts +5 -1
  7. package/dist/contracts/system/files-contract.js +50 -9
  8. package/dist/contracts/system/files-shared.d.ts +68 -3
  9. package/dist/contracts/system/files-shared.js +65 -5
  10. package/dist/data/derived-row-id.d.ts +26 -0
  11. package/dist/data/derived-row-id.js +47 -0
  12. package/dist/data/files/chunk-store.d.ts +119 -0
  13. package/dist/data/files/chunk-store.js +112 -0
  14. package/dist/data/files/file-read-stream.d.ts +32 -3
  15. package/dist/data/files/file-read-stream.js +44 -29
  16. package/dist/data/files/file-rows.d.ts +77 -0
  17. package/dist/data/files/file-rows.js +182 -0
  18. package/dist/data/files/file-write-stream.d.ts +27 -0
  19. package/dist/data/files/file-write-stream.js +67 -42
  20. package/dist/data/files/file.types.d.ts +59 -23
  21. package/dist/data/files/file.types.js +54 -51
  22. package/dist/data/files/files.d.ts +28 -24
  23. package/dist/data/files/files.js +71 -85
  24. package/dist/data/files/index.d.ts +2 -0
  25. package/dist/data/files/index.js +2 -0
  26. package/dist/data/groups.d.ts +13 -0
  27. package/dist/data/groups.js +6 -0
  28. package/dist/data/index.d.ts +1 -0
  29. package/dist/data/index.js +1 -0
  30. package/dist/data/orm/write-checks.d.ts +15 -0
  31. package/dist/data/orm/write-checks.js +21 -0
  32. package/dist/define/system-contracts.d.ts +9 -2
  33. package/dist/define/system-contracts.js +6 -2
  34. package/dist/device-pairing/types.d.ts +2 -2
  35. package/dist/invites/invite-messages.d.ts +38 -0
  36. package/dist/package-installer/package-installer.d.ts +3 -5
  37. package/dist/package-installer/package-installer.js +5 -21
  38. package/dist/package-installer/package-propagation.js +6 -17
  39. package/dist/system-ids.d.ts +2 -0
  40. package/dist/system-ids.js +3 -1
  41. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DERIVED_ROW_ID_PREFIX = void 0;
4
+ exports.derivedRowId = derivedRowId;
5
+ exports.isDerivedRowId = isDerivedRowId;
6
+ /**
7
+ * Row ids derived from a row's own values, the same on every device. `Files` is the first user;
8
+ * a later `defineTable({ idFrom })` declaration derives its ids the same way, so no id changes.
9
+ *
10
+ * Kept free of heavy imports, like `signed-row-id.ts`, so package code can compute them.
11
+ */
12
+ const peer_id_1 = require("../peer-id");
13
+ /**
14
+ * Leading characters of every derived row id. Every derived prefix starts `000`, while `newid()`
15
+ * ids start with their creation time, so the two can't collide.
16
+ */
17
+ exports.DERIVED_ROW_ID_PREFIX = "000r";
18
+ /**
19
+ * The id of a row derived from its values: a hash of `["row/1", tableName, values]` as JSON,
20
+ * after {@link DERIVED_ROW_ID_PREFIX}. The encoding is frozen: ids derived this way are stored
21
+ * on every device, so changing it would orphan them.
22
+ *
23
+ * The hash part is 21 base-36 characters (about 108 bits). Taking over an existing id needs a
24
+ * second preimage (about 2^108 work); crafting two rows with one id costs about 2^54.
25
+ *
26
+ * @param tableName - The table's name.
27
+ * @param values - The values the id is derived from, in a fixed order.
28
+ * @throws When a value isn't a string, a finite number or a boolean.
29
+ */
30
+ function derivedRowId(tableName, values) {
31
+ for (const value of values) {
32
+ if (typeof value !== "string" &&
33
+ typeof value !== "boolean" &&
34
+ !(typeof value === "number" && Number.isFinite(value))) {
35
+ throw new Error(`Table ${tableName}: a derived id's values must be strings, numbers or booleans`);
36
+ }
37
+ }
38
+ return (0, peer_id_1.deterministicPeerId)(exports.DERIVED_ROW_ID_PREFIX, JSON.stringify(["row/1", tableName, values]));
39
+ }
40
+ /**
41
+ * Whether `id` has the form of a {@link derivedRowId}: a hint, not a proof that it was derived.
42
+ *
43
+ * @param id - A candidate row id.
44
+ */
45
+ function isDerivedRowId(id) {
46
+ return typeof id === "string" && id.length === 25 && id.startsWith(exports.DERIVED_ROW_ID_PREFIX);
47
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Where a device keeps file chunks, by hash. Each host supplies one store per account
3
+ * ({@link setChunkStore}): an encrypted SQLite database on Node hosts, IndexedDB in the browser.
4
+ * Chunks are deduplicated across the account's contexts: a chunk three groups list is stored once.
5
+ */
6
+ /** One stored chunk, as {@link ChunkStore.list} describes it. */
7
+ export interface IStoredChunk {
8
+ /** The chunk's SHA-256 hash, as `hashBytes` gives it. */
9
+ hash: string;
10
+ /** Its length in bytes. */
11
+ size: number;
12
+ /** When it was last stored, in milliseconds by this device's clock. */
13
+ storedAt: number;
14
+ }
15
+ /** A device's chunk storage for one account. */
16
+ export interface ChunkStore {
17
+ /**
18
+ * Whether a chunk is stored.
19
+ *
20
+ * @param hash - The chunk's hash.
21
+ */
22
+ has(hash: string): Promise<boolean>;
23
+ /**
24
+ * A stored chunk's bytes.
25
+ *
26
+ * @param hash - The chunk's hash.
27
+ * @returns The bytes, or undefined when the chunk isn't stored.
28
+ */
29
+ get(hash: string): Promise<Uint8Array | undefined>;
30
+ /**
31
+ * Store a chunk. Storing one that's already there keeps its bytes and refreshes `storedAt`, so
32
+ * a new file reusing an old chunk protects it from a sweep.
33
+ *
34
+ * @param hash - The chunk's hash; the caller has checked it against the bytes.
35
+ * @param bytes - The chunk.
36
+ */
37
+ put(hash: string, bytes: Uint8Array): Promise<void>;
38
+ /**
39
+ * Delete chunks, each only if it was stored before `storedBefore`. The age check and the delete
40
+ * are one step, so a concurrent {@link put} that refreshes a chunk wins.
41
+ *
42
+ * @param hashes - The chunks to delete; missing ones are skipped.
43
+ * @param storedBefore - Keep any chunk stored at or after this time.
44
+ * @returns How many chunks were deleted.
45
+ */
46
+ delete(hashes: readonly string[], storedBefore: number): Promise<number>;
47
+ /**
48
+ * Delete on an already-open store without yielding, so collection's liveness check and
49
+ * deletion are one synchronous step. Node stores provide this; IndexedDB cannot.
50
+ */
51
+ deleteSync?(hashes: readonly string[], storedBefore: number): IChunkStoreStats;
52
+ /**
53
+ * A page of stored chunks, in hash order.
54
+ *
55
+ * @param opts.after - Start after this hash; from the first when omitted.
56
+ * @param opts.limit - Most chunks to return (default 1,000).
57
+ */
58
+ list(opts?: {
59
+ after?: string;
60
+ limit?: number;
61
+ }): Promise<IStoredChunk[]>;
62
+ /** How many chunks are stored, and their total size in bytes. */
63
+ stats(): Promise<IChunkStoreStats>;
64
+ }
65
+ /** A chunk store's totals. */
66
+ export interface IChunkStoreStats {
67
+ /** Chunks stored. */
68
+ count: number;
69
+ /** Their total size in bytes. */
70
+ bytes: number;
71
+ }
72
+ /** Most chunks one {@link ChunkStore.list} page returns by default. */
73
+ export declare const CHUNK_LIST_PAGE = 1000;
74
+ /** A {@link ChunkStore} in memory, for tests and ephemeral hosts. */
75
+ export declare class MemoryChunkStore implements ChunkStore {
76
+ private readonly now;
77
+ private readonly chunks;
78
+ /** @param now - The clock `storedAt` comes from. */
79
+ constructor(now?: () => number);
80
+ has(hash: string): Promise<boolean>;
81
+ get(hash: string): Promise<Uint8Array | undefined>;
82
+ put(hash: string, bytes: Uint8Array): Promise<void>;
83
+ delete(hashes: readonly string[], storedBefore: number): Promise<number>;
84
+ /** Delete without yielding, using the same age check as the persistent Node store. */
85
+ deleteSync(hashes: readonly string[], storedBefore: number): IChunkStoreStats;
86
+ list(opts?: {
87
+ after?: string;
88
+ limit?: number;
89
+ }): Promise<IStoredChunk[]>;
90
+ stats(): Promise<IChunkStoreStats>;
91
+ }
92
+ /**
93
+ * Set this process's chunk store. Reads and writes waiting for one continue.
94
+ *
95
+ * @param store - The account's chunk store.
96
+ */
97
+ export declare function setChunkStore(store: ChunkStore): void;
98
+ /** This process's chunk store, once {@link setChunkStore} has set it. */
99
+ export declare function getChunkStore(): Promise<ChunkStore>;
100
+ /**
101
+ * Fetches a chunk this device lacks from a connected device, checks its hash and stores it.
102
+ * Resolves null when no device has it.
103
+ */
104
+ export type ChunkDownloader = (hash: string) => Promise<Uint8Array | null>;
105
+ /**
106
+ * Set how chunks this device lacks are fetched. peers-device installs the connection manager's.
107
+ *
108
+ * @param downloader - The downloader, or null for none.
109
+ */
110
+ export declare function setChunkDownloader(downloader: ChunkDownloader | null): void;
111
+ /**
112
+ * Fetch a chunk this device lacks from a connected device; the downloader stores it.
113
+ *
114
+ * @param hash - The chunk's hash.
115
+ * @returns The bytes, or null when no device has it or there's no downloader.
116
+ */
117
+ export declare function downloadChunk(hash: string): Promise<Uint8Array | null>;
118
+ /** Forget the chunk store and downloader (for tests). */
119
+ export declare function resetChunkStore(): void;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Where a device keeps file chunks, by hash. Each host supplies one store per account
4
+ * ({@link setChunkStore}): an encrypted SQLite database on Node hosts, IndexedDB in the browser.
5
+ * Chunks are deduplicated across the account's contexts: a chunk three groups list is stored once.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.MemoryChunkStore = exports.CHUNK_LIST_PAGE = void 0;
9
+ exports.setChunkStore = setChunkStore;
10
+ exports.getChunkStore = getChunkStore;
11
+ exports.setChunkDownloader = setChunkDownloader;
12
+ exports.downloadChunk = downloadChunk;
13
+ exports.resetChunkStore = resetChunkStore;
14
+ /** Most chunks one {@link ChunkStore.list} page returns by default. */
15
+ exports.CHUNK_LIST_PAGE = 1000;
16
+ /** A {@link ChunkStore} in memory, for tests and ephemeral hosts. */
17
+ class MemoryChunkStore {
18
+ now;
19
+ chunks = new Map();
20
+ /** @param now - The clock `storedAt` comes from. */
21
+ constructor(now = Date.now) {
22
+ this.now = now;
23
+ }
24
+ async has(hash) {
25
+ return this.chunks.has(hash);
26
+ }
27
+ async get(hash) {
28
+ return this.chunks.get(hash)?.bytes;
29
+ }
30
+ async put(hash, bytes) {
31
+ const stored = this.chunks.get(hash);
32
+ this.chunks.set(hash, { bytes: stored?.bytes ?? new Uint8Array(bytes), storedAt: this.now() });
33
+ }
34
+ async delete(hashes, storedBefore) {
35
+ return this.deleteSync(hashes, storedBefore).count;
36
+ }
37
+ /** Delete without yielding, using the same age check as the persistent Node store. */
38
+ deleteSync(hashes, storedBefore) {
39
+ const deleted = { count: 0, bytes: 0 };
40
+ for (const hash of hashes) {
41
+ const stored = this.chunks.get(hash);
42
+ if (stored && stored.storedAt < storedBefore) {
43
+ this.chunks.delete(hash);
44
+ deleted.count++;
45
+ deleted.bytes += stored.bytes.length;
46
+ }
47
+ }
48
+ return deleted;
49
+ }
50
+ async list(opts) {
51
+ const limit = opts?.limit ?? exports.CHUNK_LIST_PAGE;
52
+ return [...this.chunks.keys()]
53
+ .filter((hash) => opts?.after === undefined || hash > opts.after)
54
+ .sort()
55
+ .slice(0, limit)
56
+ .map((hash) => {
57
+ const { bytes, storedAt } = this.chunks.get(hash);
58
+ return { hash, size: bytes.length, storedAt };
59
+ });
60
+ }
61
+ async stats() {
62
+ let bytes = 0;
63
+ for (const chunk of this.chunks.values())
64
+ bytes += chunk.bytes.length;
65
+ return { count: this.chunks.size, bytes };
66
+ }
67
+ }
68
+ exports.MemoryChunkStore = MemoryChunkStore;
69
+ let chunkStore = null;
70
+ let chunkStoreWaiters = [];
71
+ /**
72
+ * Set this process's chunk store. Reads and writes waiting for one continue.
73
+ *
74
+ * @param store - The account's chunk store.
75
+ */
76
+ function setChunkStore(store) {
77
+ chunkStore = store;
78
+ const waiters = chunkStoreWaiters;
79
+ chunkStoreWaiters = [];
80
+ for (const resolve of waiters)
81
+ resolve(store);
82
+ }
83
+ /** This process's chunk store, once {@link setChunkStore} has set it. */
84
+ function getChunkStore() {
85
+ if (chunkStore)
86
+ return Promise.resolve(chunkStore);
87
+ return new Promise((resolve) => chunkStoreWaiters.push(resolve));
88
+ }
89
+ let chunkDownloader = null;
90
+ /**
91
+ * Set how chunks this device lacks are fetched. peers-device installs the connection manager's.
92
+ *
93
+ * @param downloader - The downloader, or null for none.
94
+ */
95
+ function setChunkDownloader(downloader) {
96
+ chunkDownloader = downloader;
97
+ }
98
+ /**
99
+ * Fetch a chunk this device lacks from a connected device; the downloader stores it.
100
+ *
101
+ * @param hash - The chunk's hash.
102
+ * @returns The bytes, or null when no device has it or there's no downloader.
103
+ */
104
+ async function downloadChunk(hash) {
105
+ return chunkDownloader ? chunkDownloader(hash) : null;
106
+ }
107
+ /** Forget the chunk store and downloader (for tests). */
108
+ function resetChunkStore() {
109
+ chunkStore = null;
110
+ chunkStoreWaiters = [];
111
+ chunkDownloader = null;
112
+ }
@@ -1,8 +1,11 @@
1
1
  import { type IFile } from "./file.types";
2
- import type { FilesTable } from "./files";
2
+ /**
3
+ * Reads one file's bytes chunk by chunk, from this device's chunks or, for a chunk it lacks, from
4
+ * a connected device. Each chunk is checked against its hash unless verification is skipped.
5
+ * Open one with {@link FilesTable.openReadStream}.
6
+ */
3
7
  export declare class FileReadStream {
4
8
  private fileRecord;
5
- private fileTable;
6
9
  private preloadChunksCount;
7
10
  private skipVerification;
8
11
  private chunkHashes;
@@ -12,17 +15,43 @@ export declare class FileReadStream {
12
15
  private positionInChunk;
13
16
  private totalPosition;
14
17
  private eof;
15
- constructor(fileRecord: IFile, fileTable: FilesTable, preloadChunksCount?: number, skipVerification?: boolean);
18
+ /**
19
+ * @param fileRecord - The file's row.
20
+ * @param preloadChunksCount - Chunks to fetch ahead of the reader, counting the current one.
21
+ * @param skipVerification - Skip hash checks (only for trusted local data).
22
+ */
23
+ constructor(fileRecord: IFile, preloadChunksCount?: number, skipVerification?: boolean);
16
24
  private loadChunkHashes;
17
25
  private loadChunkByIndex;
18
26
  private preloadChunks;
19
27
  private loadCurrentChunk;
28
+ /**
29
+ * Read the next bytes.
30
+ *
31
+ * @param size - Most bytes to read; one chunk's worth when omitted.
32
+ * @returns The bytes, or null at the end of the file.
33
+ */
20
34
  read(size?: number): Promise<Uint8Array | null>;
35
+ /**
36
+ * Move to a byte position; the next read starts there.
37
+ *
38
+ * @param position - From 0 to the file's size.
39
+ */
21
40
  seek(position: number): Promise<void>;
41
+ /** A copy of the file's row. */
22
42
  getMetadata(): IFile;
43
+ /** The byte position the next read starts at. */
23
44
  getPosition(): number;
45
+ /** The whole file's size in bytes. */
24
46
  getFileSize(): number;
47
+ /** Whether the stream is at the end of the file. */
25
48
  isEOF(): boolean;
49
+ /** Bytes between the position and the end of the file. */
26
50
  getBytesRemaining(): number;
51
+ /**
52
+ * Read the whole file from the start.
53
+ *
54
+ * @returns The bytes, or null when a chunk is missing everywhere or fails its hash check.
55
+ */
27
56
  readAll(): Promise<Uint8Array | null>;
28
57
  }
@@ -2,10 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FileReadStream = void 0;
4
4
  const keys_1 = require("../../keys");
5
+ const chunk_store_1 = require("./chunk-store");
5
6
  const file_types_1 = require("./file.types");
7
+ /**
8
+ * Reads one file's bytes chunk by chunk, from this device's chunks or, for a chunk it lacks, from
9
+ * a connected device. Each chunk is checked against its hash unless verification is skipped.
10
+ * Open one with {@link FilesTable.openReadStream}.
11
+ */
6
12
  class FileReadStream {
7
13
  fileRecord;
8
- fileTable;
9
14
  preloadChunksCount;
10
15
  skipVerification;
11
16
  chunkHashes = null;
@@ -15,9 +20,13 @@ class FileReadStream {
15
20
  positionInChunk = 0;
16
21
  totalPosition = 0;
17
22
  eof = false;
18
- constructor(fileRecord, fileTable, preloadChunksCount = 2, skipVerification = false) {
23
+ /**
24
+ * @param fileRecord - The file's row.
25
+ * @param preloadChunksCount - Chunks to fetch ahead of the reader, counting the current one.
26
+ * @param skipVerification - Skip hash checks (only for trusted local data).
27
+ */
28
+ constructor(fileRecord, preloadChunksCount = 2, skipVerification = false) {
19
29
  this.fileRecord = fileRecord;
20
- this.fileTable = fileTable;
21
30
  this.preloadChunksCount = preloadChunksCount;
22
31
  this.skipVerification = skipVerification;
23
32
  }
@@ -25,24 +34,19 @@ class FileReadStream {
25
34
  if (this.chunkHashes !== null) {
26
35
  return; // Already loaded
27
36
  }
28
- if (this.fileRecord.chunkHashes) {
29
- // Small file: chunk hashes stored directly in database
30
- this.chunkHashes = this.fileRecord.chunkHashes;
31
- }
32
- else if (this.fileRecord.indexFileId) {
33
- // Large file: chunk hashes stored in separate index file (recursively)
34
- this.chunkHashes = await this.fileTable.loadChunkHashesRecursively(this.fileRecord.indexFileId);
35
- }
36
- else {
37
- throw new Error(`File ${this.fileRecord.fileId} has neither chunkHashes nor indexFileId`);
37
+ const chunkHashes = this.fileRecord.chunkHashes ?? [];
38
+ if (chunkHashes.length === 0 && this.fileRecord.fileSize > 0) {
39
+ // Older builds kept a file over 1,000 chunks in index files, which are no longer read.
40
+ throw new Error(`File ${this.fileRecord.fileId} has no chunk list`);
38
41
  }
39
42
  if (!this.skipVerification) {
40
- const computedFileHash = (0, keys_1.hashBytes)(new TextEncoder().encode(JSON.stringify(this.chunkHashes)));
43
+ const computedFileHash = (0, file_types_1.fileHashOf)(chunkHashes);
41
44
  if (computedFileHash !== this.fileRecord.fileHash) {
42
45
  throw new Error(`File integrity check failed for ${this.fileRecord.fileId}: ` +
43
46
  `chunk hashes produce ${computedFileHash}, expected ${this.fileRecord.fileHash}`);
44
47
  }
45
48
  }
49
+ this.chunkHashes = chunkHashes;
46
50
  }
47
51
  async loadChunkByIndex(chunkIndex) {
48
52
  await this.loadChunkHashes();
@@ -51,19 +55,11 @@ class FileReadStream {
51
55
  throw new Error(`Chunk index ${chunkIndex} out of bounds`);
52
56
  }
53
57
  const chunkHash = chunkHashes[chunkIndex];
54
- const ops = await (0, file_types_1.getFileOps)();
55
- const chunkPath = `${file_types_1.CHUNKS_DIR}/${chunkHash}`;
56
- let chunk;
57
- if (await ops.fileExists(chunkPath)) {
58
- // Read from local storage
59
- chunk = await ops.readFile(chunkPath);
60
- }
61
- else {
62
- // Download from peer by hash
63
- chunk = (await ops.downloadFileChunk(chunkHash));
64
- if (!chunk) {
65
- throw new Error(`Chunk ${chunkHash} not found in storage or peers`);
66
- }
58
+ const store = await (0, chunk_store_1.getChunkStore)();
59
+ // Stored here, or downloaded from a connected device (which stores it).
60
+ const chunk = (await store.get(chunkHash)) ?? (await (0, chunk_store_1.downloadChunk)(chunkHash));
61
+ if (!chunk) {
62
+ throw new Error(`Chunk ${chunkHash} not found in storage or peers`);
67
63
  }
68
64
  // Verify chunk integrity (skip for trusted local reads)
69
65
  if (!this.skipVerification) {
@@ -109,6 +105,12 @@ class FileReadStream {
109
105
  this.currentChunkBuffer = await chunkPromise;
110
106
  this.chunkPromises.delete(this.currentChunkIndex);
111
107
  }
108
+ /**
109
+ * Read the next bytes.
110
+ *
111
+ * @param size - Most bytes to read; one chunk's worth when omitted.
112
+ * @returns The bytes, or null at the end of the file.
113
+ */
112
114
  async read(size) {
113
115
  if (this.eof) {
114
116
  return null;
@@ -146,6 +148,11 @@ class FileReadStream {
146
148
  await this.preloadChunks();
147
149
  return chunks.length > 0 ? new Uint8Array(Buffer.concat(chunks)) : null;
148
150
  }
151
+ /**
152
+ * Move to a byte position; the next read starts there.
153
+ *
154
+ * @param position - From 0 to the file's size.
155
+ */
149
156
  async seek(position) {
150
157
  if (position < 0) {
151
158
  throw new Error("Seek position cannot be negative");
@@ -167,22 +174,31 @@ class FileReadStream {
167
174
  this.chunkPromises.clear();
168
175
  await this.preloadChunks();
169
176
  }
177
+ /** A copy of the file's row. */
170
178
  getMetadata() {
171
179
  return { ...this.fileRecord };
172
180
  }
181
+ /** The byte position the next read starts at. */
173
182
  getPosition() {
174
183
  return this.totalPosition;
175
184
  }
185
+ /** The whole file's size in bytes. */
176
186
  getFileSize() {
177
187
  return this.fileRecord.fileSize;
178
188
  }
189
+ /** Whether the stream is at the end of the file. */
179
190
  isEOF() {
180
191
  return this.eof;
181
192
  }
193
+ /** Bytes between the position and the end of the file. */
182
194
  getBytesRemaining() {
183
195
  return this.fileRecord.fileSize - this.totalPosition;
184
196
  }
185
- // Read the entire file (for compatibility with existing getFile API)
197
+ /**
198
+ * Read the whole file from the start.
199
+ *
200
+ * @returns The bytes, or null when a chunk is missing everywhere or fails its hash check.
201
+ */
186
202
  async readAll() {
187
203
  await this.seek(0); // Start from beginning
188
204
  const chunks = [];
@@ -199,7 +215,6 @@ class FileReadStream {
199
215
  }
200
216
  catch (error) {
201
217
  // Only return null for chunk-specific errors (not found, download failed)
202
- // Rethrow other errors like index file issues
203
218
  if (error instanceof Error &&
204
219
  error.message.includes("Chunk") &&
205
220
  (error.message.includes("not found") || error.message.includes("integrity check failed"))) {
@@ -0,0 +1,77 @@
1
+ import { type IFile } from "./file.types";
2
+ /** What a file's id is derived from. */
3
+ export interface IFileIdentity {
4
+ fileHash: string;
5
+ fileSize: number;
6
+ name: string;
7
+ /** Empty when unknown; omitted counts as empty. */
8
+ mimeType?: string;
9
+ keep: boolean;
10
+ }
11
+ /**
12
+ * The id of a file: derived from its hash, size, name, MIME type and `keep`, so saving the same
13
+ * content with the same description gives the same file on every device. Give the name and MIME
14
+ * type as storage keeps them (`FileWriteStream` normalizes them), or other devices, which
15
+ * rederive the id from the stored row, refuse it.
16
+ *
17
+ * @param file - The file's hash, size, name, MIME type and `keep`.
18
+ */
19
+ export declare function fileIdFor(file: IFileIdentity): string;
20
+ /**
21
+ * Whether a row is a legacy file: one saved under a random id, before ids were derived from
22
+ * content. Legacy rows are admitted as they are, never change, and are always kept.
23
+ *
24
+ * @param fileId - The row's id.
25
+ */
26
+ export declare function isLegacyFileId(fileId: string): boolean;
27
+ /**
28
+ * Whether a legacy row is a package bundle the installer saved. The runtime owns these, so they
29
+ * are collected once nothing references them, and their rows may be deleted.
30
+ *
31
+ * @param file - The row's id and name.
32
+ */
33
+ export declare function isLegacyBundleFile(file: Pick<IFile, "fileId" | "name">): boolean;
34
+ /**
35
+ * Whether this is a runtime-owned package bundle: a legacy bundle, or a derived file with
36
+ * an installer bundle name, JavaScript MIME type and keep=false. Read keep through its
37
+ * identity when an older device relayed the row without that column.
38
+ */
39
+ export declare function isRuntimeBundleFile(file: IFile): boolean;
40
+ /**
41
+ * Whether a file is kept: never collected, even with nothing referencing it. A legacy row always
42
+ * is. A row relayed by a build that predates `keep` arrives without it; its id, derived from
43
+ * `keep`, then decides, and a row whose id says neither is kept.
44
+ *
45
+ * @param file - The row.
46
+ */
47
+ export declare function isKeptFile(file: IFile): boolean;
48
+ /** Whether an unreferenced file may expire, including the runtime's bundle exception. */
49
+ export declare function isCollectableFile(file: IFile): boolean;
50
+ /**
51
+ * Whether two rows describe the same file, ignoring how an older build stored it (no `keep`, no
52
+ * MIME type).
53
+ *
54
+ * @param a - One row.
55
+ * @param b - The other.
56
+ */
57
+ export declare function isSameFile(a: IFile, b: IFile): boolean;
58
+ /**
59
+ * Why `file` can't be stored as a new row under `recordId`, or undefined when it can. A legacy
60
+ * row needs only a size within the limit. Any other must have its derived id, a chunk list of
61
+ * well-formed hashes, one per started MiB, and the `fileHash` of that list.
62
+ *
63
+ * @param file - The candidate row.
64
+ * @param recordId - The record id it's written under.
65
+ */
66
+ export declare function fileRowProblem(file: IFile, recordId: string): string | undefined;
67
+ /**
68
+ * The `Files` write check: a new row must pass {@link fileRowProblem}; a stored row never
69
+ * changes (rewriting the same file is allowed); and only a stored runtime bundle row may be
70
+ * deleted. Unknown legacy ids remain allowed.
71
+ *
72
+ * @param existing - The row as stored, if any.
73
+ * @param candidate - The row to store, or undefined for a delete.
74
+ * @param recordId - The record written.
75
+ * @throws A {@link writeNotAuthorized} refusal.
76
+ */
77
+ export declare function checkFileWrite(existing: IFile | undefined, candidate: IFile | undefined, recordId: string): void;