@ultimat3/storage 27.11.0 → 27.13.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/CLAUDE.md +3 -1
- package/package.json +2 -2
- package/src/driver-local-write.ts +17 -0
- package/src/driver-local.ts +23 -24
package/CLAUDE.md
CHANGED
|
@@ -83,7 +83,9 @@ Gotchas:
|
|
|
83
83
|
in which the sidecar may not describe the bytes: `get`/`stat`/`copy` re-check against the bytes,
|
|
84
84
|
`list` reports no type and `etag: ''`. A fresh key is absent until its bytes land.
|
|
85
85
|
`driver-local-crash.test.ts` dies at every step through `WriteSteps`. Writers and `get()` of one
|
|
86
|
-
key also queue in-process (`keyedQueue`) — unqueued, two puts interleaved 3/3.
|
|
86
|
+
key also queue in-process (`keyedQueue`) — unqueued, two puts interleaved 3/3. `copy()` holds BOTH
|
|
87
|
+
keys' turns (`inTurnOfBoth`, key order): measured in one and read in the other, an overwritten
|
|
88
|
+
source committed new bytes under the old etag.
|
|
87
89
|
- **A refused write is `X_STORAGE_PUT_FAILED` and a refused read `X_STORAGE_READ_FAILED`**, on
|
|
88
90
|
local and s3 (both were bare throws); a 404 mid-read stays `X_STORAGE_NOT_FOUND`; `assertPutOptions` holds `metadata`/`cacheControl`/`contentType` to their types on
|
|
89
91
|
all three drivers before a byte moves.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/storage",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.13.0",
|
|
4
4
|
"description": "Named disks over Bun.file and Bun.s3: safe keys, signed URLs, sniffed uploads",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,6 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "27.
|
|
35
|
+
"@ultimat3/core": "27.13.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -164,6 +164,23 @@ export async function commitObject(
|
|
|
164
164
|
/** Runs `task` after every earlier task queued under the same key has settled. */
|
|
165
165
|
export type KeyedQueue = <T>(key: string, task: () => Promise<T>) => Promise<T>;
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* `task` holding BOTH keys' turns — what a copy needs: the measured source must be the source it
|
|
169
|
+
* then reads, and the destination must be its alone. Taken in key order, so two crossing copies
|
|
170
|
+
* (`a → b`, `b → a`) never each hold the turn the other waits on; one key is taken once, since a
|
|
171
|
+
* queue's second turn on a key it already holds would wait on itself.
|
|
172
|
+
*/
|
|
173
|
+
export const inTurnOfBoth = <T>(
|
|
174
|
+
queue: KeyedQueue,
|
|
175
|
+
one: string,
|
|
176
|
+
other: string,
|
|
177
|
+
task: () => Promise<T>,
|
|
178
|
+
): Promise<T> => {
|
|
179
|
+
if (one === other) return queue(one, task);
|
|
180
|
+
const [first, second] = one < other ? [one, other] : [other, one];
|
|
181
|
+
return queue(first, () => queue(second, task));
|
|
182
|
+
};
|
|
183
|
+
|
|
167
184
|
/**
|
|
168
185
|
* One writer — or one reader — per key at a time, inside this process. An object is two files, so
|
|
169
186
|
* two unserialised `put()`s of one key interleaved their four writes and left one's bytes under the
|
package/src/driver-local.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { etagOfFile, headObject, readObjectBytes, readSidecar } from './driver-l
|
|
|
35
35
|
import { lockOfSidecar, type Sidecar, sidecarLock } from './driver-local-sidecar';
|
|
36
36
|
import {
|
|
37
37
|
commitObject,
|
|
38
|
+
inTurnOfBoth,
|
|
38
39
|
type KeyedQueue,
|
|
39
40
|
keyedQueue,
|
|
40
41
|
pendingPathOf,
|
|
@@ -286,20 +287,22 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
|
|
|
286
287
|
async copy(from: string, to: string): Promise<StorageObject> {
|
|
287
288
|
const source = assertSafeKey(from);
|
|
288
289
|
const destination = assertSafeKey(to);
|
|
289
|
-
// Measured
|
|
290
|
-
// a
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
290
|
+
// Measured AND read in one turn on both keys. Measured in the source's turn and read in the
|
|
291
|
+
// destination's, a put() of the source in between committed its NEW bytes under the OLD
|
|
292
|
+
// etag and type — a pair no marker flags, so every later `get()` of the copy trusted it.
|
|
293
|
+
return inTurnOfBoth(oneAtATime, source, destination, async () => {
|
|
294
|
+
// Measured, not merely read: a sidecar carrying `etag: ''` or a torn source's borrowed
|
|
295
|
+
// type is the same durable lie. The hash is streamed, and only for a pair in doubt.
|
|
296
|
+
const entry = await headObject(root, source, () => etagOfFile(root, source, registered));
|
|
297
|
+
if (entry === undefined) throw objectNotFound(registered, source);
|
|
298
|
+
const lastModified = clock.now();
|
|
299
|
+
const sidecar: Sidecar = {
|
|
300
|
+
contentType: entry.contentType ?? DEFAULT_CONTENT_TYPE,
|
|
301
|
+
etag: entry.etag,
|
|
302
|
+
cacheControl: entry.cacheControl,
|
|
303
|
+
metadata: entry.metadata,
|
|
304
|
+
lastModified: lastModified.toISOString(),
|
|
305
|
+
};
|
|
303
306
|
await refuseOverwrite(destination, 'copy onto');
|
|
304
307
|
await commitObject({
|
|
305
308
|
root,
|
|
@@ -307,18 +310,14 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
|
|
|
307
310
|
disk: registered,
|
|
308
311
|
body: Bun.file(filePath(source)),
|
|
309
312
|
sidecar,
|
|
313
|
+
}).catch(async (error: unknown) => {
|
|
314
|
+
// The queue is this process's: another process can still take the source mid-copy.
|
|
315
|
+
if (!(await Bun.file(filePath(source)).exists()))
|
|
316
|
+
throw objectNotFound(registered, source);
|
|
317
|
+
throw error;
|
|
310
318
|
});
|
|
311
|
-
|
|
312
|
-
// The source was deleted while this copy waited its turn on the destination.
|
|
313
|
-
if (!(await Bun.file(filePath(source)).exists())) throw objectNotFound(registered, source);
|
|
314
|
-
throw error;
|
|
319
|
+
return { ...entry, key: destination, contentType: sidecar.contentType, lastModified };
|
|
315
320
|
});
|
|
316
|
-
return {
|
|
317
|
-
...entry,
|
|
318
|
-
key: destination,
|
|
319
|
-
contentType: sidecar.contentType,
|
|
320
|
-
lastModified,
|
|
321
|
-
};
|
|
322
321
|
},
|
|
323
322
|
|
|
324
323
|
async delete(key: string): Promise<void> {
|