@peers-app/peers-sdk 0.30.1 → 0.30.3
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/system/file-storage-contract.d.ts +97 -3
- package/dist/contracts/system/file-storage-contract.js +51 -3
- package/dist/contracts/system/mesh-metrics-types.d.ts +148 -0
- package/dist/contracts/system/mesh-metrics-types.js +2 -0
- package/dist/contracts/system/network-contracts.d.ts +8 -0
- package/dist/contracts/system/network-contracts.js +16 -0
- package/dist/data/files/chunk-store.d.ts +7 -0
- package/dist/data/files/chunk-store.js +7 -2
- package/dist/data/files/file-rows.d.ts +10 -1
- package/dist/data/files/file-rows.js +28 -4
- package/dist/data/files/file-write-stream.d.ts +2 -1
- package/dist/data/files/file-write-stream.js +2 -1
- package/dist/data/files/file.types.d.ts +15 -0
- package/dist/data/files/file.types.js +22 -1
- package/dist/data/files/files.d.ts +5 -4
- package/dist/data/files/files.js +16 -24
- package/dist/data/groups.d.ts +13 -0
- package/dist/data/groups.js +6 -0
- package/dist/data/orm/write-checks.d.ts +15 -0
- package/dist/data/orm/write-checks.js +21 -0
- package/dist/device/connection.d.ts +3 -0
- package/dist/device/connection.js +14 -0
- package/dist/invites/invite-messages.d.ts +38 -0
- package/dist/rpc-types.d.ts +0 -22
- package/dist/rpc-types.js +0 -10
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ export interface IChunkMigrationResult {
|
|
|
14
14
|
files: number;
|
|
15
15
|
/** Distinct chunks those rows list, less the skipped ones. */
|
|
16
16
|
needed: number;
|
|
17
|
-
/** Chunks only orphaned
|
|
17
|
+
/** Chunks only orphaned runtime bundle rows list, which aren't copied. */
|
|
18
18
|
skippedBundleChunks: number;
|
|
19
19
|
/** Needed chunks the store already had. */
|
|
20
20
|
alreadyStored: number;
|
|
@@ -35,6 +35,86 @@ export interface IChunkMigrationResult {
|
|
|
35
35
|
* `verified`.
|
|
36
36
|
*/
|
|
37
37
|
export type ChunkMigrationState = "not-needed" | "pending" | "running" | "blocked" | "verified";
|
|
38
|
+
/** Why a file is retained, or where it is in its local retention window. */
|
|
39
|
+
export type FileCollectionState = "kept" | "referenced" | "never" | "waiting" | "expired" | "unindexed" | "disabled";
|
|
40
|
+
/** Ordinary-file retention on this device; runtime bundles always use grace alone. */
|
|
41
|
+
export type FileRetention = number | "never";
|
|
42
|
+
/** One context's part of a collection plan; chunk totals may overlap other contexts. */
|
|
43
|
+
export interface IFileCollectionContextResult {
|
|
44
|
+
dataContextId: string;
|
|
45
|
+
disabled: boolean;
|
|
46
|
+
retentionMs: FileRetention;
|
|
47
|
+
files: number;
|
|
48
|
+
expiredFiles: number;
|
|
49
|
+
/** Chunks/bytes eligible in this plan, including shared expired chunks; a concurrent put can keep them longer. */
|
|
50
|
+
chunksEligible: number;
|
|
51
|
+
bytesEligible: number;
|
|
52
|
+
rowsDeleted: number;
|
|
53
|
+
/** Next file deadline after this run's planning time. */
|
|
54
|
+
nextExpirationAt?: number;
|
|
55
|
+
/** Next chunk age deadline among expired files (deletion requires strictly older). */
|
|
56
|
+
nextChunkAt?: number;
|
|
57
|
+
}
|
|
58
|
+
/** A collection run. Dry runs report eligible chunks/bytes and bundle rows in the deletion totals, but delete nothing. */
|
|
59
|
+
export interface IFileCollectionResult {
|
|
60
|
+
state: "completed" | "skipped" | "stopped";
|
|
61
|
+
dryRun: boolean;
|
|
62
|
+
reason?: string;
|
|
63
|
+
contexts: IFileCollectionContextResult[];
|
|
64
|
+
files: number;
|
|
65
|
+
expiredFiles: number;
|
|
66
|
+
liveChunks: number;
|
|
67
|
+
chunksFreed: number;
|
|
68
|
+
bytesFreed: number;
|
|
69
|
+
rowsDeleted: number;
|
|
70
|
+
problems: string[];
|
|
71
|
+
startedAt: number;
|
|
72
|
+
finishedAt: number;
|
|
73
|
+
}
|
|
74
|
+
/** A file in a paged, device-local list. */
|
|
75
|
+
export interface IListedFile {
|
|
76
|
+
fileId: string;
|
|
77
|
+
name: string;
|
|
78
|
+
fileSize: number;
|
|
79
|
+
mimeType: string;
|
|
80
|
+
kept: boolean;
|
|
81
|
+
legacy: boolean;
|
|
82
|
+
/** Runtime-owned bundle, whether saved by an older or current installer. */
|
|
83
|
+
runtimeBundle: boolean;
|
|
84
|
+
referenceCount: number;
|
|
85
|
+
referenceTables: string[];
|
|
86
|
+
chunks: "all" | "some" | "none";
|
|
87
|
+
unreferencedAt?: number;
|
|
88
|
+
state: FileCollectionState;
|
|
89
|
+
/** Earliest collection deadline here; shared chunks and the chunk age floor can keep bytes longer. */
|
|
90
|
+
expiresAt?: number;
|
|
91
|
+
/** Why there is no deadline, when applicable. */
|
|
92
|
+
retainedReason?: string;
|
|
93
|
+
}
|
|
94
|
+
/** Context totals, counting logical file bytes (shared content counts for each file). */
|
|
95
|
+
export interface IFileListSummary {
|
|
96
|
+
files: number;
|
|
97
|
+
bytes: number;
|
|
98
|
+
states: Record<FileCollectionState, number>;
|
|
99
|
+
retentionMs: FileRetention;
|
|
100
|
+
}
|
|
101
|
+
/** Keyset paging in file-id order. */
|
|
102
|
+
export interface IListFilesInput {
|
|
103
|
+
after?: string;
|
|
104
|
+
/** Page size, 1–200; defaults to 50. */
|
|
105
|
+
limit?: number;
|
|
106
|
+
}
|
|
107
|
+
/** One page and context totals, with a cursor only when more rows remain. */
|
|
108
|
+
export interface IListFilesResult {
|
|
109
|
+
files: IListedFile[];
|
|
110
|
+
nextCursor?: string;
|
|
111
|
+
summary: IFileListSummary;
|
|
112
|
+
}
|
|
113
|
+
/** Forced reindex outcome for every loaded context. */
|
|
114
|
+
export interface IFileReindexResult {
|
|
115
|
+
contexts: number;
|
|
116
|
+
problems: string[];
|
|
117
|
+
}
|
|
38
118
|
/** This device's chunk storage for the signed-in account. */
|
|
39
119
|
export interface IFileStorageStatus {
|
|
40
120
|
/** The account's chunk store. */
|
|
@@ -48,6 +128,12 @@ export interface IFileStorageStatus {
|
|
|
48
128
|
/** Accounts on this device still to migrate; it can be removed once none are left. */
|
|
49
129
|
waitingAccounts: string[];
|
|
50
130
|
};
|
|
131
|
+
/** Collection's state in this session and its next scheduled run. */
|
|
132
|
+
collection?: {
|
|
133
|
+
state: "idle" | "running" | "skipped" | "stopped";
|
|
134
|
+
lastRun?: IFileCollectionResult;
|
|
135
|
+
nextRunAt?: number;
|
|
136
|
+
};
|
|
51
137
|
/** The migration of the legacy store. */
|
|
52
138
|
migration: {
|
|
53
139
|
state: ChunkMigrationState;
|
|
@@ -60,6 +146,14 @@ export interface IFileStorageConsumer {
|
|
|
60
146
|
tools: {
|
|
61
147
|
/** The chunk store's size, the legacy store, and where the migration stands. */
|
|
62
148
|
getStatus(): Promise<IFileStorageStatus>;
|
|
149
|
+
/** Free expired files' unshared chunks, or report what would be freed now. */
|
|
150
|
+
collect(input?: {
|
|
151
|
+
dryRun?: boolean;
|
|
152
|
+
}): Promise<IFileCollectionResult>;
|
|
153
|
+
/** Page through the caller's context, with its summary. */
|
|
154
|
+
listFiles(input?: IListFilesInput): Promise<IListFilesResult>;
|
|
155
|
+
/** Rebuild every loaded context's file references after raw SQL edits. */
|
|
156
|
+
reindex(): Promise<IFileReindexResult>;
|
|
63
157
|
/** Copy this account's chunks out of the legacy store now, and verify them. */
|
|
64
158
|
migrate(): Promise<IChunkMigrationResult>;
|
|
65
159
|
/** Copy again, then delete the legacy store once every account on the device has migrated. */
|
|
@@ -70,7 +164,7 @@ export interface IFileStorageConsumer {
|
|
|
70
164
|
};
|
|
71
165
|
}
|
|
72
166
|
/**
|
|
73
|
-
* Return
|
|
74
|
-
*
|
|
167
|
+
* Return System File Storage v1: local collection, file inspection, reference reindexing and
|
|
168
|
+
* legacy migration. Only the user's own screens and CLI on this device may call it.
|
|
75
169
|
*/
|
|
76
170
|
export declare function getFileStorageContractDefinition(): IContractDefinition;
|
|
@@ -23,8 +23,8 @@ const migrationResultFields = [
|
|
|
23
23
|
field("finishedAt", field_type_1.FieldType.number, "When it finished."),
|
|
24
24
|
];
|
|
25
25
|
/**
|
|
26
|
-
* Return
|
|
27
|
-
*
|
|
26
|
+
* Return System File Storage v1: local collection, file inspection, reference reindexing and
|
|
27
|
+
* legacy migration. Only the user's own screens and CLI on this device may call it.
|
|
28
28
|
*/
|
|
29
29
|
function getFileStorageContractDefinition() {
|
|
30
30
|
return {
|
|
@@ -32,7 +32,7 @@ function getFileStorageContractDefinition() {
|
|
|
32
32
|
version: exports.FILE_STORAGE_CONTRACT_VERSION,
|
|
33
33
|
devTag: "dev",
|
|
34
34
|
name: "System File Storage",
|
|
35
|
-
description: "This device's
|
|
35
|
+
description: "This device's file storage, collection and legacy migration.",
|
|
36
36
|
tables: [],
|
|
37
37
|
observables: [],
|
|
38
38
|
events: [],
|
|
@@ -47,6 +47,54 @@ function getFileStorageContractDefinition() {
|
|
|
47
47
|
optional: true,
|
|
48
48
|
}),
|
|
49
49
|
field("migration", field_type_1.FieldType.object, "The migration's state and last run."),
|
|
50
|
+
field("collection", field_type_1.FieldType.object, "Collection's state, last run and next run.", {
|
|
51
|
+
optional: true,
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "collect",
|
|
57
|
+
usageDescription: "Free expired files' unshared chunks on this device, or preview collection.",
|
|
58
|
+
inputFields: [
|
|
59
|
+
field("dryRun", field_type_1.FieldType.boolean, "Report only; delete nothing.", { optional: true }),
|
|
60
|
+
],
|
|
61
|
+
outputFields: [
|
|
62
|
+
field("state", field_type_1.FieldType.string, "completed, skipped, or stopped."),
|
|
63
|
+
field("dryRun", field_type_1.FieldType.boolean, "Whether this was a preview."),
|
|
64
|
+
field("reason", field_type_1.FieldType.string, "Why it skipped or stopped.", { optional: true }),
|
|
65
|
+
...["contexts", "problems"].map((name) => field(name, field_type_1.FieldType.any, name)),
|
|
66
|
+
...[
|
|
67
|
+
"files",
|
|
68
|
+
"expiredFiles",
|
|
69
|
+
"liveChunks",
|
|
70
|
+
"chunksFreed",
|
|
71
|
+
"bytesFreed",
|
|
72
|
+
"rowsDeleted",
|
|
73
|
+
"startedAt",
|
|
74
|
+
"finishedAt",
|
|
75
|
+
].map((name) => field(name, field_type_1.FieldType.number, name)),
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "listFiles",
|
|
80
|
+
usageDescription: "List this context's files and their local collection deadlines, in pages.",
|
|
81
|
+
inputFields: [
|
|
82
|
+
field("after", field_type_1.FieldType.string, "Continue after this file id.", { optional: true }),
|
|
83
|
+
field("limit", field_type_1.FieldType.number, "Page size, 1–200; default 50.", { optional: true }),
|
|
84
|
+
],
|
|
85
|
+
outputFields: [
|
|
86
|
+
field("files", field_type_1.FieldType.any, "The page of files."),
|
|
87
|
+
field("nextCursor", field_type_1.FieldType.string, "Continue with this cursor.", { optional: true }),
|
|
88
|
+
field("summary", field_type_1.FieldType.object, "Context counts, logical bytes and retention."),
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "reindex",
|
|
93
|
+
usageDescription: "Rebuild file references in every loaded context after raw SQL edits.",
|
|
94
|
+
inputFields: [],
|
|
95
|
+
outputFields: [
|
|
96
|
+
field("contexts", field_type_1.FieldType.number, "Contexts reindexed."),
|
|
97
|
+
field("problems", field_type_1.FieldType.any, "Tables that could not be opened or indexed."),
|
|
50
98
|
],
|
|
51
99
|
},
|
|
52
100
|
{
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Trigger for an overlapping, at most five-minute connectivity measurement. */
|
|
2
|
+
export type MeshEpisodeReason = "startup" | "resume" | "network-change" | "connection-loss";
|
|
3
|
+
/** Provenance of a dial; explicit requests and group joins are also distinguished. */
|
|
4
|
+
export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-answer" | "configured-server" | "lan-scan" | "websocket-upgrade" | "incoming" | "direct-request" | "group-join";
|
|
5
|
+
/** Bounded path categories used for lifetime counters; attempts also retain the address. */
|
|
6
|
+
export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
|
|
7
|
+
/** Failure categories, independent of free-form transport error messages. */
|
|
8
|
+
export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported";
|
|
9
|
+
/** Observed stage; timestamps are offsets in milliseconds from the attempt's queued time. */
|
|
10
|
+
export type MeshStage = "queued" | "started" | "authenticated" | "admitted" | "ready" | "firstSync";
|
|
11
|
+
/** Optional observational metadata passed through a dial without affecting its result. */
|
|
12
|
+
export interface IMeshDial {
|
|
13
|
+
attemptId?: string;
|
|
14
|
+
reason?: MeshDialReason;
|
|
15
|
+
targetDeviceId?: string;
|
|
16
|
+
}
|
|
17
|
+
/** One dial or inbound connection. Missing stages have not been observed. */
|
|
18
|
+
export interface IMeshAttempt {
|
|
19
|
+
attemptId: string;
|
|
20
|
+
targetDeviceId?: string;
|
|
21
|
+
reason: MeshDialReason;
|
|
22
|
+
direction: "outgoing" | "incoming";
|
|
23
|
+
path: MeshPath;
|
|
24
|
+
address?: string;
|
|
25
|
+
queuedAt: number;
|
|
26
|
+
stages: Partial<Record<MeshStage, number>>;
|
|
27
|
+
answeredDeviceId?: string;
|
|
28
|
+
expectedIdentity?: boolean;
|
|
29
|
+
answered: boolean;
|
|
30
|
+
lastSignalFailure?: {
|
|
31
|
+
reason: "ttl0" | "no-route";
|
|
32
|
+
detail?: string;
|
|
33
|
+
};
|
|
34
|
+
ready: Record<string, number>;
|
|
35
|
+
firstSync: Record<string, number>;
|
|
36
|
+
outcome: "pending" | "admitted" | "failed" | "unfinished";
|
|
37
|
+
endReason?: "buffer-evicted" | "disposed";
|
|
38
|
+
endedAt?: number;
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
failure?: {
|
|
41
|
+
stage: MeshStage;
|
|
42
|
+
reason: MeshFailureReason;
|
|
43
|
+
detail?: string;
|
|
44
|
+
};
|
|
45
|
+
doubleDial: boolean;
|
|
46
|
+
offline: boolean;
|
|
47
|
+
outOfOrder: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Millisecond durations derived from one episode; absent means not observed. */
|
|
50
|
+
export interface IMeshDurations {
|
|
51
|
+
T_first?: number;
|
|
52
|
+
T_own?: number;
|
|
53
|
+
T_cover?: number;
|
|
54
|
+
T_resume?: number;
|
|
55
|
+
repairStart?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Coverage at the latest observation (at completion for a finished episode). */
|
|
58
|
+
export interface IMeshContextCoverage {
|
|
59
|
+
dataContextId: string;
|
|
60
|
+
possibleSourceDeviceIds: string[];
|
|
61
|
+
usableDeviceIds: string[];
|
|
62
|
+
excluded: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** One connectivity episode, independent of any other episode in flight. */
|
|
65
|
+
export interface IMeshEpisode {
|
|
66
|
+
episodeId: string;
|
|
67
|
+
reason: MeshEpisodeReason;
|
|
68
|
+
trigger?: string;
|
|
69
|
+
startedAt: number;
|
|
70
|
+
endedAt?: number;
|
|
71
|
+
durationMs?: number;
|
|
72
|
+
outcome: "active" | "covered" | "unfinished";
|
|
73
|
+
endReason?: "covered" | "deadline" | "buffer-evicted" | "disposed";
|
|
74
|
+
metrics: IMeshDurations;
|
|
75
|
+
contexts: IMeshContextCoverage[];
|
|
76
|
+
}
|
|
77
|
+
/** Nearest-rank percentiles of observed values only; count exposes the denominator. */
|
|
78
|
+
export interface IMeshPercentiles {
|
|
79
|
+
count: number;
|
|
80
|
+
p50?: number;
|
|
81
|
+
p90?: number;
|
|
82
|
+
p95?: number;
|
|
83
|
+
p99?: number;
|
|
84
|
+
}
|
|
85
|
+
/** Lifetime dial totals. Incoming connections are separate from the outgoing-dial ratio. */
|
|
86
|
+
export interface IMeshPathCounters {
|
|
87
|
+
dials: number;
|
|
88
|
+
connections: number;
|
|
89
|
+
incomingConnections: number;
|
|
90
|
+
dialsPerConnection: number | null;
|
|
91
|
+
failures: Partial<Record<MeshFailureReason, number>>;
|
|
92
|
+
}
|
|
93
|
+
/** Shared relay totals and current gauges, also exposed on non-Services hosts. */
|
|
94
|
+
export interface IMeshServicesCounters {
|
|
95
|
+
directRelays: number;
|
|
96
|
+
floodsForwarded: number;
|
|
97
|
+
floodsPruned: number;
|
|
98
|
+
pendingEmits: number;
|
|
99
|
+
concurrentConnections: number;
|
|
100
|
+
connects: number;
|
|
101
|
+
disconnects: number;
|
|
102
|
+
}
|
|
103
|
+
/** In-memory diagnostics snapshot. No history survives a runtime restart. */
|
|
104
|
+
export interface IMeshStats {
|
|
105
|
+
startedAt: number;
|
|
106
|
+
sampledAt: number;
|
|
107
|
+
uptimeMs: number;
|
|
108
|
+
readyToDialMs?: number;
|
|
109
|
+
limits: {
|
|
110
|
+
episodes: number;
|
|
111
|
+
attempts: number;
|
|
112
|
+
episodeMs: number;
|
|
113
|
+
};
|
|
114
|
+
currentEpisodes: IMeshEpisode[];
|
|
115
|
+
recentEpisodes: IMeshEpisode[];
|
|
116
|
+
percentiles: Record<keyof IMeshDurations, IMeshPercentiles>;
|
|
117
|
+
counters: {
|
|
118
|
+
paths: Record<MeshPath, IMeshPathCounters>;
|
|
119
|
+
lanScan: {
|
|
120
|
+
scans: number;
|
|
121
|
+
probes: number;
|
|
122
|
+
connected: number;
|
|
123
|
+
};
|
|
124
|
+
unfinishedAttempts: number;
|
|
125
|
+
doubleDials: number;
|
|
126
|
+
offlineDials: number;
|
|
127
|
+
offlineDialsPerHour: number;
|
|
128
|
+
broadcasts: {
|
|
129
|
+
sent: number;
|
|
130
|
+
answered: number;
|
|
131
|
+
ttl0: number;
|
|
132
|
+
failed: number;
|
|
133
|
+
pending: number;
|
|
134
|
+
perHour: number;
|
|
135
|
+
};
|
|
136
|
+
services: IMeshServicesCounters;
|
|
137
|
+
};
|
|
138
|
+
recentAttempts: IMeshAttempt[];
|
|
139
|
+
recentBroadcasts: IMeshBroadcast[];
|
|
140
|
+
}
|
|
141
|
+
/** A discovery broadcast and its observed reply, bounded with the attempt history. */
|
|
142
|
+
export interface IMeshBroadcast {
|
|
143
|
+
broadcastId: string;
|
|
144
|
+
dataContextId?: string;
|
|
145
|
+
sentAt: number;
|
|
146
|
+
durationMs?: number;
|
|
147
|
+
outcome: "pending" | "answered" | "ttl0" | "failed";
|
|
148
|
+
}
|
|
@@ -4,12 +4,15 @@ import type { Observable } from "../../observable";
|
|
|
4
4
|
import type { INetworkInfo } from "../../types/peer-device";
|
|
5
5
|
import type { IContractConsumerBase } from "../contract-proxy";
|
|
6
6
|
import type { IContractDefinition } from "../types";
|
|
7
|
+
import type { IMeshStats } from "./mesh-metrics-types";
|
|
8
|
+
export type * from "./mesh-metrics-types";
|
|
7
9
|
/** Version of the built-in System Network Diagnostics contract. */
|
|
8
10
|
export declare const NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
9
11
|
/** Version of the built-in System Network Control contract. */
|
|
10
12
|
export declare const NETWORK_CONTROL_CONTRACT_VERSION = 1;
|
|
11
13
|
/** Stable IDs for tools in System Network Diagnostics v1. */
|
|
12
14
|
export declare const networkDiagnosticsToolIds: {
|
|
15
|
+
readonly getMeshStats: string;
|
|
13
16
|
readonly getOverview: "00ms1yb50ym353ads0gel5jpv";
|
|
14
17
|
readonly getConnections: "00ms1yb50ykny9kdgiclpa1t4";
|
|
15
18
|
readonly getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq";
|
|
@@ -241,6 +244,11 @@ export interface INetworkDiagnosticsConsumer extends IContractConsumerBase {
|
|
|
241
244
|
getOverview(): Promise<{
|
|
242
245
|
overview: INetworkOverview;
|
|
243
246
|
}>;
|
|
247
|
+
getMeshStats(input?: {
|
|
248
|
+
limit?: number;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
stats: IMeshStats;
|
|
251
|
+
}>;
|
|
244
252
|
getConnections(): Promise<{
|
|
245
253
|
connections: INetworkConnectionInfo[];
|
|
246
254
|
}>;
|
|
@@ -6,12 +6,18 @@ exports.getNetworkControlContractDefinition = getNetworkControlContractDefinitio
|
|
|
6
6
|
const system_ids_1 = require("../../system-ids");
|
|
7
7
|
const field_type_1 = require("../../types/field-type");
|
|
8
8
|
const system_contracts_1 = require("../system-contracts");
|
|
9
|
+
const tool_catalog_identity_1 = require("../tool-catalog-identity");
|
|
9
10
|
/** Version of the built-in System Network Diagnostics contract. */
|
|
10
11
|
exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
11
12
|
/** Version of the built-in System Network Control contract. */
|
|
12
13
|
exports.NETWORK_CONTROL_CONTRACT_VERSION = 1;
|
|
13
14
|
/** Stable IDs for tools in System Network Diagnostics v1. */
|
|
14
15
|
exports.networkDiagnosticsToolIds = {
|
|
16
|
+
getMeshStats: (0, tool_catalog_identity_1.deriveToolCatalogId)({
|
|
17
|
+
contractId: system_ids_1.networkDiagnosticsContractId,
|
|
18
|
+
version: exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION,
|
|
19
|
+
toolName: "getMeshStats",
|
|
20
|
+
}),
|
|
15
21
|
getOverview: "00ms1yb50ym353ads0gel5jpv",
|
|
16
22
|
getConnections: "00ms1yb50ykny9kdgiclpa1t4",
|
|
17
23
|
getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq",
|
|
@@ -46,6 +52,16 @@ function getNetworkDiagnosticsContractDefinition() {
|
|
|
46
52
|
description: "Portable local network, connection, and synchronization diagnostics.",
|
|
47
53
|
tables: [],
|
|
48
54
|
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: "getMeshStats",
|
|
57
|
+
usageDescription: "Read bounded in-memory connectivity episodes, attempts, percentiles and relay counters.",
|
|
58
|
+
inputFields: [
|
|
59
|
+
field("limit", field_type_1.FieldType.number, "Maximum recent attempts and broadcasts (default 50, range 0–500). Episodes and counters are always complete.", { optional: true }),
|
|
60
|
+
],
|
|
61
|
+
outputFields: [
|
|
62
|
+
field("stats", field_type_1.FieldType.object, "Connectivity measurements since runtime startup."),
|
|
63
|
+
],
|
|
64
|
+
},
|
|
49
65
|
{
|
|
50
66
|
name: "getOverview",
|
|
51
67
|
usageDescription: "Return aggregate network and group information.",
|
|
@@ -44,6 +44,11 @@ export interface ChunkStore {
|
|
|
44
44
|
* @returns How many chunks were deleted.
|
|
45
45
|
*/
|
|
46
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;
|
|
47
52
|
/**
|
|
48
53
|
* A page of stored chunks, in hash order.
|
|
49
54
|
*
|
|
@@ -76,6 +81,8 @@ export declare class MemoryChunkStore implements ChunkStore {
|
|
|
76
81
|
get(hash: string): Promise<Uint8Array | undefined>;
|
|
77
82
|
put(hash: string, bytes: Uint8Array): Promise<void>;
|
|
78
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;
|
|
79
86
|
list(opts?: {
|
|
80
87
|
after?: string;
|
|
81
88
|
limit?: number;
|
|
@@ -32,12 +32,17 @@ class MemoryChunkStore {
|
|
|
32
32
|
this.chunks.set(hash, { bytes: stored?.bytes ?? new Uint8Array(bytes), storedAt: this.now() });
|
|
33
33
|
}
|
|
34
34
|
async delete(hashes, storedBefore) {
|
|
35
|
-
|
|
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 };
|
|
36
40
|
for (const hash of hashes) {
|
|
37
41
|
const stored = this.chunks.get(hash);
|
|
38
42
|
if (stored && stored.storedAt < storedBefore) {
|
|
39
43
|
this.chunks.delete(hash);
|
|
40
|
-
deleted++;
|
|
44
|
+
deleted.count++;
|
|
45
|
+
deleted.bytes += stored.bytes.length;
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
return deleted;
|
|
@@ -31,6 +31,12 @@ export declare function isLegacyFileId(fileId: string): boolean;
|
|
|
31
31
|
* @param file - The row's id and name.
|
|
32
32
|
*/
|
|
33
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;
|
|
34
40
|
/**
|
|
35
41
|
* Whether a file is kept: never collected, even with nothing referencing it. A legacy row always
|
|
36
42
|
* is. A row relayed by a build that predates `keep` arrives without it; its id, derived from
|
|
@@ -39,6 +45,8 @@ export declare function isLegacyBundleFile(file: Pick<IFile, "fileId" | "name">)
|
|
|
39
45
|
* @param file - The row.
|
|
40
46
|
*/
|
|
41
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;
|
|
42
50
|
/**
|
|
43
51
|
* Whether two rows describe the same file, ignoring how an older build stored it (no `keep`, no
|
|
44
52
|
* MIME type).
|
|
@@ -58,7 +66,8 @@ export declare function isSameFile(a: IFile, b: IFile): boolean;
|
|
|
58
66
|
export declare function fileRowProblem(file: IFile, recordId: string): string | undefined;
|
|
59
67
|
/**
|
|
60
68
|
* The `Files` write check: a new row must pass {@link fileRowProblem}; a stored row never
|
|
61
|
-
* changes (rewriting the same file is allowed); and only a
|
|
69
|
+
* changes (rewriting the same file is allowed); and only a stored runtime bundle row may be
|
|
70
|
+
* deleted. Unknown legacy ids remain allowed.
|
|
62
71
|
*
|
|
63
72
|
* @param existing - The row as stored, if any.
|
|
64
73
|
* @param candidate - The row to store, or undefined for a delete.
|
|
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.fileIdFor = fileIdFor;
|
|
4
4
|
exports.isLegacyFileId = isLegacyFileId;
|
|
5
5
|
exports.isLegacyBundleFile = isLegacyBundleFile;
|
|
6
|
+
exports.isRuntimeBundleFile = isRuntimeBundleFile;
|
|
6
7
|
exports.isKeptFile = isKeptFile;
|
|
8
|
+
exports.isCollectableFile = isCollectableFile;
|
|
7
9
|
exports.isSameFile = isSameFile;
|
|
8
10
|
exports.fileRowProblem = fileRowProblem;
|
|
9
11
|
exports.checkFileWrite = checkFileWrite;
|
|
10
12
|
/**
|
|
11
13
|
* The rules every `Files` row meets, on every device: ids derived from the content, rows that
|
|
12
|
-
* never change, and deletes only of runtime
|
|
14
|
+
* never change, and deletes only of runtime bundle rows. Each device checks an
|
|
13
15
|
* incoming row in full, so no member can put other content under a file's id.
|
|
14
16
|
*/
|
|
15
17
|
const derived_row_id_1 = require("../derived-row-id");
|
|
@@ -57,6 +59,18 @@ function isLegacyFileId(fileId) {
|
|
|
57
59
|
function isLegacyBundleFile(file) {
|
|
58
60
|
return isLegacyFileId(file.fileId) && BUNDLE_FILE_NAME.test(file.name);
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether this is a runtime-owned package bundle: a legacy bundle, or a derived file with
|
|
64
|
+
* an installer bundle name, JavaScript MIME type and keep=false. Read keep through its
|
|
65
|
+
* identity when an older device relayed the row without that column.
|
|
66
|
+
*/
|
|
67
|
+
function isRuntimeBundleFile(file) {
|
|
68
|
+
return (isLegacyBundleFile(file) ||
|
|
69
|
+
(!isLegacyFileId(file.fileId) &&
|
|
70
|
+
BUNDLE_FILE_NAME.test(file.name) &&
|
|
71
|
+
file.mimeType === "application/javascript" &&
|
|
72
|
+
!isKeptFile(file)));
|
|
73
|
+
}
|
|
60
74
|
/**
|
|
61
75
|
* Whether a file is kept: never collected, even with nothing referencing it. A legacy row always
|
|
62
76
|
* is. A row relayed by a build that predates `keep` arrives without it; its id, derived from
|
|
@@ -71,6 +85,10 @@ function isKeptFile(file) {
|
|
|
71
85
|
return file.keep;
|
|
72
86
|
return fileIdFor({ ...file, keep: false }) !== file.fileId;
|
|
73
87
|
}
|
|
88
|
+
/** Whether an unreferenced file may expire, including the runtime's bundle exception. */
|
|
89
|
+
function isCollectableFile(file) {
|
|
90
|
+
return isRuntimeBundleFile(file) || !isKeptFile(file);
|
|
91
|
+
}
|
|
74
92
|
/**
|
|
75
93
|
* Whether two rows describe the same file, ignoring how an older build stored it (no `keep`, no
|
|
76
94
|
* MIME type).
|
|
@@ -133,7 +151,8 @@ function fileRowProblem(file, recordId) {
|
|
|
133
151
|
}
|
|
134
152
|
/**
|
|
135
153
|
* The `Files` write check: a new row must pass {@link fileRowProblem}; a stored row never
|
|
136
|
-
* changes (rewriting the same file is allowed); and only a
|
|
154
|
+
* changes (rewriting the same file is allowed); and only a stored runtime bundle row may be
|
|
155
|
+
* deleted. Unknown legacy ids remain allowed.
|
|
137
156
|
*
|
|
138
157
|
* @param existing - The row as stored, if any.
|
|
139
158
|
* @param candidate - The row to store, or undefined for a delete.
|
|
@@ -142,9 +161,14 @@ function fileRowProblem(file, recordId) {
|
|
|
142
161
|
*/
|
|
143
162
|
function checkFileWrite(existing, candidate, recordId) {
|
|
144
163
|
if (!candidate) {
|
|
145
|
-
if (
|
|
164
|
+
if (existing ? isRuntimeBundleFile(existing) : isLegacyFileId(recordId))
|
|
146
165
|
return;
|
|
147
|
-
|
|
166
|
+
if (existing)
|
|
167
|
+
throw (0, write_checks_1.writeNotAuthorized)("Files", "a file can't be deleted");
|
|
168
|
+
// An unknown derived id carries no evidence that the row is a bundle. Refuse its delete
|
|
169
|
+
// so a later non-bundle row is still materialized from its authorized history. No later
|
|
170
|
+
// membership data changes that, so sync needn't keep the refusal for recovery.
|
|
171
|
+
throw (0, write_checks_1.writeRefusedForGood)("Files", "a file can't be deleted");
|
|
148
172
|
}
|
|
149
173
|
if (existing) {
|
|
150
174
|
if (!isSameFile(existing, candidate))
|
|
@@ -28,7 +28,8 @@ export declare class FileWriteStream {
|
|
|
28
28
|
private storeChunk;
|
|
29
29
|
/**
|
|
30
30
|
* Store the last chunk and save the file's row. Saving content that already has a row (the
|
|
31
|
-
* same bytes, name, MIME type and `keep`)
|
|
31
|
+
* same bytes, name, MIME type and `keep`) returns the stored one; only a package bundle's row
|
|
32
|
+
* is written again, so a peer that deleted it gets it back.
|
|
32
33
|
*
|
|
33
34
|
* @returns The file's row.
|
|
34
35
|
*/
|
|
@@ -76,7 +76,8 @@ class FileWriteStream {
|
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
78
|
* Store the last chunk and save the file's row. Saving content that already has a row (the
|
|
79
|
-
* same bytes, name, MIME type and `keep`)
|
|
79
|
+
* same bytes, name, MIME type and `keep`) returns the stored one; only a package bundle's row
|
|
80
|
+
* is written again, so a peer that deleted it gets it back.
|
|
80
81
|
*
|
|
81
82
|
* @returns The file's row.
|
|
82
83
|
*/
|
|
@@ -59,6 +59,21 @@ export declare const FILE_MAX_CHUNKS = 1024;
|
|
|
59
59
|
export declare const FILE_MAX_SIZE: number;
|
|
60
60
|
/** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
|
|
61
61
|
export declare const FILE_FETCH_WINDOW = 16;
|
|
62
|
+
/** Default time an unreferenced file is retained, before the grace period (30 days). */
|
|
63
|
+
export declare const FILE_RETENTION_MS: number;
|
|
64
|
+
/** Extra time for a reference to reach devices holding the bytes (7 days). */
|
|
65
|
+
export declare const FILE_COLLECTION_GRACE_MS: number;
|
|
66
|
+
/** Minimum age of a stored chunk before collection (one day, longer than a write session). */
|
|
67
|
+
export declare const FILE_CHUNK_FLOOR_MS: number;
|
|
68
|
+
/**
|
|
69
|
+
* Collection delays. `PEERS_FILE_COLLECTION_DELAY_MS` overrides all three for tests only;
|
|
70
|
+
* absent or invalid uses the constants. Explicit group retention is unaffected.
|
|
71
|
+
*/
|
|
72
|
+
export declare function fileCollectionDelays(): {
|
|
73
|
+
retentionMs: number;
|
|
74
|
+
graceMs: number;
|
|
75
|
+
chunkFloorMs: number;
|
|
76
|
+
};
|
|
62
77
|
/**
|
|
63
78
|
* A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
|
|
64
79
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FILE_FETCH_WINDOW = exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
|
|
3
|
+
exports.FILE_CHUNK_FLOOR_MS = exports.FILE_COLLECTION_GRACE_MS = exports.FILE_RETENTION_MS = exports.FILE_FETCH_WINDOW = exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
|
|
4
|
+
exports.fileCollectionDelays = fileCollectionDelays;
|
|
4
5
|
exports.fileHashOf = fileHashOf;
|
|
5
6
|
exports.computeFileHash = computeFileHash;
|
|
6
7
|
const zod_1 = require("zod");
|
|
@@ -38,6 +39,26 @@ exports.FILE_MAX_CHUNKS = 1024;
|
|
|
38
39
|
exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS * exports.FILE_CHUNK_SIZE;
|
|
39
40
|
/** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
|
|
40
41
|
exports.FILE_FETCH_WINDOW = 16;
|
|
42
|
+
/** Default time an unreferenced file is retained, before the grace period (30 days). */
|
|
43
|
+
exports.FILE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
44
|
+
/** Extra time for a reference to reach devices holding the bytes (7 days). */
|
|
45
|
+
exports.FILE_COLLECTION_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
46
|
+
/** Minimum age of a stored chunk before collection (one day, longer than a write session). */
|
|
47
|
+
exports.FILE_CHUNK_FLOOR_MS = 24 * 60 * 60 * 1000;
|
|
48
|
+
/**
|
|
49
|
+
* Collection delays. `PEERS_FILE_COLLECTION_DELAY_MS` overrides all three for tests only;
|
|
50
|
+
* absent or invalid uses the constants. Explicit group retention is unaffected.
|
|
51
|
+
*/
|
|
52
|
+
function fileCollectionDelays() {
|
|
53
|
+
const value = typeof process === "undefined" ? undefined : process.env.PEERS_FILE_COLLECTION_DELAY_MS;
|
|
54
|
+
const override = value === undefined || value.trim() === "" ? NaN : Number(value);
|
|
55
|
+
const delay = (fallback) => Number.isFinite(override) && override >= 0 ? override : fallback;
|
|
56
|
+
return {
|
|
57
|
+
retentionMs: delay(exports.FILE_RETENTION_MS),
|
|
58
|
+
graceMs: delay(exports.FILE_COLLECTION_GRACE_MS),
|
|
59
|
+
chunkFloorMs: delay(exports.FILE_CHUNK_FLOOR_MS),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
41
62
|
/**
|
|
42
63
|
* A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
|
|
43
64
|
*
|
|
@@ -28,8 +28,9 @@ export declare class FilesTable extends Table<IFile> {
|
|
|
28
28
|
/**
|
|
29
29
|
* Store a file's row as it is, after the checks every device makes: a new row must have the
|
|
30
30
|
* id derived from its content (or be a legacy row), and a stored row never changes. Storing
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* an existing ordinary file again writes nothing; saving a runtime bundle publishes a newer
|
|
32
|
+
* synced snapshot, including when restoring a deleted row. Writers use {@link saveFile} or a
|
|
33
|
+
* write stream; this is for copying a row from another context with its id.
|
|
33
34
|
*
|
|
34
35
|
* @param fileRecord - The row.
|
|
35
36
|
* @returns The row as stored.
|
|
@@ -73,11 +74,11 @@ export declare class FilesTable extends Table<IFile> {
|
|
|
73
74
|
*/
|
|
74
75
|
fetchFile(fileId: string): Promise<boolean>;
|
|
75
76
|
/**
|
|
76
|
-
* Delete a
|
|
77
|
+
* Delete a runtime bundle row, which the runtime owns. Every other file is permanent: it's
|
|
77
78
|
* collected once nothing references it. Chunks stay stored, since other files may share them.
|
|
78
79
|
*
|
|
79
80
|
* @param fileId - File to delete. A missing file is not an error.
|
|
80
|
-
* @throws When the file isn't a
|
|
81
|
+
* @throws When the file isn't a runtime bundle row.
|
|
81
82
|
*/
|
|
82
83
|
deleteFile(fileId: string): Promise<void>;
|
|
83
84
|
}
|
package/dist/data/files/files.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FilesTable = void 0;
|
|
4
4
|
exports.Files = Files;
|
|
5
5
|
const user_context_singleton_1 = require("../../context/user-context-singleton");
|
|
6
|
-
const rpc_types_1 = require("../../rpc-types");
|
|
7
6
|
const orm_1 = require("../orm");
|
|
8
7
|
const table_definitions_system_1 = require("../orm/table-definitions.system");
|
|
9
8
|
const chunk_store_1 = require("./chunk-store");
|
|
@@ -46,8 +45,9 @@ class FilesTable extends orm_1.Table {
|
|
|
46
45
|
/**
|
|
47
46
|
* Store a file's row as it is, after the checks every device makes: a new row must have the
|
|
48
47
|
* id derived from its content (or be a legacy row), and a stored row never changes. Storing
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* an existing ordinary file again writes nothing; saving a runtime bundle publishes a newer
|
|
49
|
+
* synced snapshot, including when restoring a deleted row. Writers use {@link saveFile} or a
|
|
50
|
+
* write stream; this is for copying a row from another context with its id.
|
|
51
51
|
*
|
|
52
52
|
* @param fileRecord - The row.
|
|
53
53
|
* @returns The row as stored.
|
|
@@ -56,11 +56,18 @@ class FilesTable extends orm_1.Table {
|
|
|
56
56
|
async saveFileRecord(fileRecord) {
|
|
57
57
|
const existing = await super.get(fileRecord.fileId);
|
|
58
58
|
(0, file_rows_1.checkFileWrite)(existing, fileRecord, fileRecord.fileId);
|
|
59
|
-
|
|
59
|
+
const runtimeBundle = (0, file_rows_1.isRuntimeBundleFile)(fileRecord);
|
|
60
|
+
if (existing && !runtimeBundle)
|
|
60
61
|
return existing;
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
const saved = await super.save(fileRecord, {
|
|
62
|
+
// An ordinary file inserted since the read must stay untouched. Bundles can be rewritten
|
|
63
|
+
// identically so their pending reuse reaches collection's write watch and other devices.
|
|
64
|
+
const saved = await super.save(existing ?? fileRecord, {
|
|
65
|
+
insertOnly: !runtimeBundle || !existing,
|
|
66
|
+
// Even when still present locally, a reused bundle needs a new snapshot: another device
|
|
67
|
+
// may have deleted it, and collection's write watch must protect this pending reuse.
|
|
68
|
+
restoreIfDeleted: runtimeBundle,
|
|
69
|
+
saveAsSnapshot: runtimeBundle,
|
|
70
|
+
});
|
|
64
71
|
(0, file_rows_1.checkFileWrite)(saved, fileRecord, fileRecord.fileId);
|
|
65
72
|
return saved;
|
|
66
73
|
}
|
|
@@ -132,11 +139,11 @@ class FilesTable extends orm_1.Table {
|
|
|
132
139
|
return complete;
|
|
133
140
|
}
|
|
134
141
|
/**
|
|
135
|
-
* Delete a
|
|
142
|
+
* Delete a runtime bundle row, which the runtime owns. Every other file is permanent: it's
|
|
136
143
|
* collected once nothing references it. Chunks stay stored, since other files may share them.
|
|
137
144
|
*
|
|
138
145
|
* @param fileId - File to delete. A missing file is not an error.
|
|
139
|
-
* @throws When the file isn't a
|
|
146
|
+
* @throws When the file isn't a runtime bundle row.
|
|
140
147
|
*/
|
|
141
148
|
async deleteFile(fileId) {
|
|
142
149
|
const fileRecord = await super.get(fileId);
|
|
@@ -158,18 +165,3 @@ exports.FilesTable = FilesTable;
|
|
|
158
165
|
function Files(dataContext) {
|
|
159
166
|
return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(file_types_1.filesMetaData, file_types_1.fileSchema);
|
|
160
167
|
}
|
|
161
|
-
// Deprecated RPCs, kept only for Family Hub 0.3.x until its port is Home's version. New code
|
|
162
|
-
// uses System Files, which is bound to the caller's own data context.
|
|
163
|
-
rpc_types_1.rpcServerCalls.getFileContentsBase64 = async (fileId) => {
|
|
164
|
-
const data = await Files().getFileContents(fileId);
|
|
165
|
-
if (data === null) {
|
|
166
|
-
throw new Error(`File not found: ${fileId}`);
|
|
167
|
-
}
|
|
168
|
-
return Buffer.from(data).toString("base64");
|
|
169
|
-
};
|
|
170
|
-
rpc_types_1.rpcServerCalls.saveFile = async (input) => {
|
|
171
|
-
const encoding = input.encoding || "utf8";
|
|
172
|
-
const dataBuffer = encoding === "base64" ? Buffer.from(input.data, "base64") : Buffer.from(input.data, "utf8");
|
|
173
|
-
// Kept: Family Hub 0.3.x doesn't declare its file fields, so nothing would reference these.
|
|
174
|
-
return await Files().saveFile({ name: input.name, mimeType: input.mimeType, keep: true }, new Uint8Array(dataBuffer));
|
|
175
|
-
};
|
package/dist/data/groups.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
12
12
|
iconClassName: z.ZodOptional<z.ZodString>;
|
|
13
13
|
publicRole: z.ZodOptional<z.ZodNativeEnum<typeof GroupMemberRole>>;
|
|
14
14
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
fileRetention: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
days: number | "never";
|
|
19
|
+
}, {
|
|
20
|
+
days: number | "never";
|
|
21
|
+
}>>;
|
|
15
22
|
founderUserId: z.ZodEffects<z.ZodString, string, string>;
|
|
16
23
|
signature: z.ZodString;
|
|
17
24
|
signedAt: z.ZodOptional<z.ZodNumber>;
|
|
@@ -29,6 +36,9 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
29
36
|
iconClassName?: string | undefined;
|
|
30
37
|
signedAt?: number | undefined;
|
|
31
38
|
publicRole?: GroupMemberRole | undefined;
|
|
39
|
+
fileRetention?: {
|
|
40
|
+
days: number | "never";
|
|
41
|
+
} | undefined;
|
|
32
42
|
}, {
|
|
33
43
|
name: string;
|
|
34
44
|
description: string;
|
|
@@ -41,6 +51,9 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
41
51
|
iconClassName?: string | undefined;
|
|
42
52
|
signedAt?: number | undefined;
|
|
43
53
|
publicRole?: GroupMemberRole | undefined;
|
|
54
|
+
fileRetention?: {
|
|
55
|
+
days: number | "never";
|
|
56
|
+
} | undefined;
|
|
44
57
|
}>;
|
|
45
58
|
/** A signed group record shared between personal and group contexts. */
|
|
46
59
|
export type IGroup = z.infer<typeof groupSchema>;
|
package/dist/data/groups.js
CHANGED
|
@@ -60,6 +60,12 @@ exports.groupSchema = zod_1.z.object({
|
|
|
60
60
|
.optional()
|
|
61
61
|
.describe("Whether the group is publicly readable"),
|
|
62
62
|
disabled: zod_1.z.boolean().optional().describe("Whether the group is currently disabled"),
|
|
63
|
+
// An object column stores JSON, so the number reads back as signed; a union would be a TEXT
|
|
64
|
+
// column that returns "30.0", and the row's signature would then fail.
|
|
65
|
+
fileRetention: zod_1.z
|
|
66
|
+
.object({ days: zod_1.z.union([zod_1.z.number().int().nonnegative(), zod_1.z.literal("never")]) })
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Days to retain unreferenced files before the grace period, or never; absent means 30 days."),
|
|
63
69
|
founderUserId: zod_types_1.zodPeerId.describe("The user ID of the group founder"),
|
|
64
70
|
signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
|
|
65
71
|
signedAt: zod_1.z
|
|
@@ -89,3 +89,18 @@ export declare function deletedSignedAt(tableName: string, context: ITableWriteC
|
|
|
89
89
|
* @param reason - Why, for logs and the sync recovery queue.
|
|
90
90
|
*/
|
|
91
91
|
export declare function writeNotAuthorized(tableName: string, reason: string): Error;
|
|
92
|
+
/**
|
|
93
|
+
* A {@link writeNotAuthorized} refusal that no data arriving later can lift, unlike one that
|
|
94
|
+
* waits on Users, Groups or GroupMembers rows still to sync. Sync keeps no recovery entry for
|
|
95
|
+
* such a refused delete of a record it has no row or other history for.
|
|
96
|
+
*
|
|
97
|
+
* @param tableName - Table the write was for.
|
|
98
|
+
* @param reason - Why, for logs.
|
|
99
|
+
*/
|
|
100
|
+
export declare function writeRefusedForGood(tableName: string, reason: string): Error;
|
|
101
|
+
/**
|
|
102
|
+
* Whether an error is a {@link writeRefusedForGood} refusal.
|
|
103
|
+
*
|
|
104
|
+
* @param error - The error a write check threw.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isWriteRefusedForGood(error: unknown): boolean;
|
|
@@ -7,6 +7,8 @@ exports.checkLocalSave = checkLocalSave;
|
|
|
7
7
|
exports.signingStamp = signingStamp;
|
|
8
8
|
exports.deletedSignedAt = deletedSignedAt;
|
|
9
9
|
exports.writeNotAuthorized = writeNotAuthorized;
|
|
10
|
+
exports.writeRefusedForGood = writeRefusedForGood;
|
|
11
|
+
exports.isWriteRefusedForGood = isWriteRefusedForGood;
|
|
10
12
|
/**
|
|
11
13
|
* Prefix of every refusal from a {@link TableWriteCheck}. Sync recovery parks a change refused
|
|
12
14
|
* with it, and checks it again only from local history, when the group's authority changes.
|
|
@@ -120,3 +122,22 @@ async function deletedSignedAt(tableName, context, existing) {
|
|
|
120
122
|
function writeNotAuthorized(tableName, reason) {
|
|
121
123
|
return new Error(`${exports.WRITE_NOT_AUTHORIZED}: ${tableName}: ${reason}`);
|
|
122
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* A {@link writeNotAuthorized} refusal that no data arriving later can lift, unlike one that
|
|
127
|
+
* waits on Users, Groups or GroupMembers rows still to sync. Sync keeps no recovery entry for
|
|
128
|
+
* such a refused delete of a record it has no row or other history for.
|
|
129
|
+
*
|
|
130
|
+
* @param tableName - Table the write was for.
|
|
131
|
+
* @param reason - Why, for logs.
|
|
132
|
+
*/
|
|
133
|
+
function writeRefusedForGood(tableName, reason) {
|
|
134
|
+
return Object.assign(writeNotAuthorized(tableName, reason), { refusedForGood: true });
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether an error is a {@link writeRefusedForGood} refusal.
|
|
138
|
+
*
|
|
139
|
+
* @param error - The error a write check threw.
|
|
140
|
+
*/
|
|
141
|
+
function isWriteRefusedForGood(error) {
|
|
142
|
+
return error?.refusedForGood === true;
|
|
143
|
+
}
|
|
@@ -21,6 +21,9 @@ export declare class Connection {
|
|
|
21
21
|
private _remoteDeviceInfo;
|
|
22
22
|
/** Trust assigned to the remote identity once the handshake completes. */
|
|
23
23
|
trustLevel: TrustLevel;
|
|
24
|
+
/** Synchronous diagnostics only; observer errors cannot affect handshake results. */
|
|
25
|
+
onHandshakeObserved?: (stage: "authenticated" | "failed", error?: unknown) => void;
|
|
26
|
+
private observeHandshake;
|
|
24
27
|
/** Server-side hook fired shortly after the successful handshake reply has been sent. */
|
|
25
28
|
onHandshakeComplete: ((deviceInfo: IDeviceInfo) => void) | undefined;
|
|
26
29
|
/** RPC timeout for {@link Connection.emit}. */
|
|
@@ -32,6 +32,16 @@ class Connection {
|
|
|
32
32
|
};
|
|
33
33
|
/** Trust assigned to the remote identity once the handshake completes. */
|
|
34
34
|
trustLevel = socket_type_1.TrustLevel.Unknown;
|
|
35
|
+
/** Synchronous diagnostics only; observer errors cannot affect handshake results. */
|
|
36
|
+
onHandshakeObserved;
|
|
37
|
+
observeHandshake(stage, error) {
|
|
38
|
+
try {
|
|
39
|
+
this.onHandshakeObserved?.(stage, error);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* diagnostics never affect the handshake */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
35
45
|
/** Server-side hook fired shortly after the successful handshake reply has been sent. */
|
|
36
46
|
onHandshakeComplete;
|
|
37
47
|
/** RPC timeout for {@link Connection.emit}. */
|
|
@@ -133,6 +143,7 @@ class Connection {
|
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
catch (e) {
|
|
146
|
+
this.observeHandshake("failed", e);
|
|
136
147
|
console.error("Error handling completeHandshake", e);
|
|
137
148
|
callback({ error: e?.message ?? String(e), errorType: "RPC_ERROR" });
|
|
138
149
|
}
|
|
@@ -412,6 +423,7 @@ class Connection {
|
|
|
412
423
|
};
|
|
413
424
|
this.trustLevel = trustLevel;
|
|
414
425
|
this._verified = true;
|
|
426
|
+
this.observeHandshake("authenticated");
|
|
415
427
|
return handshakeResponse;
|
|
416
428
|
}
|
|
417
429
|
/**
|
|
@@ -459,6 +471,7 @@ class Connection {
|
|
|
459
471
|
return handshakeResponse;
|
|
460
472
|
}
|
|
461
473
|
catch (e) {
|
|
474
|
+
this.observeHandshake("failed", e);
|
|
462
475
|
// A failed or timed-out attempt must neither retain its ephemeral secret nor let a
|
|
463
476
|
// late reply authenticate it. Do not clear state belonging to a newer handshake.
|
|
464
477
|
if (generation === this.handshakeGeneration)
|
|
@@ -522,6 +535,7 @@ class Connection {
|
|
|
522
535
|
: remoteDeviceInfo.deviceName,
|
|
523
536
|
};
|
|
524
537
|
this._verified = true;
|
|
538
|
+
this.observeHandshake("authenticated");
|
|
525
539
|
this._connectionAddress = remoteAddress;
|
|
526
540
|
return handshakeResponse;
|
|
527
541
|
}
|
|
@@ -344,6 +344,13 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
344
344
|
iconClassName: z.ZodOptional<z.ZodString>;
|
|
345
345
|
publicRole: z.ZodOptional<z.ZodNativeEnum<typeof import("../data").GroupMemberRole>>;
|
|
346
346
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
347
|
+
fileRetention: z.ZodOptional<z.ZodObject<{
|
|
348
|
+
days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
|
|
349
|
+
}, "strip", z.ZodTypeAny, {
|
|
350
|
+
days: number | "never";
|
|
351
|
+
}, {
|
|
352
|
+
days: number | "never";
|
|
353
|
+
}>>;
|
|
347
354
|
founderUserId: z.ZodEffects<z.ZodString, string, string>;
|
|
348
355
|
signature: z.ZodString;
|
|
349
356
|
signedAt: z.ZodOptional<z.ZodNumber>;
|
|
@@ -361,6 +368,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
361
368
|
iconClassName?: string | undefined;
|
|
362
369
|
signedAt?: number | undefined;
|
|
363
370
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
371
|
+
fileRetention?: {
|
|
372
|
+
days: number | "never";
|
|
373
|
+
} | undefined;
|
|
364
374
|
}, {
|
|
365
375
|
name: string;
|
|
366
376
|
description: string;
|
|
@@ -373,6 +383,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
373
383
|
iconClassName?: string | undefined;
|
|
374
384
|
signedAt?: number | undefined;
|
|
375
385
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
386
|
+
fileRetention?: {
|
|
387
|
+
days: number | "never";
|
|
388
|
+
} | undefined;
|
|
376
389
|
}>>;
|
|
377
390
|
membership: z.ZodLazy<z.ZodObject<{
|
|
378
391
|
groupMemberId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -654,6 +667,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
654
667
|
iconClassName?: string | undefined;
|
|
655
668
|
signedAt?: number | undefined;
|
|
656
669
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
670
|
+
fileRetention?: {
|
|
671
|
+
days: number | "never";
|
|
672
|
+
} | undefined;
|
|
657
673
|
};
|
|
658
674
|
seed: {
|
|
659
675
|
users: {
|
|
@@ -734,6 +750,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
734
750
|
iconClassName?: string | undefined;
|
|
735
751
|
signedAt?: number | undefined;
|
|
736
752
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
753
|
+
fileRetention?: {
|
|
754
|
+
days: number | "never";
|
|
755
|
+
} | undefined;
|
|
737
756
|
};
|
|
738
757
|
seed: {
|
|
739
758
|
users: {
|
|
@@ -1296,6 +1315,13 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1296
1315
|
iconClassName: z.ZodOptional<z.ZodString>;
|
|
1297
1316
|
publicRole: z.ZodOptional<z.ZodNativeEnum<typeof import("../data").GroupMemberRole>>;
|
|
1298
1317
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
1318
|
+
fileRetention: z.ZodOptional<z.ZodObject<{
|
|
1319
|
+
days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
|
|
1320
|
+
}, "strip", z.ZodTypeAny, {
|
|
1321
|
+
days: number | "never";
|
|
1322
|
+
}, {
|
|
1323
|
+
days: number | "never";
|
|
1324
|
+
}>>;
|
|
1299
1325
|
founderUserId: z.ZodEffects<z.ZodString, string, string>;
|
|
1300
1326
|
signature: z.ZodString;
|
|
1301
1327
|
signedAt: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1313,6 +1339,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1313
1339
|
iconClassName?: string | undefined;
|
|
1314
1340
|
signedAt?: number | undefined;
|
|
1315
1341
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1342
|
+
fileRetention?: {
|
|
1343
|
+
days: number | "never";
|
|
1344
|
+
} | undefined;
|
|
1316
1345
|
}, {
|
|
1317
1346
|
name: string;
|
|
1318
1347
|
description: string;
|
|
@@ -1325,6 +1354,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1325
1354
|
iconClassName?: string | undefined;
|
|
1326
1355
|
signedAt?: number | undefined;
|
|
1327
1356
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1357
|
+
fileRetention?: {
|
|
1358
|
+
days: number | "never";
|
|
1359
|
+
} | undefined;
|
|
1328
1360
|
}>>;
|
|
1329
1361
|
membership: z.ZodLazy<z.ZodObject<{
|
|
1330
1362
|
groupMemberId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -1606,6 +1638,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1606
1638
|
iconClassName?: string | undefined;
|
|
1607
1639
|
signedAt?: number | undefined;
|
|
1608
1640
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1641
|
+
fileRetention?: {
|
|
1642
|
+
days: number | "never";
|
|
1643
|
+
} | undefined;
|
|
1609
1644
|
};
|
|
1610
1645
|
seed: {
|
|
1611
1646
|
users: {
|
|
@@ -1686,6 +1721,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1686
1721
|
iconClassName?: string | undefined;
|
|
1687
1722
|
signedAt?: number | undefined;
|
|
1688
1723
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1724
|
+
fileRetention?: {
|
|
1725
|
+
days: number | "never";
|
|
1726
|
+
} | undefined;
|
|
1689
1727
|
};
|
|
1690
1728
|
seed: {
|
|
1691
1729
|
users: {
|
package/dist/rpc-types.d.ts
CHANGED
|
@@ -29,28 +29,6 @@ export interface IRemoteLogsQueryInput {
|
|
|
29
29
|
export declare const rpcServerCalls: {
|
|
30
30
|
encryptData: (value: string, groupId?: string) => Promise<string>;
|
|
31
31
|
tableMethodCall: (dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>;
|
|
32
|
-
/**
|
|
33
|
-
* @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
|
|
34
|
-
* 0.3.x; deleted once its port is Home's version.
|
|
35
|
-
*/
|
|
36
|
-
getFileContentsBase64: (fileId: string) => Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
|
|
39
|
-
* its port is Home's version.
|
|
40
|
-
*/
|
|
41
|
-
saveFile: (input: {
|
|
42
|
-
name: string;
|
|
43
|
-
mimeType?: string;
|
|
44
|
-
data: string;
|
|
45
|
-
encoding?: "utf8" | "base64";
|
|
46
|
-
fileId?: string;
|
|
47
|
-
}) => Promise<{
|
|
48
|
-
fileId: string;
|
|
49
|
-
name: string;
|
|
50
|
-
fileSize: number;
|
|
51
|
-
fileHash: string;
|
|
52
|
-
mimeType?: string;
|
|
53
|
-
}>;
|
|
54
32
|
deleteLocalDatabase: () => Promise<void>;
|
|
55
33
|
/** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
|
|
56
34
|
getOldDataDirInfo: () => Promise<{
|
package/dist/rpc-types.js
CHANGED
|
@@ -14,16 +14,6 @@ function rpcStub(rpcName) {
|
|
|
14
14
|
exports.rpcServerCalls = {
|
|
15
15
|
encryptData: rpcStub("encryptData"),
|
|
16
16
|
tableMethodCall: rpcStub("tableMethodCall"),
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
|
|
19
|
-
* 0.3.x; deleted once its port is Home's version.
|
|
20
|
-
*/
|
|
21
|
-
getFileContentsBase64: rpcStub("getFileContentsBase64"),
|
|
22
|
-
/**
|
|
23
|
-
* @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
|
|
24
|
-
* its port is Home's version.
|
|
25
|
-
*/
|
|
26
|
-
saveFile: rpcStub("saveFile"),
|
|
27
17
|
deleteLocalDatabase: rpcStub("deleteLocalDatabase"),
|
|
28
18
|
/** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
|
|
29
19
|
getOldDataDirInfo: rpcStub("getOldDataDirInfo"),
|