@hyperframes/share 0.9.26 → 0.9.28-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -1
- package/dist/index.d.mts +1 -0
- package/dist/projectLock.d.mts +2 -2
- package/dist/projectSnapshot.d.mts +3 -1
- package/dist/reviewExport.d.mts +11 -1
- package/dist/reviewUpload.d.mts +2 -0
- package/dist/shareSources.d.mts +24 -0
- package/dist/sources/adapterTypes.d.ts +39 -0
- package/dist/sources/api.d.mts +29 -0
- package/dist/sources/capture.d.mts +24 -0
- package/dist/sources/contract.d.mts +2 -0
- package/dist/sources/errors.d.mts +24 -0
- package/dist/sources/limits.d.mts +12 -0
- package/dist/sources/manifest.d.mts +23 -0
- package/dist/sources/pathPolicy.d.mts +25 -0
- package/dist/sources/projectState.d.mts +9 -0
- package/dist/sources/store.d.mts +3 -0
- package/dist/sources/transfer.d.mts +45 -0
- package/dist/sources/types.d.ts +353 -0
- package/dist/sources/unicode.d.mts +15 -0
- package/dist/sources/upload.d.mts +9 -0
- package/dist/sources/uploadFiles.d.mts +31 -0
- package/dist/sources/wire.d.mts +21 -0
- package/package.json +1 -1
- package/scripts/copy-types.mjs +3 -0
- package/src/index.mjs +6 -0
- package/src/projectLock.mjs +9 -2
- package/src/projectSnapshot.mjs +11 -4
- package/src/reviewExport.mjs +145 -37
- package/src/reviewUpload.mjs +18 -16
- package/src/shareSources.mjs +236 -0
- package/src/sources/adapterTypes.d.ts +39 -0
- package/src/sources/api.mjs +114 -0
- package/src/sources/capture.mjs +262 -0
- package/src/sources/contract/LICENSE-Unicode.txt +39 -0
- package/src/sources/contract/path-policy.json +311 -0
- package/src/sources/contract/unicode-15.1.0.json +6179 -0
- package/src/sources/contract.mjs +13 -0
- package/src/sources/errors.mjs +77 -0
- package/src/sources/limits.mjs +48 -0
- package/src/sources/manifest.mjs +154 -0
- package/src/sources/pathPolicy.mjs +197 -0
- package/src/sources/projectState.mjs +59 -0
- package/src/sources/store.mjs +82 -0
- package/src/sources/transfer.mjs +196 -0
- package/src/sources/types.d.ts +353 -0
- package/src/sources/unicode.mjs +163 -0
- package/src/sources/upload.mjs +201 -0
- package/src/sources/uploadFiles.mjs +263 -0
- package/src/sources/wire.mjs +205 -0
- package/LICENSE +0 -190
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
export interface SyncTooBig {
|
|
2
|
+
limit: "file_count" | "file_size" | "project_size" | "manifest_size";
|
|
3
|
+
actual: number;
|
|
4
|
+
maximum: number;
|
|
5
|
+
biggest: { path: string; size: number }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A file that keeps a project from being captured; it is never left out silently. */
|
|
9
|
+
export interface SyncCaptureProblem {
|
|
10
|
+
path: string;
|
|
11
|
+
problem: "symlink" | "special-file" | "unreadable" | "invalid-name" | "changed" | "no-entrypoint";
|
|
12
|
+
/** For `invalid-name`, the path policy's code (`reserved_name`, `path_collision`, ...). */
|
|
13
|
+
code?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ManifestFile {
|
|
17
|
+
path: string;
|
|
18
|
+
sha256: string;
|
|
19
|
+
size: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ManifestDocument {
|
|
23
|
+
format_version: 1;
|
|
24
|
+
entrypoint: "index.html";
|
|
25
|
+
files: ManifestFile[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A valid manifest: its document in canonical order, the canonical bytes and their SHA-256. */
|
|
29
|
+
export interface Manifest {
|
|
30
|
+
document: ManifestDocument;
|
|
31
|
+
canonical: Buffer;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Violation {
|
|
36
|
+
code: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
rule?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type LimitName = SyncTooBig["limit"];
|
|
42
|
+
|
|
43
|
+
export interface Limits {
|
|
44
|
+
maxFiles: number;
|
|
45
|
+
maxFileBytes: number;
|
|
46
|
+
maxProjectBytes: number;
|
|
47
|
+
maxManifestBytes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type OverLimit = SyncTooBig;
|
|
51
|
+
|
|
52
|
+
type ClientAction = "include" | "exclude" | "wait_then_exclude";
|
|
53
|
+
|
|
54
|
+
export interface PathRule {
|
|
55
|
+
id: string;
|
|
56
|
+
regex: string;
|
|
57
|
+
client_action: ClientAction;
|
|
58
|
+
server_action: "allow" | "reject";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The fields of contract/path-policy.json this client reads. */
|
|
62
|
+
export interface PolicyDocument {
|
|
63
|
+
policy_version: string;
|
|
64
|
+
stored_manifest: { legacy_agent_state_regex: string };
|
|
65
|
+
unicode: { version: string };
|
|
66
|
+
rules: PathRule[];
|
|
67
|
+
path: {
|
|
68
|
+
max_path_bytes: number;
|
|
69
|
+
max_segment_bytes: number;
|
|
70
|
+
forbidden_characters: string;
|
|
71
|
+
reserved_segment_regex: string;
|
|
72
|
+
canonical_spelling_roots: string[];
|
|
73
|
+
};
|
|
74
|
+
client_prefilter: { names: string[] };
|
|
75
|
+
client_symlinks: { skip_prefixes: string[] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type CaptureProblem = SyncCaptureProblem;
|
|
79
|
+
|
|
80
|
+
/** What an owner workspace's plan allows composition sync, as the limits route answered it. `unread` when it could not
|
|
81
|
+
* be read: `not-deployed` is a server without the route (a 404), and nothing is refused on an unread answer. */
|
|
82
|
+
export type SyncLimitsRead =
|
|
83
|
+
| {
|
|
84
|
+
at: "read";
|
|
85
|
+
syncAllowed: boolean;
|
|
86
|
+
maxCompositions: number;
|
|
87
|
+
/** Live compositions in the workspace when it was read. */
|
|
88
|
+
compositions: number;
|
|
89
|
+
push: Limits;
|
|
90
|
+
}
|
|
91
|
+
| { at: "unread"; why: "not-deployed" | "offline" | "failed" };
|
|
92
|
+
|
|
93
|
+
/** A file a capture lists, before hashing. */
|
|
94
|
+
export interface Found {
|
|
95
|
+
path: string;
|
|
96
|
+
full: string;
|
|
97
|
+
size: number;
|
|
98
|
+
mtimeMs: number;
|
|
99
|
+
ino: number;
|
|
100
|
+
dev: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface Walk {
|
|
104
|
+
files: Found[];
|
|
105
|
+
problems: CaptureProblem[];
|
|
106
|
+
waiting: string[];
|
|
107
|
+
skippedLinks: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type Capture =
|
|
111
|
+
| {
|
|
112
|
+
at: "captured";
|
|
113
|
+
manifest: Manifest;
|
|
114
|
+
settled: { agentTurnOpen: boolean; projectLocked: boolean };
|
|
115
|
+
waiting: string[];
|
|
116
|
+
skippedLinks: string[];
|
|
117
|
+
}
|
|
118
|
+
| { at: "problems"; problems: CaptureProblem[] }
|
|
119
|
+
| { at: "too-big"; over: OverLimit };
|
|
120
|
+
|
|
121
|
+
export type SyncErrorKind =
|
|
122
|
+
| "offline"
|
|
123
|
+
| "signed-out"
|
|
124
|
+
| "malformed"
|
|
125
|
+
| "not-enabled"
|
|
126
|
+
| "byok"
|
|
127
|
+
| "no-write-role"
|
|
128
|
+
| "forbidden"
|
|
129
|
+
| "composition-not-found"
|
|
130
|
+
| "not-found"
|
|
131
|
+
| "too-large"
|
|
132
|
+
| "reference-invalid"
|
|
133
|
+
| "upload-not-pending"
|
|
134
|
+
| "active-uploads-exceeded"
|
|
135
|
+
| "file-not-writable"
|
|
136
|
+
| "idempotency-key-reused"
|
|
137
|
+
| "request-in-progress"
|
|
138
|
+
| "snapshot-not-downloadable"
|
|
139
|
+
| "conflict-not-open"
|
|
140
|
+
| "download-expired"
|
|
141
|
+
| "invalid-parameter"
|
|
142
|
+
| "plan-required"
|
|
143
|
+
| "limit-reached"
|
|
144
|
+
| "rate-limited"
|
|
145
|
+
| "unavailable"
|
|
146
|
+
| "server-error"
|
|
147
|
+
| "refused";
|
|
148
|
+
|
|
149
|
+
export interface Composition {
|
|
150
|
+
id: string;
|
|
151
|
+
spaceId: string;
|
|
152
|
+
title: string;
|
|
153
|
+
createdBy: string;
|
|
154
|
+
/** Null before the first publish. */
|
|
155
|
+
latestSnapshotId: string | null;
|
|
156
|
+
createdAt: string;
|
|
157
|
+
updatedAt: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface Snapshot {
|
|
161
|
+
id: string;
|
|
162
|
+
compositionId: string;
|
|
163
|
+
baseSnapshotId: string | null;
|
|
164
|
+
publicationOutcome: "published" | "conflict_saved";
|
|
165
|
+
conflictedWithSnapshotId: string | null;
|
|
166
|
+
conflictState: "open" | "resolved" | "discarded" | null;
|
|
167
|
+
resolvedBySnapshotId: string | null;
|
|
168
|
+
createdBy: string;
|
|
169
|
+
createdByName?: string | null;
|
|
170
|
+
clientType: string;
|
|
171
|
+
summary: string | null;
|
|
172
|
+
summarySource: string;
|
|
173
|
+
manifestSha256: string;
|
|
174
|
+
createdAt: string;
|
|
175
|
+
/** Only with `include=manifest`, and only once its bytes hash to manifestSha256. */
|
|
176
|
+
manifest: Manifest | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type UploadStatus = "pending" | "verifying" | "completed" | "failed" | "expired";
|
|
180
|
+
|
|
181
|
+
export interface UploadReceipt {
|
|
182
|
+
uploadId: string;
|
|
183
|
+
compositionId: string;
|
|
184
|
+
snapshotId: string;
|
|
185
|
+
status: UploadStatus;
|
|
186
|
+
/** Why a failed upload failed: cancelled, content_incomplete, verification_timeout, access_revoked. */
|
|
187
|
+
errorCode: string | null;
|
|
188
|
+
baseSnapshotId: string | null;
|
|
189
|
+
resolvesConflictSnapshotIds: string[];
|
|
190
|
+
manifestSha256: string;
|
|
191
|
+
uploadDeadline: string;
|
|
192
|
+
verificationDeadline: string | null;
|
|
193
|
+
completedAt: string | null;
|
|
194
|
+
result: {
|
|
195
|
+
snapshotId: string;
|
|
196
|
+
publicationOutcome: "published" | "conflict_saved";
|
|
197
|
+
conflictedWithSnapshotId: string | null;
|
|
198
|
+
} | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface BlobRef {
|
|
202
|
+
sha256: string;
|
|
203
|
+
size: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface BeginResult extends UploadReceipt {
|
|
207
|
+
/** Already saved in the base or a named conflict: never uploaded. */
|
|
208
|
+
present: BlobRef[];
|
|
209
|
+
/** To upload through upload-urls before submitting. */
|
|
210
|
+
needed: BlobRef[];
|
|
211
|
+
/** False for a replay of the same push under the same Idempotency-Key. */
|
|
212
|
+
created: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface BeginRequest {
|
|
216
|
+
idempotencyKey: string;
|
|
217
|
+
baseSnapshotId: string | null;
|
|
218
|
+
resolvesConflictSnapshotIds: string[];
|
|
219
|
+
summary: { text: string; source: "user" | "agent" } | null;
|
|
220
|
+
manifest: Manifest;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface PartGrant {
|
|
224
|
+
partNumber: number;
|
|
225
|
+
length: number;
|
|
226
|
+
url: string;
|
|
227
|
+
headers: Record<string, string>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface FileGrant {
|
|
231
|
+
sha256: string;
|
|
232
|
+
size: number;
|
|
233
|
+
expiresAt: string;
|
|
234
|
+
/** Files up to 64 MiB: one PUT. */
|
|
235
|
+
url: string | null;
|
|
236
|
+
headers: Record<string, string>;
|
|
237
|
+
/** Files over 64 MiB: the file's one multipart upload, and the requested parts. */
|
|
238
|
+
multipartUploadId: string | null;
|
|
239
|
+
parts: PartGrant[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface GrantRequest {
|
|
243
|
+
sha256: string;
|
|
244
|
+
multipartUploadId?: string;
|
|
245
|
+
parts?: { partNumber: number; sha256: string }[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface DownloadBatch {
|
|
249
|
+
snapshotId: string;
|
|
250
|
+
manifestSha256: string;
|
|
251
|
+
continuationToken: string;
|
|
252
|
+
continuationExpiresAt: string;
|
|
253
|
+
urls: { sha256: string; size: number; url: string; expiresAt: string }[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface Page<T> {
|
|
257
|
+
items: T[];
|
|
258
|
+
nextToken: string | null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type CompositionRead =
|
|
262
|
+
| { at: "fresh"; composition: Composition; etag: string | null }
|
|
263
|
+
| { at: "unchanged"; etag: string };
|
|
264
|
+
|
|
265
|
+
/** Whose binding: the backend's API host, the signed-in account (accountPrincipal) and the project's real path. */
|
|
266
|
+
export interface BindingKey {
|
|
267
|
+
backend: string;
|
|
268
|
+
account: string;
|
|
269
|
+
projectPath: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A push recorded before its Begin is sent, so a lost answer is recovered by sending the same body under the same
|
|
273
|
+
* Idempotency-Key; the upload's ids and deadlines land once an answer comes back. */
|
|
274
|
+
export interface InflightUpload {
|
|
275
|
+
idempotencyKey: string;
|
|
276
|
+
baseSnapshotId: string | null;
|
|
277
|
+
resolvesConflictSnapshotIds: string[];
|
|
278
|
+
summary: { text: string; source: "user" | "agent" } | null;
|
|
279
|
+
manifest: ManifestDocument;
|
|
280
|
+
manifestSha256: string;
|
|
281
|
+
uploadId: string | null;
|
|
282
|
+
snapshotId: string | null;
|
|
283
|
+
uploadDeadline: string | null;
|
|
284
|
+
verificationDeadline: string | null;
|
|
285
|
+
begunAt: number;
|
|
286
|
+
/** Files whose bytes reached staging, by SHA-256, so a resumed send does not write them again. */
|
|
287
|
+
sent?: string[];
|
|
288
|
+
/** Each multipart file's pinned upload and the parts already written, by the file's SHA-256. */
|
|
289
|
+
multipart?: Record<string, { uploadId: string; partsDone: number[] }>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface Binding extends BindingKey {
|
|
293
|
+
version: 1;
|
|
294
|
+
/** Client-generated, kept before the first request so a retried create names the same composition. */
|
|
295
|
+
compositionId: string;
|
|
296
|
+
ownerSpaceId: string;
|
|
297
|
+
/** The owner workspace as the account's list last named it, so a project whose workspace the account left can
|
|
298
|
+
* still be named. Absent from bindings made before it was kept, and until a list names the workspace. */
|
|
299
|
+
owner?: { name: string; kind: "personal" | "team" };
|
|
300
|
+
boundAt: number;
|
|
301
|
+
/** The composition as last read, and the ETag it came with; null before the create was answered. */
|
|
302
|
+
composition: Composition | null;
|
|
303
|
+
etag: string | null;
|
|
304
|
+
/** The snapshot this copy was last sent as or got from, with its manifest, so local changes need no network. */
|
|
305
|
+
base: { snapshotId: string; manifest: ManifestDocument; manifestSha256: string } | null;
|
|
306
|
+
inflight: InflightUpload | null;
|
|
307
|
+
/** Set while Get from the cloud is filling a folder it made, until the folder is added to Home. Until then the
|
|
308
|
+
* folder is not a project on this Mac, and the next Get of the composition finishes it. */
|
|
309
|
+
gettingFromCloud?: true;
|
|
310
|
+
/** What the marker that Get put in its folder holds: the only proof a file of that name there is not the person's. */
|
|
311
|
+
getToken?: string;
|
|
312
|
+
/** Set on the folder a Duplicate fills from the cloud: it is bound only while it downloads, so no Get ever opens
|
|
313
|
+
* it, and its binding goes once latest is in (duplicate.mjs). */
|
|
314
|
+
duplicate?: true;
|
|
315
|
+
/** The conflicts this Mac's sends saved, newest last: Compare names them as this project's, not another
|
|
316
|
+
* computer's. */
|
|
317
|
+
savedHere?: string[];
|
|
318
|
+
/** Set on a binding Work on {owner}'s project made from a share: the account reaches the owner's workspace through
|
|
319
|
+
* that share's grant, not membership, so every request names the review project (sync-backed sharing C5). */
|
|
320
|
+
sharedFrom?: SharedFrom;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The share a binding came from: its review project, the owner's name and their workspace's, as the share named
|
|
324
|
+
* them when it was opened. */
|
|
325
|
+
export interface SharedFrom {
|
|
326
|
+
reviewProjectId: string;
|
|
327
|
+
owner: string;
|
|
328
|
+
workspace: string | null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** A workspace's last refusal of a new composition: not admitted to sync yet, or encrypted (BYOK). */
|
|
332
|
+
export interface SpaceRefusal {
|
|
333
|
+
kind: "not-enabled" | "byok";
|
|
334
|
+
at: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** One send or get under way for a project: the binding it owns until it ends, and where its states go. */
|
|
338
|
+
export interface UploadJob {
|
|
339
|
+
api: import("./api.mjs").SourceApi;
|
|
340
|
+
space: string;
|
|
341
|
+
composition: string;
|
|
342
|
+
uploadId: string;
|
|
343
|
+
inflight: InflightUpload;
|
|
344
|
+
local: Map<string, { path: string; full: string }>;
|
|
345
|
+
needed: { sha256: string; size: number }[];
|
|
346
|
+
fetchImpl: typeof fetch;
|
|
347
|
+
wait: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
348
|
+
signal: AbortSignal;
|
|
349
|
+
now: () => number;
|
|
350
|
+
persist: () => void;
|
|
351
|
+
onBytes: (bytes: number) => void;
|
|
352
|
+
onFile: () => void;
|
|
353
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Whether every code point is assigned in Unicode 15.1. @param {string} value */
|
|
2
|
+
export function allAssigned(value: string): boolean;
|
|
3
|
+
/** The spelling two names share on a case-insensitive or normalizing filesystem: NFC(casefold(NFC(value))).
|
|
4
|
+
* @param {string} value */
|
|
5
|
+
export function comparisonKey(value: string): string;
|
|
6
|
+
export const UNICODE_VERSION: "15.1.0";
|
|
7
|
+
export function nfc(value: string): string;
|
|
8
|
+
export type UnicodeTableFile = {
|
|
9
|
+
unicode_version: string;
|
|
10
|
+
casefold: Record<string, string>;
|
|
11
|
+
combining: Record<string, number>;
|
|
12
|
+
decomposition: Record<string, number[]>;
|
|
13
|
+
composition: Record<string, number>;
|
|
14
|
+
assigned_ranges: [number, number][];
|
|
15
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Resolve an older capture before starting an upload for the current files.
|
|
2
|
+
* @param {SourceUpload} job @returns {Promise<import("./adapterTypes.js").SourceResult | null>} */
|
|
3
|
+
export function recoverSourceUpload(job: SourceUpload): Promise<import("./adapterTypes.js").SourceResult | null>;
|
|
4
|
+
/** Only published verification results can become share source; conflict_saved is preserved for explicit recovery.
|
|
5
|
+
* @param {SourceRun} run @returns {Promise<import("./adapterTypes.js").SourceResult>} */
|
|
6
|
+
export function uploadSource(run: SourceRun): Promise<import("./adapterTypes.js").SourceResult>;
|
|
7
|
+
export type SourceRun = import("./adapterTypes.js").SourceRun;
|
|
8
|
+
export type SourceUpload = Pick<SourceRun, "binding" | "api" | "store">;
|
|
9
|
+
export type UploadReceipt = import("./types.js").UploadReceipt;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Writes every needed file the in-flight record does not list as sent. @param {UploadJob} job */
|
|
2
|
+
export function uploadNeeded(job: UploadJob): Promise<void>;
|
|
3
|
+
/** The file's multipart upload can take no more parts (`hyperframes_composition_upload_file_not_writable`): this
|
|
4
|
+
* upload cannot finish, so its owner must resolve it before a retry can begin a new one. */
|
|
5
|
+
export class NotWritable extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export type UploadJob = import("./types.js").UploadJob;
|
|
9
|
+
export type Signed = {
|
|
10
|
+
url: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
};
|
|
14
|
+
export type Part = {
|
|
15
|
+
number: number;
|
|
16
|
+
sha256: string;
|
|
17
|
+
start: number;
|
|
18
|
+
length: number;
|
|
19
|
+
};
|
|
20
|
+
export type Bytes = {
|
|
21
|
+
file: {
|
|
22
|
+
path: string;
|
|
23
|
+
full: string;
|
|
24
|
+
};
|
|
25
|
+
start: number;
|
|
26
|
+
length: number;
|
|
27
|
+
sha256: string;
|
|
28
|
+
};
|
|
29
|
+
export type FileGrant = import("./types.js").FileGrant;
|
|
30
|
+
export type GrantRequest = import("./types.js").GrantRequest;
|
|
31
|
+
export type InflightUpload = import("./types.js").InflightUpload;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @param {unknown} value @param {string} field @returns {Record<string, unknown>} */
|
|
2
|
+
export function objectOf(value: unknown, field: string): Record<string, unknown>;
|
|
3
|
+
/** @param {unknown} raw @returns {Composition} */
|
|
4
|
+
export function compositionOf(raw: unknown): Composition;
|
|
5
|
+
/** @param {unknown} raw @returns {Snapshot} */
|
|
6
|
+
export function snapshotOf(raw: unknown): Snapshot;
|
|
7
|
+
/** @param {unknown} raw @returns {UploadReceipt} */
|
|
8
|
+
export function uploadOf(raw: unknown): UploadReceipt;
|
|
9
|
+
/** @param {unknown} raw @returns {Extract<SyncLimitsRead, { at: "read" }>} */
|
|
10
|
+
export function syncLimitsOf(raw: unknown): Extract<SyncLimitsRead, {
|
|
11
|
+
at: "read";
|
|
12
|
+
}>;
|
|
13
|
+
export function beginOf(raw: unknown, created: boolean): BeginResult;
|
|
14
|
+
export function grantsOf(raw: unknown): FileGrant[];
|
|
15
|
+
export type SyncLimitsRead = import("./types.js").SyncLimitsRead;
|
|
16
|
+
export type Composition = import("./types.js").Composition;
|
|
17
|
+
export type Snapshot = import("./types.js").Snapshot;
|
|
18
|
+
export type UploadReceipt = import("./types.js").UploadReceipt;
|
|
19
|
+
export type BeginResult = import("./types.js").BeginResult;
|
|
20
|
+
export type FileGrant = import("./types.js").FileGrant;
|
|
21
|
+
export type DownloadBatch = import("./types.js").DownloadBatch;
|
package/package.json
CHANGED
package/scripts/copy-types.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import { cp } from "node:fs/promises";
|
|
2
2
|
await cp("src/types", "dist/types", { recursive: true });
|
|
3
3
|
await cp("src/hosts.d.ts", "dist/hosts.d.ts");
|
|
4
|
+
|
|
5
|
+
await cp("src/sources/types.d.ts", "dist/sources/types.d.ts");
|
|
6
|
+
await cp("src/sources/adapterTypes.d.ts", "dist/sources/adapterTypes.d.ts");
|
package/src/index.mjs
CHANGED
|
@@ -80,3 +80,9 @@ export { bindShareHost } from "./host.mjs";
|
|
|
80
80
|
/** @typedef {import("./types/share.js").AccessMode} AccessMode */
|
|
81
81
|
/** @typedef {import("./types/share.js").ReviewExportProgress} ReviewExportProgress */
|
|
82
82
|
export { pendingReviewExport } from "./reviewExport.mjs";
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
shareSources,
|
|
86
|
+
shareSourceFingerprint,
|
|
87
|
+
createProjectShareSourceStore,
|
|
88
|
+
} from "./shareSources.mjs";
|
package/src/projectLock.mjs
CHANGED
|
@@ -80,7 +80,7 @@ async function claim(candidate, lock, owner) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
/** @template T @param {string} projectPath @param {"review" | "export"} lane @param {() => Promise<T>} run
|
|
83
|
+
/** @template T @param {string} projectPath @param {"review" | "export" | "source"} lane @param {() => Promise<T>} run
|
|
84
84
|
* @param {{ timeoutMs?: number, signal?: AbortSignal }} [options] @returns {Promise<T>} */
|
|
85
85
|
export async function withProjectShareLock(
|
|
86
86
|
projectPath,
|
|
@@ -90,7 +90,14 @@ export async function withProjectShareLock(
|
|
|
90
90
|
) {
|
|
91
91
|
const root = await realpath(projectPath);
|
|
92
92
|
const folder = await mkdirInside(root, ".hyperframes");
|
|
93
|
-
const lock = join(
|
|
93
|
+
const lock = join(
|
|
94
|
+
folder,
|
|
95
|
+
lane === "review"
|
|
96
|
+
? "review.lock"
|
|
97
|
+
: lane === "source"
|
|
98
|
+
? "share-source.lock"
|
|
99
|
+
: "review-export.lock",
|
|
100
|
+
);
|
|
94
101
|
const owner = { token: randomUUID(), pid: process.pid, hostname: hostname() };
|
|
95
102
|
const candidate = `${lock}.${owner.token}.tmp`;
|
|
96
103
|
await writeFile(candidate, JSON.stringify(owner), { flag: "wx", mode: 0o600 });
|
package/src/projectSnapshot.mjs
CHANGED
|
@@ -31,7 +31,7 @@ async function snapshotKey(full, byContent) {
|
|
|
31
31
|
const stat = await handle.stat();
|
|
32
32
|
if (!stat.isFile()) return undefined;
|
|
33
33
|
if (full.endsWith(".html")) return contentKey(await handle.readFile("utf8"));
|
|
34
|
-
if (byContent) return
|
|
34
|
+
if (byContent) return await hashFile(handle);
|
|
35
35
|
return `${stat.mtimeMs}:${stat.size}`;
|
|
36
36
|
} catch {
|
|
37
37
|
return undefined;
|
|
@@ -40,6 +40,13 @@ async function snapshotKey(full, byContent) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** @param {import("node:fs/promises").FileHandle} handle */
|
|
44
|
+
async function hashFile(handle) {
|
|
45
|
+
const hash = createHash("sha256");
|
|
46
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) hash.update(chunk);
|
|
47
|
+
return hash.digest("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
/** @param {import("node:fs").Dirent} entry @param {string} full @param {boolean} followSymlinks */
|
|
44
51
|
async function entryKind(entry, full, followSymlinks) {
|
|
45
52
|
if (!entry.isSymbolicLink()) return entry;
|
|
@@ -115,6 +122,6 @@ export function diffChangedFiles(before, after) {
|
|
|
115
122
|
return changed;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
/** @param {string} projectPath */
|
|
119
|
-
export const projectFingerprint = async (projectPath) =>
|
|
120
|
-
snapshotFingerprint(await snapshotProject(projectPath, { followSymlinks: true }));
|
|
125
|
+
/** @param {string} projectPath @param {{ byContent?: boolean }} [options] */
|
|
126
|
+
export const projectFingerprint = async (projectPath, options) =>
|
|
127
|
+
snapshotFingerprint(await snapshotProject(projectPath, { ...options, followSymlinks: true }));
|