@sequenceholdings/storage 0.1.2 → 0.1.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/README.md +8 -0
- package/dist/client.d.ts +2 -529
- package/dist/client.js +16 -320
- package/dist/contracts.d.ts +2 -362
- package/dist/contracts.js +16 -230
- package/dist/crc32c.d.ts +2 -11
- package/dist/crc32c.js +16 -74
- package/dist/index.d.ts +2 -6
- package/dist/index.js +2 -6
- package/dist/paths.d.ts +2 -4
- package/dist/paths.js +16 -26
- package/dist/scope.d.ts +2 -15
- package/dist/scope.js +16 -40
- package/dist/transfer.d.ts +2 -34
- package/dist/transfer.js +16 -263
- package/package.json +3 -2
package/dist/client.js
CHANGED
|
@@ -1,322 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const contracts_js_1 = require("./contracts.js");
|
|
8
|
-
const transfer_js_1 = require("./transfer.js");
|
|
9
|
-
exports.STORAGE_TOKEN_HEADER = 'x-sequence-storage-token';
|
|
10
|
-
exports.STORAGE_SERVICE_TOKEN_HEADER = 'x-sequence-storage-token-service';
|
|
11
|
-
class StorageApiError extends Error {
|
|
12
|
-
status;
|
|
13
|
-
code;
|
|
14
|
-
constructor(status, code, message) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.status = status;
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.name = 'StorageApiError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
exports.StorageApiError = StorageApiError;
|
|
22
|
-
/** ~5 attempts within ~10s: long enough for the outbox sweeper to catch up. */
|
|
23
|
-
const PERMISSIONS_RETRY = { attempts: 5, baseDelayMs: 625 };
|
|
24
|
-
/** Metadata requests fail fast so a replay-safe caller can retry them. */
|
|
25
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
26
|
-
// Duck-typed so errors re-thrown by an Artifact bridge transport still match.
|
|
27
|
-
function errorStatus(error) {
|
|
28
|
-
return typeof error === 'object' &&
|
|
29
|
-
error !== null &&
|
|
30
|
-
'status' in error &&
|
|
31
|
-
typeof error.status === 'number'
|
|
32
|
-
? error.status
|
|
33
|
-
: undefined;
|
|
34
|
-
}
|
|
35
|
-
function errorCode(error) {
|
|
36
|
-
return typeof error === 'object' &&
|
|
37
|
-
error !== null &&
|
|
38
|
-
'code' in error &&
|
|
39
|
-
typeof error.code === 'string'
|
|
40
|
-
? error.code
|
|
41
|
-
: undefined;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The server's retryable 503s: a just-committed permission change is still
|
|
45
|
-
* being applied (`permissions_pending`), or the workspace lock is saturated
|
|
46
|
-
* (`storage_busy`). Neither leaves partial state behind for a replay-safe call.
|
|
47
|
-
*/
|
|
48
|
-
function isRetryableConflict(error) {
|
|
49
|
-
const code = errorCode(error);
|
|
50
|
-
return (errorStatus(error) === 503 &&
|
|
51
|
-
(code === 'permissions_pending' || code === 'storage_busy'));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* A start whose reply never arrived or failed server-side may still have
|
|
55
|
-
* committed. With its idempotency key it is safe to resend, and the server
|
|
56
|
-
* replays the committed session instead of reserving a second one.
|
|
57
|
-
*/
|
|
58
|
-
function isUnacknowledged(error) {
|
|
59
|
-
if (error instanceof zod_1.z.ZodError)
|
|
60
|
-
return false;
|
|
61
|
-
const status = errorStatus(error);
|
|
62
|
-
// No status: the request never got an HTTP answer (network, bridge).
|
|
63
|
-
if (status === undefined)
|
|
64
|
-
return true;
|
|
65
|
-
return status === 0 || status === 408 || status >= 500;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Retries a replay-safe request while `retryable` says its failure may clear:
|
|
69
|
-
* reads, and mutations the server deduplicates. Byte transfers never come here.
|
|
70
|
-
*/
|
|
71
|
-
async function retryReplaySafe({ run, retry, retryable, }) {
|
|
72
|
-
const attempts = retry.attempts ?? PERMISSIONS_RETRY.attempts;
|
|
73
|
-
const baseDelayMs = retry.baseDelayMs ?? PERMISSIONS_RETRY.baseDelayMs;
|
|
74
|
-
for (let attempt = 1;; attempt++) {
|
|
75
|
-
try {
|
|
76
|
-
return await run();
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
if (attempt >= attempts || !retryable(error))
|
|
80
|
-
throw error;
|
|
81
|
-
// Equal jitter: spread a burst of callers without retrying immediately.
|
|
82
|
-
const ceiling = baseDelayMs * 2 ** (attempt - 1);
|
|
83
|
-
await new Promise((resolve) => setTimeout(resolve, ceiling / 2 + (Math.random() * ceiling) / 2));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function readJson(response) {
|
|
88
|
-
const text = await response.text();
|
|
89
|
-
try {
|
|
90
|
-
return { parsed: true, value: JSON.parse(text) };
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
91
7
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
exports.storageDownloadSchema = zod_1.z.object({
|
|
104
|
-
url: zod_1.z.url(),
|
|
105
|
-
expiresAt: zod_1.z.string(),
|
|
106
|
-
preview: zod_1.z.boolean(),
|
|
107
|
-
});
|
|
108
|
-
/** Supply a transport for an Artifact bridge, or use createStorageClient for HTTP. */
|
|
109
|
-
function storageClient(transport, { permissionsRetry = {}, } = {}) {
|
|
110
|
-
const send = async ({ method, path, schema, body, retryable, }) => {
|
|
111
|
-
const run = () => transport.request(method, `/api/storage${path}`, body);
|
|
112
|
-
try {
|
|
113
|
-
return schema.parse(retryable
|
|
114
|
-
? await retryReplaySafe({ run, retry: permissionsRetry, retryable })
|
|
115
|
-
: await run());
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
// A mutation can commit and then fail re-reading its result, or commit
|
|
119
|
-
// one step before a later one hits the busy lock, so neither reply
|
|
120
|
-
// proves nothing changed.
|
|
121
|
-
if (!retryable && isRetryableConflict(error))
|
|
122
|
-
throw new StorageApiError(503, errorCode(error) ?? 'permissions_pending', errorCode(error) === 'storage_busy'
|
|
123
|
-
? 'The workspace was busy. The change may have been applied; refresh before retrying.'
|
|
124
|
-
: 'Permissions are still synchronizing. The change may have been applied; refresh before retrying.');
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
/** Mutation: never replayed automatically. */
|
|
129
|
-
const request = (method, path, schema, body) => send({ method, path, schema, body });
|
|
130
|
-
/** Read, or a mutation the server deduplicates: retried on permissions_pending / storage_busy. */
|
|
131
|
-
const replayable = (method, path, schema, body) => send({ method, path, schema, body, retryable: isRetryableConflict });
|
|
132
|
-
return {
|
|
133
|
-
capabilities: () => replayable('GET', '/capabilities', zod_1.z.object({
|
|
134
|
-
canProvision: zod_1.z.boolean(),
|
|
135
|
-
uploadMethods: zod_1.z.array(zod_1.z.string()),
|
|
136
|
-
pendingUploads: zod_1.z.object({
|
|
137
|
-
perPrincipal: zod_1.z.number().int(),
|
|
138
|
-
perWorkspace: zod_1.z.number().int(),
|
|
139
|
-
}),
|
|
140
|
-
uploadBatchLimit: zod_1.z.number().int(),
|
|
141
|
-
importConcurrency: zod_1.z.object({
|
|
142
|
-
default: zod_1.z.number().int(),
|
|
143
|
-
max: zod_1.z.number().int(),
|
|
144
|
-
}),
|
|
145
|
-
})),
|
|
146
|
-
list: (options = {}) => replayable('GET', `/workspaces${query({ ...options })}`, (0, contracts_js_1.storagePage)(contracts_js_1.workspaceSchema)),
|
|
147
|
-
shared: (options = {}) => replayable('GET', `/shared${query({ ...options })}`, (0, contracts_js_1.storagePage)(contracts_js_1.nodeSchema)),
|
|
148
|
-
// Manifest apply converges on the named workspace, so it is replay-safe.
|
|
149
|
-
apply: (manifest) => replayable('POST', '/workspaces', contracts_js_1.workspaceSchema, contracts_js_1.workspaceManifestSchema.parse(manifest)),
|
|
150
|
-
workspace(id) {
|
|
151
|
-
const root = `/workspaces/${contracts_js_1.storageId.parse(id)}`;
|
|
152
|
-
const node = (nodeId) => `${root}/nodes/${contracts_js_1.storageId.parse(nodeId)}`;
|
|
153
|
-
const session = (uploadId) => `${root}/uploads/${contracts_js_1.storageId.parse(uploadId)}`;
|
|
154
|
-
const access = (nodeId) => `${nodeId ? node(nodeId) : root}/access`;
|
|
155
|
-
const upload = {
|
|
156
|
-
// The idempotency key makes a replayed start return the same session,
|
|
157
|
-
// so it is also resent when its reply was lost: a start that committed
|
|
158
|
-
// but timed out would otherwise hold a pending-upload slot unnamed
|
|
159
|
-
// until it expires.
|
|
160
|
-
start: (input) => send({
|
|
161
|
-
method: 'POST',
|
|
162
|
-
path: `${root}/uploads`,
|
|
163
|
-
schema: contracts_js_1.uploadSessionSchema,
|
|
164
|
-
body: contracts_js_1.startUploadSchema.parse(input),
|
|
165
|
-
retryable: (error) => isRetryableConflict(error) || isUnacknowledged(error),
|
|
166
|
-
}),
|
|
167
|
-
get: (id) => replayable('GET', session(id), contracts_js_1.uploadSessionSchema),
|
|
168
|
-
/** `resume` passes the CRC32C it computed; pass your own when transferring bytes yourself. */
|
|
169
|
-
complete: (id, input = {}) => replayable('POST', `${session(id)}/complete`, contracts_js_1.uploadSessionSchema, contracts_js_1.completeUploadSchema.parse(input)),
|
|
170
|
-
// Abort is idempotent: an aborted session stays aborted, and a completed
|
|
171
|
-
// one deterministically rejects, so a replay cannot change the outcome.
|
|
172
|
-
abort: (id) => replayable('DELETE', session(id), contracts_js_1.uploadSessionSchema),
|
|
173
|
-
startBatch: (items) => send({
|
|
174
|
-
method: 'POST',
|
|
175
|
-
path: `${root}/uploads/batch`,
|
|
176
|
-
schema: zod_1.z.object({ items: zod_1.z.array(contracts_js_1.uploadSessionSchema) }),
|
|
177
|
-
body: contracts_js_1.startUploadBatchSchema.parse({ items }),
|
|
178
|
-
retryable: (error) => isRetryableConflict(error) || isUnacknowledged(error),
|
|
179
|
-
}),
|
|
180
|
-
list: () => replayable('GET', `${root}/uploads`, contracts_js_1.uploadListSchema),
|
|
181
|
-
reconcile: (input = {}) => send({
|
|
182
|
-
method: 'POST',
|
|
183
|
-
path: `${root}/uploads/reconcile`,
|
|
184
|
-
schema: contracts_js_1.reconcileUploadsResultSchema,
|
|
185
|
-
body: contracts_js_1.reconcileUploadsSchema.parse(input),
|
|
186
|
-
retryable: isRetryableConflict,
|
|
187
|
-
}),
|
|
188
|
-
async resume(id, source, options = {}) {
|
|
189
|
-
const current = await upload.get(id);
|
|
190
|
-
if (current.state === 'complete')
|
|
191
|
-
return current;
|
|
192
|
-
if (!current.uploadUrl || current.state !== 'pending')
|
|
193
|
-
throw new StorageApiError(409, 'upload_unavailable', 'Upload is no longer pending');
|
|
194
|
-
const { crc32c } = await (0, transfer_js_1.transferStorageUpload)({
|
|
195
|
-
...options,
|
|
196
|
-
uploadUrl: current.uploadUrl,
|
|
197
|
-
maxBytes: current.maxBytes,
|
|
198
|
-
source,
|
|
199
|
-
});
|
|
200
|
-
return upload.complete(id, { crc32c });
|
|
201
|
-
},
|
|
202
|
-
/**
|
|
203
|
-
* Start, transfer and complete in one call. A failure after the session
|
|
204
|
-
* starts aborts it (best effort) to release its reservation; pass
|
|
205
|
-
* `keepSessionOnFailure` to keep it for a manual `resume`.
|
|
206
|
-
*/
|
|
207
|
-
async put(input, source, { keepSessionOnFailure = false, ...options } = {}) {
|
|
208
|
-
// One key per put: every resend of its start names the same session.
|
|
209
|
-
const current = await upload.start({
|
|
210
|
-
...input,
|
|
211
|
-
idempotencyKey: input.idempotencyKey ?? globalThis.crypto.randomUUID(),
|
|
212
|
-
size: source instanceof Blob ? source.size : input.size,
|
|
213
|
-
});
|
|
214
|
-
try {
|
|
215
|
-
return await upload.resume(current.id, source, {
|
|
216
|
-
...options,
|
|
217
|
-
size: input.size,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
if (!keepSessionOnFailure)
|
|
222
|
-
await upload.abort(current.id).catch(() => { });
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
return {
|
|
228
|
-
get: () => replayable('GET', root, contracts_js_1.workspaceSchema),
|
|
229
|
-
delete: (expectedRevision) => request('DELETE', `${root}${query({ expectedRevision })}`, contracts_js_1.operationSchema),
|
|
230
|
-
list: (options = {}) => replayable('GET', `${root}/nodes${query({ ...options })}`, (0, contracts_js_1.storagePage)(contracts_js_1.nodeSchema)),
|
|
231
|
-
getNode: (id) => replayable('GET', node(id), contracts_js_1.nodeSchema),
|
|
232
|
-
/**
|
|
233
|
-
* Keyed like upload start, so every resend (storage_busy,
|
|
234
|
-
* permissions_pending, a lost reply) returns the one folder it made.
|
|
235
|
-
* Pass `idempotencyKey` to extend that across your own retries.
|
|
236
|
-
*/
|
|
237
|
-
createFolder: (name, parentId = null, { idempotencyKey = globalThis.crypto.randomUUID(), } = {}) => send({
|
|
238
|
-
method: 'POST',
|
|
239
|
-
path: `${root}/nodes`,
|
|
240
|
-
schema: contracts_js_1.nodeSchema,
|
|
241
|
-
body: contracts_js_1.createFolderSchema.parse({ name, parentId, idempotencyKey }),
|
|
242
|
-
retryable: (error) => isRetryableConflict(error) || isUnacknowledged(error),
|
|
243
|
-
}),
|
|
244
|
-
update: (id, input) => request('PATCH', node(id), contracts_js_1.nodeSchema, input),
|
|
245
|
-
trash: (id, expectedRevision) => request('POST', `${node(id)}/trash`, contracts_js_1.nodeSchema, {
|
|
246
|
-
expectedRevision,
|
|
247
|
-
}),
|
|
248
|
-
restore: (id, expectedRevision) => request('POST', `${node(id)}/restore`, contracts_js_1.nodeSchema, {
|
|
249
|
-
expectedRevision,
|
|
250
|
-
}),
|
|
251
|
-
purge: (id, expectedRevision) => request('POST', `${node(id)}/purge`, contracts_js_1.operationSchema, {
|
|
252
|
-
expectedRevision,
|
|
253
|
-
}),
|
|
254
|
-
download: (id, options = {}) => replayable('POST', `${node(id)}/download`, exports.storageDownloadSchema, options),
|
|
255
|
-
versions: (id, options = {}) => replayable('GET', `${node(id)}/versions${query({ ...options })}`, zod_1.z.object({
|
|
256
|
-
items: zod_1.z.array(contracts_js_1.versionSchema),
|
|
257
|
-
nextRevision: zod_1.z.number().nullable(),
|
|
258
|
-
})),
|
|
259
|
-
restoreVersion: (id, versionId, expectedRevision) => request('POST', `${node(id)}/versions/${contracts_js_1.storageId.parse(versionId)}/restore`, contracts_js_1.versionSchema, { expectedRevision }),
|
|
260
|
-
deleteVersion: (id, versionId, expectedRevision) => request('DELETE', `${node(id)}/versions/${contracts_js_1.storageId.parse(versionId)}${query({ expectedRevision })}`, contracts_js_1.operationSchema),
|
|
261
|
-
access: (id) => replayable('GET', access(id), contracts_js_1.storageAccessSchema),
|
|
262
|
-
setAccess: (input, id) => request('PUT', access(id), contracts_js_1.storageAccessSchema, input),
|
|
263
|
-
transferOwner: (principal, expectedRevision) => request('PUT', `${root}/owner`, contracts_js_1.storageAccessSchema, {
|
|
264
|
-
principal,
|
|
265
|
-
expectedRevision,
|
|
266
|
-
}),
|
|
267
|
-
operations: (options = {}) => replayable('GET', `${root}/operations${query({ ...options })}`, (0, contracts_js_1.storagePage)(contracts_js_1.operationSchema)),
|
|
268
|
-
retryOperation: (id) => request('POST', `${root}/operations/${contracts_js_1.storageId.parse(id)}/retry`, contracts_js_1.operationSchema, {}),
|
|
269
|
-
uploads: upload,
|
|
270
|
-
};
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
function createStorageClient({ baseUrl, getToken, getHeaders, fetch: fetcher = globalThis.fetch, permissionsRetry, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, }) {
|
|
275
|
-
const base = baseUrl.replace(/\/$/, '');
|
|
276
|
-
return storageClient({
|
|
277
|
-
async request(method, path, body) {
|
|
278
|
-
const token = await getToken?.();
|
|
279
|
-
const signal = requestTimeoutMs > 0
|
|
280
|
-
? AbortSignal.timeout(requestTimeoutMs)
|
|
281
|
-
: undefined;
|
|
282
|
-
const timedOut = () => new StorageApiError(0, 'request_timeout', `Storage did not answer within ${requestTimeoutMs}ms; the request may still complete`);
|
|
283
|
-
let response;
|
|
284
|
-
try {
|
|
285
|
-
response = await fetcher(`${base}${path}`, {
|
|
286
|
-
method,
|
|
287
|
-
redirect: 'error',
|
|
288
|
-
signal,
|
|
289
|
-
headers: {
|
|
290
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
291
|
-
...(await getHeaders?.()),
|
|
292
|
-
...(body === undefined
|
|
293
|
-
? {}
|
|
294
|
-
: { 'Content-Type': 'application/json' }),
|
|
295
|
-
},
|
|
296
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
if (signal?.aborted)
|
|
301
|
-
throw timedOut();
|
|
302
|
-
throw error;
|
|
303
|
-
}
|
|
304
|
-
// Proxies and load balancers answer some failures with HTML; check the
|
|
305
|
-
// status before trusting the body to be JSON.
|
|
306
|
-
const result = await readJson(response).catch((error) => {
|
|
307
|
-
if (signal?.aborted)
|
|
308
|
-
throw timedOut();
|
|
309
|
-
throw error;
|
|
310
|
-
});
|
|
311
|
-
if (!response.ok) {
|
|
312
|
-
const parsed = contracts_js_1.storageErrorSchema.safeParse(result.parsed ? result.value : undefined);
|
|
313
|
-
throw new StorageApiError(response.status, parsed.success ? parsed.data.error.code : 'request_failed', parsed.success
|
|
314
|
-
? parsed.data.error.message
|
|
315
|
-
: `Storage request failed (${response.status})`);
|
|
316
|
-
}
|
|
317
|
-
if (!result.parsed)
|
|
318
|
-
throw new StorageApiError(response.status, 'invalid_response', 'Storage returned a response that is not JSON');
|
|
319
|
-
return result.value;
|
|
320
|
-
},
|
|
321
|
-
}, { permissionsRetry });
|
|
322
|
-
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/** Runtime compatibility export; implementation lives in the platform SDK. */
|
|
18
|
+
__exportStar(require("@sequenceholdings/platform/storage"), exports);
|