@peers-app/peers-sdk 0.29.2 → 0.30.1
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/dist/contracts/contract-resolvers.js +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/system/file-storage-contract.d.ts +76 -0
- package/dist/contracts/system/file-storage-contract.js +75 -0
- package/dist/contracts/system/files-contract.d.ts +5 -1
- package/dist/contracts/system/files-contract.js +50 -9
- package/dist/contracts/system/files-shared.d.ts +68 -3
- package/dist/contracts/system/files-shared.js +65 -5
- package/dist/contracts/validate.d.ts +1 -0
- package/dist/contracts/validate.js +6 -1
- package/dist/data/derived-row-id.d.ts +26 -0
- package/dist/data/derived-row-id.js +47 -0
- package/dist/data/files/chunk-store.d.ts +112 -0
- package/dist/data/files/chunk-store.js +107 -0
- package/dist/data/files/file-read-stream.d.ts +32 -3
- package/dist/data/files/file-read-stream.js +44 -29
- package/dist/data/files/file-rows.d.ts +68 -0
- package/dist/data/files/file-rows.js +158 -0
- package/dist/data/files/file-write-stream.d.ts +26 -0
- package/dist/data/files/file-write-stream.js +66 -42
- package/dist/data/files/file.types.d.ts +44 -23
- package/dist/data/files/file.types.js +33 -51
- package/dist/data/files/files.d.ts +27 -24
- package/dist/data/files/files.js +63 -85
- package/dist/data/files/index.d.ts +2 -0
- package/dist/data/files/index.js +2 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/messages.d.ts +21 -0
- package/dist/data/messages.js +21 -1
- package/dist/data/orm/sql.data-source.d.ts +2 -1
- package/dist/data/orm/sql.data-source.js +1 -0
- package/dist/data/orm/types.js +12 -0
- package/dist/data/package-versions.js +3 -3
- package/dist/define/index.d.ts +1 -1
- package/dist/define/index.js +2 -1
- package/dist/define/system-contracts.d.ts +9 -2
- package/dist/define/system-contracts.js +6 -2
- package/dist/device-pairing/types.d.ts +2 -2
- package/dist/package-build/build-package.d.ts +12 -0
- package/dist/package-build/build-package.js +45 -6
- package/dist/package-build/cli.js +8 -0
- package/dist/package-build/contract-source.js +7 -5
- package/dist/package-installer/package-installer.d.ts +3 -5
- package/dist/package-installer/package-installer.js +5 -21
- package/dist/package-installer/package-propagation.js +6 -17
- package/dist/system-ids.d.ts +2 -0
- package/dist/system-ids.js +3 -1
- package/dist/types/field-type.d.ts +14 -0
- package/dist/types/field-type.js +14 -1
- package/dist/types/zod-types.d.ts +10 -0
- package/dist/types/zod-types.js +25 -7
- package/package.json +1 -1
|
@@ -211,6 +211,7 @@ async function isSelfTrustedContractCaller(userContext, remoteIdentity) {
|
|
|
211
211
|
}
|
|
212
212
|
const permissionDenied = (call, reason) => new Error(`Permission denied: ${call.member} '${call.name}'.${call.operation} on contract ${call.contractId}${reason ? ` (${reason})` : ""}`);
|
|
213
213
|
const localOnlyContractIds = new Set([
|
|
214
|
+
system_ids_1.fileStorageContractId,
|
|
214
215
|
system_ids_1.filesContractId,
|
|
215
216
|
system_ids_1.groupMembersContractId,
|
|
216
217
|
system_ids_1.hostToolsContractId,
|
|
@@ -19,6 +19,7 @@ export * from "./system/app-updates-contract";
|
|
|
19
19
|
export { CONTRACT_CATALOG_VERSION, contractCatalogProviderPackageId, getContractCatalogDefinition, type IContractCatalogConsumer, type IContractDefinitionWithPolicy, type IContractProviderInfo, type IContractToolPolicy, type IExposedContractInfo, type IExposedToolInfo, type IGetContractDefinitionInput, type IListContractProvidersInput, } from "./system/contract-catalog-contract";
|
|
20
20
|
export * from "./system/desktop-tools-contract";
|
|
21
21
|
export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
|
|
22
|
+
export * from "./system/file-storage-contract";
|
|
22
23
|
export * from "./system/files-contract";
|
|
23
24
|
export * from "./system/group-members-contract";
|
|
24
25
|
export * from "./system/host-shell-contract";
|
package/dist/contracts/index.js
CHANGED
|
@@ -105,6 +105,7 @@ Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_TOOLS", { enumerable: true
|
|
|
105
105
|
Object.defineProperty(exports, "deviceOperationsProviderPackageId", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsProviderPackageId; } });
|
|
106
106
|
Object.defineProperty(exports, "deviceOperationsToolIds", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsToolIds; } });
|
|
107
107
|
Object.defineProperty(exports, "getDeviceOperationsContractDefinition", { enumerable: true, get: function () { return device_operations_contract_1.getDeviceOperationsContractDefinition; } });
|
|
108
|
+
__exportStar(require("./system/file-storage-contract"), exports);
|
|
108
109
|
__exportStar(require("./system/files-contract"), exports);
|
|
109
110
|
__exportStar(require("./system/group-members-contract"), exports);
|
|
110
111
|
__exportStar(require("./system/host-shell-contract"), exports);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { IChunkStoreStats } from "../../data/files/chunk-store";
|
|
2
|
+
import type { IContractDefinition } from "../types";
|
|
3
|
+
/** Version of the local-only System File Storage contract. */
|
|
4
|
+
export declare const FILE_STORAGE_CONTRACT_VERSION = 1;
|
|
5
|
+
/** What one migration of legacy chunks into the account's chunk store found and did. */
|
|
6
|
+
export interface IChunkMigrationResult {
|
|
7
|
+
/** Whether every chunk the account's files list, that the legacy store had, is now stored. */
|
|
8
|
+
verified: boolean;
|
|
9
|
+
/** Why it stopped before copying: rows it can't migrate, or contexts it can't read or index. */
|
|
10
|
+
problems: string[];
|
|
11
|
+
/** Contexts read: the personal one and every group, disabled ones included. */
|
|
12
|
+
contexts: number;
|
|
13
|
+
/** Files rows read. */
|
|
14
|
+
files: number;
|
|
15
|
+
/** Distinct chunks those rows list, less the skipped ones. */
|
|
16
|
+
needed: number;
|
|
17
|
+
/** Chunks only orphaned legacy bundle rows list, which aren't copied. */
|
|
18
|
+
skippedBundleChunks: number;
|
|
19
|
+
/** Needed chunks the store already had. */
|
|
20
|
+
alreadyStored: number;
|
|
21
|
+
/** Needed chunks copied from the legacy store. */
|
|
22
|
+
copied: number;
|
|
23
|
+
/** Needed chunks the legacy store didn't have: this device never had them. */
|
|
24
|
+
missing: number;
|
|
25
|
+
/** Needed chunks the legacy store held with the wrong bytes (a write cut short). */
|
|
26
|
+
corrupt: number;
|
|
27
|
+
/** When it started, by this device's clock. */
|
|
28
|
+
startedAt: number;
|
|
29
|
+
/** When it finished, by this device's clock. */
|
|
30
|
+
finishedAt: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Where a migration stands: `not-needed` when the device has no legacy store (or it's removed),
|
|
34
|
+
* `pending` before the first run, then `running`, `blocked` (see the result's problems) or
|
|
35
|
+
* `verified`.
|
|
36
|
+
*/
|
|
37
|
+
export type ChunkMigrationState = "not-needed" | "pending" | "running" | "blocked" | "verified";
|
|
38
|
+
/** This device's chunk storage for the signed-in account. */
|
|
39
|
+
export interface IFileStorageStatus {
|
|
40
|
+
/** The account's chunk store. */
|
|
41
|
+
store: IChunkStoreStats;
|
|
42
|
+
/** The legacy store, while it exists. */
|
|
43
|
+
legacy?: {
|
|
44
|
+
/** Where it is, such as the folder's path. */
|
|
45
|
+
location: string;
|
|
46
|
+
/** Accounts on this device whose migration is verified. */
|
|
47
|
+
migratedAccounts: string[];
|
|
48
|
+
/** Accounts on this device still to migrate; it can be removed once none are left. */
|
|
49
|
+
waitingAccounts: string[];
|
|
50
|
+
};
|
|
51
|
+
/** The migration of the legacy store. */
|
|
52
|
+
migration: {
|
|
53
|
+
state: ChunkMigrationState;
|
|
54
|
+
/** The last run this session, if any. */
|
|
55
|
+
lastRun?: IChunkMigrationResult;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The tools of System File Storage v1. */
|
|
59
|
+
export interface IFileStorageConsumer {
|
|
60
|
+
tools: {
|
|
61
|
+
/** The chunk store's size, the legacy store, and where the migration stands. */
|
|
62
|
+
getStatus(): Promise<IFileStorageStatus>;
|
|
63
|
+
/** Copy this account's chunks out of the legacy store now, and verify them. */
|
|
64
|
+
migrate(): Promise<IChunkMigrationResult>;
|
|
65
|
+
/** Copy again, then delete the legacy store once every account on the device has migrated. */
|
|
66
|
+
removeLegacyChunks(): Promise<{
|
|
67
|
+
removed: boolean;
|
|
68
|
+
location?: string;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Return the System File Storage v1 declaration: this device's chunk store and the migration
|
|
74
|
+
* of its legacy chunks. Only the user's own screens and CLI on this device may call it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getFileStorageContractDefinition(): IContractDefinition;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FILE_STORAGE_CONTRACT_VERSION = void 0;
|
|
4
|
+
exports.getFileStorageContractDefinition = getFileStorageContractDefinition;
|
|
5
|
+
const system_ids_1 = require("../../system-ids");
|
|
6
|
+
const field_type_1 = require("../../types/field-type");
|
|
7
|
+
const system_contracts_1 = require("../system-contracts");
|
|
8
|
+
/** Version of the local-only System File Storage contract. */
|
|
9
|
+
exports.FILE_STORAGE_CONTRACT_VERSION = 1;
|
|
10
|
+
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
11
|
+
const migrationResultFields = [
|
|
12
|
+
field("verified", field_type_1.FieldType.boolean, "Whether every chunk the legacy store had is now stored."),
|
|
13
|
+
field("problems", field_type_1.FieldType.any, "Why it stopped before copying, if it did."),
|
|
14
|
+
field("contexts", field_type_1.FieldType.number, "Contexts read."),
|
|
15
|
+
field("files", field_type_1.FieldType.number, "Files rows read."),
|
|
16
|
+
field("needed", field_type_1.FieldType.number, "Distinct chunks the rows list, less skipped ones."),
|
|
17
|
+
field("skippedBundleChunks", field_type_1.FieldType.number, "Chunks only orphaned bundle rows list."),
|
|
18
|
+
field("alreadyStored", field_type_1.FieldType.number, "Needed chunks already stored."),
|
|
19
|
+
field("copied", field_type_1.FieldType.number, "Needed chunks copied."),
|
|
20
|
+
field("missing", field_type_1.FieldType.number, "Needed chunks this device never had."),
|
|
21
|
+
field("corrupt", field_type_1.FieldType.number, "Needed chunks stored with the wrong bytes."),
|
|
22
|
+
field("startedAt", field_type_1.FieldType.number, "When it started."),
|
|
23
|
+
field("finishedAt", field_type_1.FieldType.number, "When it finished."),
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Return the System File Storage v1 declaration: this device's chunk store and the migration
|
|
27
|
+
* of its legacy chunks. Only the user's own screens and CLI on this device may call it.
|
|
28
|
+
*/
|
|
29
|
+
function getFileStorageContractDefinition() {
|
|
30
|
+
return {
|
|
31
|
+
contractId: system_ids_1.fileStorageContractId,
|
|
32
|
+
version: exports.FILE_STORAGE_CONTRACT_VERSION,
|
|
33
|
+
devTag: "dev",
|
|
34
|
+
name: "System File Storage",
|
|
35
|
+
description: "This device's chunk store and the migration of its legacy chunks.",
|
|
36
|
+
tables: [],
|
|
37
|
+
observables: [],
|
|
38
|
+
events: [],
|
|
39
|
+
tools: [
|
|
40
|
+
{
|
|
41
|
+
name: "getStatus",
|
|
42
|
+
usageDescription: "Report the chunk store's size and the legacy migration's state.",
|
|
43
|
+
inputFields: [],
|
|
44
|
+
outputFields: [
|
|
45
|
+
field("store", field_type_1.FieldType.object, "Chunks stored and their total bytes."),
|
|
46
|
+
field("legacy", field_type_1.FieldType.object, "The legacy store, while it exists.", {
|
|
47
|
+
optional: true,
|
|
48
|
+
}),
|
|
49
|
+
field("migration", field_type_1.FieldType.object, "The migration's state and last run."),
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "migrate",
|
|
54
|
+
usageDescription: "Copy the account's chunks from the legacy store, and verify them.",
|
|
55
|
+
inputFields: [],
|
|
56
|
+
outputFields: migrationResultFields,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "removeLegacyChunks",
|
|
60
|
+
usageDescription: "Delete the legacy chunk store once every account on this device has migrated.",
|
|
61
|
+
inputFields: [],
|
|
62
|
+
outputFields: [
|
|
63
|
+
field("removed", field_type_1.FieldType.boolean, "False when there was nothing to remove."),
|
|
64
|
+
field("location", field_type_1.FieldType.string, "Where it was.", { optional: true }),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
(0, system_contracts_1.registerSystemContract)({
|
|
71
|
+
providerPackageId: system_ids_1.peersSystemPackageId,
|
|
72
|
+
contractId: system_ids_1.fileStorageContractId,
|
|
73
|
+
version: exports.FILE_STORAGE_CONTRACT_VERSION,
|
|
74
|
+
getDefinition: getFileStorageContractDefinition,
|
|
75
|
+
});
|
|
@@ -2,5 +2,9 @@ import type { IContractDefinition } from "../types";
|
|
|
2
2
|
export * from "./files-shared";
|
|
3
3
|
/** Version of the local-only System Files contract. */
|
|
4
4
|
export declare const FILES_CONTRACT_VERSION = 1;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Return the System Files v1 declaration: `save` and `read`, and a write session (`startSave`,
|
|
7
|
+
* `appendSave`, `finishSave`, `cancelSave`) for files over one call. Tool identities are derived
|
|
8
|
+
* by the catalog.
|
|
9
|
+
*/
|
|
6
10
|
export declare function getFilesContractDefinition(): IContractDefinition;
|
|
@@ -24,9 +24,22 @@ __exportStar(require("./files-shared"), exports);
|
|
|
24
24
|
/** Version of the local-only System Files contract. */
|
|
25
25
|
exports.FILES_CONTRACT_VERSION = 1;
|
|
26
26
|
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Return the System Files v1 declaration: `save` and `read`, and a write session (`startSave`,
|
|
29
|
+
* `appendSave`, `finishSave`, `cancelSave`) for files over one call. Tool identities are derived
|
|
30
|
+
* by the catalog.
|
|
31
|
+
*/
|
|
28
32
|
function getFilesContractDefinition() {
|
|
29
33
|
const size = `${files_shared_1.FILES_MAX_CALL_BYTES / (1024 * 1024)} MiB`;
|
|
34
|
+
const maxFile = `${files_shared_1.FILES_MAX_FILE_BYTES / (1024 * 1024 * 1024)} GiB`;
|
|
35
|
+
const sessionField = field("sessionId", field_type_1.FieldType.id, "The write session, from startSave.");
|
|
36
|
+
const savedFields = [
|
|
37
|
+
field("fileId", field_type_1.FieldType.id, "Id to read the file with."),
|
|
38
|
+
field("name", field_type_1.FieldType.string, "File name."),
|
|
39
|
+
field("fileSize", field_type_1.FieldType.number, "Size in bytes."),
|
|
40
|
+
field("fileHash", field_type_1.FieldType.string, "Hash of the file's chunk hashes."),
|
|
41
|
+
field("mimeType", field_type_1.FieldType.string, "MIME type.", { optional: true }),
|
|
42
|
+
];
|
|
30
43
|
return {
|
|
31
44
|
contractId: system_ids_1.filesContractId,
|
|
32
45
|
version: exports.FILES_CONTRACT_VERSION,
|
|
@@ -39,7 +52,7 @@ function getFilesContractDefinition() {
|
|
|
39
52
|
tools: [
|
|
40
53
|
{
|
|
41
54
|
name: "save",
|
|
42
|
-
usageDescription: `Save a
|
|
55
|
+
usageDescription: `Save a file of up to ${size} in this context and return its id.`,
|
|
43
56
|
inputFields: [
|
|
44
57
|
field("name", field_type_1.FieldType.string, "File name, such as avatar.jpg."),
|
|
45
58
|
field("mimeType", field_type_1.FieldType.string, "MIME type, such as image/jpeg.", {
|
|
@@ -47,13 +60,7 @@ function getFilesContractDefinition() {
|
|
|
47
60
|
}),
|
|
48
61
|
field("bytes", field_type_1.FieldType.blob, `The whole file, at most ${size}.`),
|
|
49
62
|
],
|
|
50
|
-
outputFields:
|
|
51
|
-
field("fileId", field_type_1.FieldType.id, "Id to read the file with."),
|
|
52
|
-
field("name", field_type_1.FieldType.string, "File name."),
|
|
53
|
-
field("fileSize", field_type_1.FieldType.number, "Size in bytes."),
|
|
54
|
-
field("fileHash", field_type_1.FieldType.string, "Hash of the file's chunk hashes."),
|
|
55
|
-
field("mimeType", field_type_1.FieldType.string, "MIME type.", { optional: true }),
|
|
56
|
-
],
|
|
63
|
+
outputFields: savedFields,
|
|
57
64
|
},
|
|
58
65
|
{
|
|
59
66
|
name: "read",
|
|
@@ -74,6 +81,40 @@ function getFilesContractDefinition() {
|
|
|
74
81
|
field("mimeType", field_type_1.FieldType.string, "MIME type.", { optional: true }),
|
|
75
82
|
],
|
|
76
83
|
},
|
|
84
|
+
{
|
|
85
|
+
name: "startSave",
|
|
86
|
+
usageDescription: `Start saving a file over ${size}, of up to ${maxFile}, in pieces.`,
|
|
87
|
+
inputFields: [
|
|
88
|
+
field("name", field_type_1.FieldType.string, "File name, such as video.mp4."),
|
|
89
|
+
field("mimeType", field_type_1.FieldType.string, "MIME type, such as video/mp4.", {
|
|
90
|
+
optional: true,
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
outputFields: [sessionField],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "appendSave",
|
|
97
|
+
usageDescription: `Add the next piece, of up to ${size}, to a file being saved.`,
|
|
98
|
+
inputFields: [
|
|
99
|
+
sessionField,
|
|
100
|
+
field("bytes", field_type_1.FieldType.blob, `The next bytes, at most ${size}.`),
|
|
101
|
+
],
|
|
102
|
+
outputFields: [field("bytesWritten", field_type_1.FieldType.number, "Bytes saved so far.")],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "finishSave",
|
|
106
|
+
usageDescription: "Finish saving a file and return its id.",
|
|
107
|
+
inputFields: [sessionField],
|
|
108
|
+
outputFields: savedFields,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "cancelSave",
|
|
112
|
+
usageDescription: "Stop saving a file; nothing is saved.",
|
|
113
|
+
inputFields: [sessionField],
|
|
114
|
+
outputFields: [
|
|
115
|
+
field("cancelled", field_type_1.FieldType.boolean, "False when the session had already ended."),
|
|
116
|
+
],
|
|
117
|
+
},
|
|
77
118
|
],
|
|
78
119
|
};
|
|
79
120
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { IContractConsumerBase } from "../contract-proxy";
|
|
2
|
-
/** Largest file `save` accepts,
|
|
2
|
+
/** Largest file `save` accepts, the most bytes one `appendSave` takes, and one `read` returns. */
|
|
3
3
|
export declare const FILES_MAX_CALL_BYTES: number;
|
|
4
|
+
/** Largest file Peers stores (1 GiB). Save one over {@link FILES_MAX_CALL_BYTES} in pieces. */
|
|
5
|
+
export declare const FILES_MAX_FILE_BYTES: number;
|
|
6
|
+
/**
|
|
7
|
+
* Largest file {@link readWholeFile} reads unless told otherwise (64 MiB), so a screen doesn't
|
|
8
|
+
* hold a large file in memory. Read a larger file, such as a video, in ranges.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FILES_WHOLE_READ_MAX_BYTES: number;
|
|
4
11
|
/** Input for {@link IFilesConsumer tools.save}. */
|
|
5
12
|
export interface IFilesSaveInput {
|
|
6
13
|
/** File name, such as `avatar.jpg`. Shown to people; not a path. */
|
|
@@ -19,6 +26,22 @@ export interface IFilesSaveResult {
|
|
|
19
26
|
fileHash: string;
|
|
20
27
|
mimeType?: string;
|
|
21
28
|
}
|
|
29
|
+
/** Input for {@link IFilesConsumer tools.startSave}: the file's name and MIME type. */
|
|
30
|
+
export type IFilesStartSaveInput = Omit<IFilesSaveInput, "bytes">;
|
|
31
|
+
/** A write session, for {@link IFilesConsumer tools.appendSave} and `finishSave`. */
|
|
32
|
+
export interface IFilesSaveSession {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
}
|
|
35
|
+
/** Input for {@link IFilesConsumer tools.appendSave}. */
|
|
36
|
+
export interface IFilesAppendSaveInput extends IFilesSaveSession {
|
|
37
|
+
/** The next bytes, at most {@link FILES_MAX_CALL_BYTES}. */
|
|
38
|
+
bytes: Uint8Array;
|
|
39
|
+
}
|
|
40
|
+
/** What {@link IFilesConsumer tools.appendSave} returns. */
|
|
41
|
+
export interface IFilesAppendSaveResult {
|
|
42
|
+
/** Bytes the session holds so far. */
|
|
43
|
+
bytesWritten: number;
|
|
44
|
+
}
|
|
22
45
|
/** Input for {@link IFilesConsumer tools.read}. */
|
|
23
46
|
export interface IFilesReadInput {
|
|
24
47
|
fileId: string;
|
|
@@ -42,18 +65,60 @@ export interface IFilesReadResult {
|
|
|
42
65
|
/**
|
|
43
66
|
* Consumer shape for System Files: save and read files in the caller's own data context.
|
|
44
67
|
* Files sync to the context's other devices, as every file does.
|
|
68
|
+
*
|
|
69
|
+
* A file over {@link FILES_MAX_CALL_BYTES} is saved in a write session: `startSave`, then
|
|
70
|
+
* `appendSave` for each piece in order, then `finishSave`. {@link saveWholeFile} does that. A
|
|
71
|
+
* session ends after 10 minutes without a call, or 12 hours after it started; `cancelSave` ends
|
|
72
|
+
* one early.
|
|
45
73
|
*/
|
|
46
74
|
export interface IFilesConsumer extends IContractConsumerBase {
|
|
47
75
|
tools: {
|
|
48
76
|
save(input: IFilesSaveInput): Promise<IFilesSaveResult>;
|
|
49
77
|
read(input: IFilesReadInput): Promise<IFilesReadResult>;
|
|
78
|
+
startSave(input: IFilesStartSaveInput): Promise<IFilesSaveSession>;
|
|
79
|
+
appendSave(input: IFilesAppendSaveInput): Promise<IFilesAppendSaveResult>;
|
|
80
|
+
finishSave(input: IFilesSaveSession): Promise<IFilesSaveResult>;
|
|
81
|
+
cancelSave(input: IFilesSaveSession): Promise<{
|
|
82
|
+
cancelled: boolean;
|
|
83
|
+
}>;
|
|
50
84
|
};
|
|
51
85
|
}
|
|
52
86
|
/**
|
|
53
|
-
*
|
|
87
|
+
* What {@link saveWholeFile} saves from: bytes, or anything that reads in slices like a browser
|
|
88
|
+
* `Blob` or `File`, so a large file needn't be in memory all at once.
|
|
89
|
+
*/
|
|
90
|
+
export type FileBytesSource = Uint8Array | {
|
|
91
|
+
size: number;
|
|
92
|
+
slice(start: number, end: number): {
|
|
93
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Save a file of up to {@link FILES_MAX_FILE_BYTES} through System Files: one `save` when it
|
|
98
|
+
* fits in one call, otherwise a write session in pieces of {@link FILES_MAX_CALL_BYTES}. A
|
|
99
|
+
* session that fails is cancelled.
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* const { fileId } = await saveWholeFile(files.tools, { name: file.name, mimeType: file.type, bytes: file });
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @param tools - The consumer's tools, from a package, a screen or the host.
|
|
106
|
+
* @param input - Name, optional MIME type, and the bytes or a `Blob`.
|
|
107
|
+
* @returns The saved file's description.
|
|
108
|
+
*/
|
|
109
|
+
export declare function saveWholeFile(tools: Pick<IFilesConsumer["tools"], "save" | "startSave" | "appendSave" | "finishSave" | "cancelSave">, input: Omit<IFilesSaveInput, "bytes"> & {
|
|
110
|
+
bytes: FileBytesSource;
|
|
111
|
+
}): Promise<IFilesSaveResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Read a whole file through System Files, in ranges of {@link FILES_MAX_CALL_BYTES}. It refuses
|
|
114
|
+
* a file over `maxBytes`, so a large file isn't read into memory by mistake: read a video, say,
|
|
115
|
+
* in ranges instead.
|
|
54
116
|
*
|
|
55
117
|
* @param read - The consumer's `read` tool, from a package, a screen or the host.
|
|
56
118
|
* @param fileId - File to read.
|
|
119
|
+
* @param opts.maxBytes - Largest file to read; {@link FILES_WHOLE_READ_MAX_BYTES} by default.
|
|
57
120
|
* @returns The file's bytes, name and MIME type.
|
|
58
121
|
*/
|
|
59
|
-
export declare function readWholeFile(read: IFilesConsumer["tools"]["read"], fileId: string
|
|
122
|
+
export declare function readWholeFile(read: IFilesConsumer["tools"]["read"], fileId: string, opts?: {
|
|
123
|
+
maxBytes?: number;
|
|
124
|
+
}): Promise<IFilesReadResult>;
|
|
@@ -1,20 +1,80 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FILES_MAX_CALL_BYTES = void 0;
|
|
3
|
+
exports.FILES_WHOLE_READ_MAX_BYTES = exports.FILES_MAX_FILE_BYTES = exports.FILES_MAX_CALL_BYTES = void 0;
|
|
4
|
+
exports.saveWholeFile = saveWholeFile;
|
|
4
5
|
exports.readWholeFile = readWholeFile;
|
|
5
|
-
// System Files' limits, shapes and
|
|
6
|
+
// System Files' limits, shapes and helpers, without the contract's registration, so package
|
|
6
7
|
// code can import them from `@peers-app/peers-sdk/define`.
|
|
7
|
-
/** Largest file `save` accepts,
|
|
8
|
+
/** Largest file `save` accepts, the most bytes one `appendSave` takes, and one `read` returns. */
|
|
8
9
|
exports.FILES_MAX_CALL_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
/** Largest file Peers stores (1 GiB). Save one over {@link FILES_MAX_CALL_BYTES} in pieces. */
|
|
11
|
+
exports.FILES_MAX_FILE_BYTES = 1024 * 1024 * 1024;
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
13
|
+
* Largest file {@link readWholeFile} reads unless told otherwise (64 MiB), so a screen doesn't
|
|
14
|
+
* hold a large file in memory. Read a larger file, such as a video, in ranges.
|
|
15
|
+
*/
|
|
16
|
+
exports.FILES_WHOLE_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Save a file of up to {@link FILES_MAX_FILE_BYTES} through System Files: one `save` when it
|
|
19
|
+
* fits in one call, otherwise a write session in pieces of {@link FILES_MAX_CALL_BYTES}. A
|
|
20
|
+
* session that fails is cancelled.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const { fileId } = await saveWholeFile(files.tools, { name: file.name, mimeType: file.type, bytes: file });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @param tools - The consumer's tools, from a package, a screen or the host.
|
|
27
|
+
* @param input - Name, optional MIME type, and the bytes or a `Blob`.
|
|
28
|
+
* @returns The saved file's description.
|
|
29
|
+
*/
|
|
30
|
+
async function saveWholeFile(tools, input) {
|
|
31
|
+
const { bytes: source, ...description } = input;
|
|
32
|
+
// Make a local view too: direct host tools validate with `instanceof Uint8Array`.
|
|
33
|
+
const bytes = ArrayBuffer.isView(source) && Object.prototype.toString.call(source) === "[object Uint8Array]"
|
|
34
|
+
? new Uint8Array(source.buffer, source.byteOffset, source.byteLength)
|
|
35
|
+
: undefined;
|
|
36
|
+
const blob = source;
|
|
37
|
+
if (!bytes &&
|
|
38
|
+
(!blob || typeof blob.slice !== "function" || !Number.isSafeInteger(blob.size) || blob.size < 0)) {
|
|
39
|
+
throw new Error("System Files: bytes must be a Uint8Array or a Blob-like source");
|
|
40
|
+
}
|
|
41
|
+
const size = bytes ? bytes.length : blob.size;
|
|
42
|
+
const piece = async (start, end) => bytes ? bytes.subarray(start, end) : new Uint8Array(await blob.slice(start, end).arrayBuffer());
|
|
43
|
+
if (size <= exports.FILES_MAX_CALL_BYTES) {
|
|
44
|
+
return tools.save({ ...description, bytes: await piece(0, size) });
|
|
45
|
+
}
|
|
46
|
+
if (size > exports.FILES_MAX_FILE_BYTES) {
|
|
47
|
+
throw new Error(`System Files: a file can be at most ${exports.FILES_MAX_FILE_BYTES / 1024 ** 2} MiB`);
|
|
48
|
+
}
|
|
49
|
+
const { sessionId } = await tools.startSave(description);
|
|
50
|
+
try {
|
|
51
|
+
for (let start = 0; start < size; start += exports.FILES_MAX_CALL_BYTES) {
|
|
52
|
+
const end = Math.min(size, start + exports.FILES_MAX_CALL_BYTES);
|
|
53
|
+
await tools.appendSave({ sessionId, bytes: await piece(start, end) });
|
|
54
|
+
}
|
|
55
|
+
return await tools.finishSave({ sessionId });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await tools.cancelSave({ sessionId }).catch(() => undefined);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read a whole file through System Files, in ranges of {@link FILES_MAX_CALL_BYTES}. It refuses
|
|
64
|
+
* a file over `maxBytes`, so a large file isn't read into memory by mistake: read a video, say,
|
|
65
|
+
* in ranges instead.
|
|
11
66
|
*
|
|
12
67
|
* @param read - The consumer's `read` tool, from a package, a screen or the host.
|
|
13
68
|
* @param fileId - File to read.
|
|
69
|
+
* @param opts.maxBytes - Largest file to read; {@link FILES_WHOLE_READ_MAX_BYTES} by default.
|
|
14
70
|
* @returns The file's bytes, name and MIME type.
|
|
15
71
|
*/
|
|
16
|
-
async function readWholeFile(read, fileId) {
|
|
72
|
+
async function readWholeFile(read, fileId, opts) {
|
|
73
|
+
const maxBytes = opts?.maxBytes ?? exports.FILES_WHOLE_READ_MAX_BYTES;
|
|
17
74
|
const first = await read({ fileId, length: exports.FILES_MAX_CALL_BYTES });
|
|
75
|
+
if (first.fileSize > maxBytes) {
|
|
76
|
+
throw new Error(`File ${fileId} is ${first.fileSize} bytes, over readWholeFile's limit of ${maxBytes} bytes; read it in ranges`);
|
|
77
|
+
}
|
|
18
78
|
if (first.bytes.length >= first.fileSize)
|
|
19
79
|
return first;
|
|
20
80
|
const bytes = new Uint8Array(first.fileSize);
|
|
@@ -3,6 +3,7 @@ import type { IAlsoImplementsDeclaration, IContractDefinition, IValidationResult
|
|
|
3
3
|
* Validate that an implementation contract satisfies a target contract shape.
|
|
4
4
|
* The implementation must be a superset: all contract tables, tools,
|
|
5
5
|
* observables, and events must be present with compatible shapes.
|
|
6
|
+
* A table field that keeps files in the target must keep them in the implementation too.
|
|
6
7
|
*/
|
|
7
8
|
export declare function validateProviderSatisfiesContract(impl: IContractDefinition, contract: IContractDefinition): IValidationResult;
|
|
8
9
|
/**
|
|
@@ -5,6 +5,7 @@ exports.contractShapeChanges = contractShapeChanges;
|
|
|
5
5
|
exports.validateImmutability = validateImmutability;
|
|
6
6
|
exports.validateAlsoImplements = validateAlsoImplements;
|
|
7
7
|
const lodash_1 = require("lodash");
|
|
8
|
+
const field_type_1 = require("../types/field-type");
|
|
8
9
|
const types_1 = require("./types");
|
|
9
10
|
function ok() {
|
|
10
11
|
return { valid: true, errors: [] };
|
|
@@ -56,7 +57,10 @@ function validateTableFields(contractTable, implTable) {
|
|
|
56
57
|
}
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
|
-
const compat = isFieldCompatible(contractField, implField, "both")
|
|
60
|
+
const compat = isFieldCompatible(contractField, implField, "both") ??
|
|
61
|
+
((0, field_type_1.isFileReferenceField)(contractField) && !(0, field_type_1.isFileReferenceField)(implField)
|
|
62
|
+
? "contract requires a file reference, implementation does not declare one"
|
|
63
|
+
: null);
|
|
60
64
|
if (compat) {
|
|
61
65
|
errors.push({
|
|
62
66
|
kind: "field",
|
|
@@ -150,6 +154,7 @@ function validateEventFields(contractEvent, implEvent) {
|
|
|
150
154
|
* Validate that an implementation contract satisfies a target contract shape.
|
|
151
155
|
* The implementation must be a superset: all contract tables, tools,
|
|
152
156
|
* observables, and events must be present with compatible shapes.
|
|
157
|
+
* A table field that keeps files in the target must keep them in the implementation too.
|
|
153
158
|
*/
|
|
154
159
|
function validateProviderSatisfiesContract(impl, contract) {
|
|
155
160
|
const errors = [];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leading characters of every derived row id. Every derived prefix starts `000`, while `newid()`
|
|
3
|
+
* ids start with their creation time, so the two can't collide.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DERIVED_ROW_ID_PREFIX = "000r";
|
|
6
|
+
/** A value a derived row id is computed from. */
|
|
7
|
+
export type DerivedRowValue = string | number | boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The id of a row derived from its values: a hash of `["row/1", tableName, values]` as JSON,
|
|
10
|
+
* after {@link DERIVED_ROW_ID_PREFIX}. The encoding is frozen: ids derived this way are stored
|
|
11
|
+
* on every device, so changing it would orphan them.
|
|
12
|
+
*
|
|
13
|
+
* The hash part is 21 base-36 characters (about 108 bits). Taking over an existing id needs a
|
|
14
|
+
* second preimage (about 2^108 work); crafting two rows with one id costs about 2^54.
|
|
15
|
+
*
|
|
16
|
+
* @param tableName - The table's name.
|
|
17
|
+
* @param values - The values the id is derived from, in a fixed order.
|
|
18
|
+
* @throws When a value isn't a string, a finite number or a boolean.
|
|
19
|
+
*/
|
|
20
|
+
export declare function derivedRowId(tableName: string, values: readonly DerivedRowValue[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether `id` has the form of a {@link derivedRowId}: a hint, not a proof that it was derived.
|
|
23
|
+
*
|
|
24
|
+
* @param id - A candidate row id.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isDerivedRowId(id: unknown): boolean;
|
|
@@ -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
|
+
}
|