pyric 0.1.0-alpha.21 → 0.1.0-alpha.22
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/storage/persistence.d.ts +81 -17
- package/dist/storage/persistence.d.ts.map +1 -1
- package/dist/storage/persistence.js +267 -50
- package/dist/storage/persistence.js.map +1 -1
- package/dist/storage/service.d.ts +2 -4
- package/dist/storage/service.d.ts.map +1 -1
- package/dist/storage/service.js +63 -21
- package/dist/storage/service.js.map +1 -1
- package/package.json +1 -1
- package/src/storage/persistence.ts +291 -63
- package/src/storage/service.ts +81 -27
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One database (`pyric-storage` by default), two object stores —
|
|
5
5
|
* `blobs` for the file content and `metadata` for the descriptive
|
|
6
|
-
* record. Both stores key on
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* record. Both stores key on composite bucket and path strings
|
|
7
|
+
* (`${bucket}/${fullPath}`) to isolate storage persistence across
|
|
8
|
+
* buckets. Object upload writes both stores within a single `readwrite`
|
|
9
|
+
* transaction so the pair stays consistent — a partial failure can't
|
|
10
|
+
* leave a blob without metadata or vice versa.
|
|
10
11
|
*
|
|
11
12
|
* Slice 3 of the v1 scope (per the design rationale):
|
|
12
13
|
*
|
|
@@ -43,6 +44,30 @@
|
|
|
43
44
|
*/
|
|
44
45
|
const DEFAULT_DB_NAME = 'pyric-storage';
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Default sandbox bucket identifier when not explicitly provided.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_BUCKET = 'pyric-default';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encode a composite key scoping an object path to its bucket: `${bucket}/${fullPath}`.
|
|
54
|
+
*/
|
|
55
|
+
export function toStorageKey(bucket: string, path: string): string {
|
|
56
|
+
return `${bucket}/${path}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decode a composite key into bucket and object path.
|
|
61
|
+
*/
|
|
62
|
+
export function parseStorageKey(key: string): { bucket: string; path: string } | null {
|
|
63
|
+
const slashIdx = key.indexOf('/');
|
|
64
|
+
if (slashIdx === -1) return null;
|
|
65
|
+
return {
|
|
66
|
+
bucket: key.slice(0, slashIdx),
|
|
67
|
+
path: key.slice(slashIdx + 1),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
/**
|
|
47
72
|
* Resolve the default IndexedDB database name for a project identity:
|
|
48
73
|
* `pyric-storage:<projectId>`, or the legacy shared {@link DEFAULT_DB_NAME}
|
|
@@ -54,9 +79,7 @@ export function storageDbName(projectId?: string | null): string {
|
|
|
54
79
|
}
|
|
55
80
|
/**
|
|
56
81
|
* Schema version. Bump and add an `upgradeneeded` branch when the
|
|
57
|
-
* store layout changes.
|
|
58
|
-
* fullPath, no indexes. Future migrations follow IndexedDB's standard
|
|
59
|
-
* upgrade path.
|
|
82
|
+
* store layout changes.
|
|
60
83
|
*/
|
|
61
84
|
const SCHEMA_VERSION = 1;
|
|
62
85
|
const BLOBS_STORE = 'blobs';
|
|
@@ -74,11 +97,11 @@ const METADATA_STORE = 'metadata';
|
|
|
74
97
|
* caller's `SettableMetadata`.
|
|
75
98
|
*/
|
|
76
99
|
export interface StoredMetadata {
|
|
77
|
-
/** Full path of the object within the bucket.
|
|
100
|
+
/** Full path of the object within the bucket. */
|
|
78
101
|
fullPath: string;
|
|
79
102
|
/** Last path segment (`b.txt` in `a/b.txt`). */
|
|
80
103
|
name: string;
|
|
81
|
-
/** Sandbox bucket id.
|
|
104
|
+
/** Sandbox bucket id. */
|
|
82
105
|
bucket: string;
|
|
83
106
|
/** Monotonically increasing version of the object content. */
|
|
84
107
|
generation: string;
|
|
@@ -122,16 +145,16 @@ export interface StorageBackend {
|
|
|
122
145
|
put(path: string, blob: Blob, metadata: StoredMetadata): Promise<void>;
|
|
123
146
|
|
|
124
147
|
/**
|
|
125
|
-
* Read the blob stored at `path`. Resolves to `undefined` when no
|
|
148
|
+
* Read the blob stored at `path` within `bucket`. Resolves to `undefined` when no
|
|
126
149
|
* entry exists — callers translate that into `object-not-found`.
|
|
127
150
|
*/
|
|
128
|
-
getBlob(path: string): Promise<Blob | undefined>;
|
|
151
|
+
getBlob(path: string, bucket?: string): Promise<Blob | undefined>;
|
|
129
152
|
|
|
130
153
|
/**
|
|
131
|
-
* Read the metadata record stored at `path`. Resolves to
|
|
154
|
+
* Read the metadata record stored at `path` within `bucket`. Resolves to
|
|
132
155
|
* `undefined` when no entry exists.
|
|
133
156
|
*/
|
|
134
|
-
getMetadata(path: string): Promise<StoredMetadata | undefined>;
|
|
157
|
+
getMetadata(path: string, bucket?: string): Promise<StoredMetadata | undefined>;
|
|
135
158
|
|
|
136
159
|
/**
|
|
137
160
|
* Replace the metadata record at `path` without touching the blob.
|
|
@@ -139,31 +162,37 @@ export interface StorageBackend {
|
|
|
139
162
|
* preserving server-set fields (`generation`, `timeCreated`, …)
|
|
140
163
|
* the spec says metadata-only updates leave intact.
|
|
141
164
|
*/
|
|
142
|
-
putMetadata(path: string, metadata: StoredMetadata): Promise<void>;
|
|
165
|
+
putMetadata(path: string, metadata: StoredMetadata, bucket?: string): Promise<void>;
|
|
143
166
|
|
|
144
167
|
/**
|
|
145
168
|
* Delete both the blob and the metadata at `path` atomically.
|
|
146
169
|
* No-op when neither entry exists.
|
|
147
170
|
*/
|
|
148
|
-
delete(path: string): Promise<void>;
|
|
171
|
+
delete(path: string, bucket?: string): Promise<void>;
|
|
149
172
|
|
|
150
173
|
/**
|
|
151
174
|
* Enumerate metadata records whose `fullPath` starts with `prefix`.
|
|
152
|
-
* Returned in
|
|
175
|
+
* Returned in key order (lexicographic by path within bucket). The
|
|
153
176
|
* prefix is used verbatim — no implicit trailing-slash addition,
|
|
154
177
|
* matching the survey's path-semantics call (Section 6).
|
|
155
178
|
*/
|
|
156
|
-
listByPrefix(prefix: string): Promise<StoredMetadata[]>;
|
|
179
|
+
listByPrefix(prefix: string, bucket?: string): Promise<StoredMetadata[]>;
|
|
157
180
|
|
|
158
181
|
/**
|
|
159
|
-
* Clear
|
|
182
|
+
* Clear stores. If `bucket` is supplied, clears only entries in that bucket;
|
|
183
|
+
* otherwise clears all stores.
|
|
160
184
|
*/
|
|
161
|
-
reset(): Promise<void>;
|
|
185
|
+
reset(bucket?: string): Promise<void>;
|
|
162
186
|
|
|
163
187
|
/**
|
|
164
188
|
* Close the underlying IndexedDB connection. Idempotent.
|
|
165
189
|
*/
|
|
166
190
|
close(): void;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Return a view of this backend scoped to a specific bucket.
|
|
194
|
+
*/
|
|
195
|
+
scoped?(bucket: string): StorageBackend;
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
/**
|
|
@@ -178,46 +207,97 @@ export interface StorageBackend {
|
|
|
178
207
|
*/
|
|
179
208
|
export async function openStorageBackend(
|
|
180
209
|
dbName: string = DEFAULT_DB_NAME,
|
|
210
|
+
defaultBucket?: string,
|
|
181
211
|
): Promise<StorageBackend> {
|
|
182
212
|
try {
|
|
183
213
|
const db = await openDatabase(dbName);
|
|
184
|
-
return new IndexedDbStorageBackend(db);
|
|
214
|
+
return new IndexedDbStorageBackend(db, defaultBucket);
|
|
185
215
|
} catch (err) {
|
|
186
|
-
return new InMemoryStorageBackend();
|
|
216
|
+
return new InMemoryStorageBackend(defaultBucket);
|
|
187
217
|
}
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
class InMemoryStorageBackend implements StorageBackend {
|
|
220
|
+
export class InMemoryStorageBackend implements StorageBackend {
|
|
191
221
|
private readonly blobs = new Map<string, Blob>();
|
|
192
222
|
private readonly metadata = new Map<string, StoredMetadata>();
|
|
223
|
+
private _defaultBucket: string | undefined;
|
|
224
|
+
|
|
225
|
+
constructor(defaultBucket?: string) {
|
|
226
|
+
this._defaultBucket = defaultBucket;
|
|
227
|
+
}
|
|
193
228
|
|
|
194
229
|
async put(path: string, blob: Blob, metadata: StoredMetadata): Promise<void> {
|
|
195
|
-
this.
|
|
196
|
-
this.
|
|
230
|
+
const bucket = metadata.bucket || this._defaultBucket || DEFAULT_BUCKET;
|
|
231
|
+
if (!this._defaultBucket) {
|
|
232
|
+
this._defaultBucket = bucket;
|
|
233
|
+
}
|
|
234
|
+
if (!metadata.bucket) {
|
|
235
|
+
metadata.bucket = bucket;
|
|
236
|
+
}
|
|
237
|
+
const key = toStorageKey(bucket, path);
|
|
238
|
+
this.blobs.set(key, blob);
|
|
239
|
+
this.metadata.set(key, metadata);
|
|
197
240
|
}
|
|
198
241
|
|
|
199
|
-
async getBlob(path: string): Promise<Blob | undefined> {
|
|
200
|
-
|
|
242
|
+
async getBlob(path: string, bucket?: string): Promise<Blob | undefined> {
|
|
243
|
+
if (bucket) {
|
|
244
|
+
return this.blobs.get(toStorageKey(bucket, path));
|
|
245
|
+
}
|
|
246
|
+
const targetBucket = this._defaultBucket ?? DEFAULT_BUCKET;
|
|
247
|
+
const direct = this.blobs.get(toStorageKey(targetBucket, path));
|
|
248
|
+
if (direct !== undefined) return direct;
|
|
249
|
+
if (targetBucket !== DEFAULT_BUCKET) {
|
|
250
|
+
const def = this.blobs.get(toStorageKey(DEFAULT_BUCKET, path));
|
|
251
|
+
if (def !== undefined) return def;
|
|
252
|
+
}
|
|
253
|
+
for (const [key, blob] of this.blobs.entries()) {
|
|
254
|
+
const parsed = parseStorageKey(key);
|
|
255
|
+
if (parsed && parsed.path === path) {
|
|
256
|
+
return blob;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
201
260
|
}
|
|
202
261
|
|
|
203
|
-
async getMetadata(path: string): Promise<StoredMetadata | undefined> {
|
|
204
|
-
|
|
262
|
+
async getMetadata(path: string, bucket?: string): Promise<StoredMetadata | undefined> {
|
|
263
|
+
if (bucket) {
|
|
264
|
+
return this.metadata.get(toStorageKey(bucket, path));
|
|
265
|
+
}
|
|
266
|
+
const targetBucket = this._defaultBucket ?? DEFAULT_BUCKET;
|
|
267
|
+
const direct = this.metadata.get(toStorageKey(targetBucket, path));
|
|
268
|
+
if (direct !== undefined) return direct;
|
|
269
|
+
if (targetBucket !== DEFAULT_BUCKET) {
|
|
270
|
+
const def = this.metadata.get(toStorageKey(DEFAULT_BUCKET, path));
|
|
271
|
+
if (def !== undefined) return def;
|
|
272
|
+
}
|
|
273
|
+
for (const [key, meta] of this.metadata.entries()) {
|
|
274
|
+
const parsed = parseStorageKey(key);
|
|
275
|
+
if (parsed && parsed.path === path) {
|
|
276
|
+
return meta;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
205
280
|
}
|
|
206
281
|
|
|
207
|
-
async putMetadata(path: string, metadata: StoredMetadata): Promise<void> {
|
|
208
|
-
|
|
282
|
+
async putMetadata(path: string, metadata: StoredMetadata, bucket?: string): Promise<void> {
|
|
283
|
+
const b = bucket ?? metadata.bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
284
|
+
if (!metadata.bucket) metadata.bucket = b;
|
|
285
|
+
this.metadata.set(toStorageKey(b, path), metadata);
|
|
209
286
|
}
|
|
210
287
|
|
|
211
|
-
async delete(path: string): Promise<void> {
|
|
212
|
-
this.
|
|
213
|
-
this.
|
|
288
|
+
async delete(path: string, bucket?: string): Promise<void> {
|
|
289
|
+
const targetBucket = bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
290
|
+
this.blobs.delete(toStorageKey(targetBucket, path));
|
|
291
|
+
this.metadata.delete(toStorageKey(targetBucket, path));
|
|
214
292
|
}
|
|
215
293
|
|
|
216
|
-
async listByPrefix(prefix: string): Promise<StoredMetadata[]> {
|
|
294
|
+
async listByPrefix(prefix: string, bucket?: string): Promise<StoredMetadata[]> {
|
|
295
|
+
const targetBucket = bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
296
|
+
const keyPrefix = `${targetBucket}/${prefix}`;
|
|
217
297
|
const results: StoredMetadata[] = [];
|
|
218
298
|
const keys = Array.from(this.metadata.keys()).sort();
|
|
219
299
|
for (const key of keys) {
|
|
220
|
-
if (key.startsWith(
|
|
300
|
+
if (key.startsWith(keyPrefix)) {
|
|
221
301
|
const meta = this.metadata.get(key);
|
|
222
302
|
if (meta) results.push(meta);
|
|
223
303
|
}
|
|
@@ -225,74 +305,222 @@ class InMemoryStorageBackend implements StorageBackend {
|
|
|
225
305
|
return results;
|
|
226
306
|
}
|
|
227
307
|
|
|
228
|
-
async reset(): Promise<void> {
|
|
229
|
-
|
|
230
|
-
|
|
308
|
+
async reset(bucket?: string): Promise<void> {
|
|
309
|
+
if (!bucket) {
|
|
310
|
+
this.blobs.clear();
|
|
311
|
+
this.metadata.clear();
|
|
312
|
+
this._defaultBucket = undefined;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const bucketPrefix = `${bucket}/`;
|
|
316
|
+
for (const key of Array.from(this.blobs.keys())) {
|
|
317
|
+
if (key.startsWith(bucketPrefix)) {
|
|
318
|
+
this.blobs.delete(key);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const key of Array.from(this.metadata.keys())) {
|
|
322
|
+
if (key.startsWith(bucketPrefix)) {
|
|
323
|
+
this.metadata.delete(key);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
231
326
|
}
|
|
232
327
|
|
|
233
328
|
close(): void {}
|
|
329
|
+
|
|
330
|
+
scoped(bucket: string): StorageBackend {
|
|
331
|
+
return new ScopedStorageBackend(this, bucket);
|
|
332
|
+
}
|
|
234
333
|
}
|
|
235
334
|
|
|
236
335
|
// ─── Internal ──────────────────────────────────────────────────────
|
|
237
336
|
|
|
238
|
-
class IndexedDbStorageBackend implements StorageBackend {
|
|
239
|
-
|
|
337
|
+
export class IndexedDbStorageBackend implements StorageBackend {
|
|
338
|
+
private _defaultBucket: string | undefined;
|
|
339
|
+
|
|
340
|
+
constructor(
|
|
341
|
+
private readonly db: IDBDatabase,
|
|
342
|
+
defaultBucket?: string,
|
|
343
|
+
) {
|
|
344
|
+
this._defaultBucket = defaultBucket;
|
|
345
|
+
}
|
|
240
346
|
|
|
241
347
|
async put(path: string, blob: Blob, metadata: StoredMetadata): Promise<void> {
|
|
348
|
+
const bucket = metadata.bucket || this._defaultBucket || DEFAULT_BUCKET;
|
|
349
|
+
if (!this._defaultBucket) {
|
|
350
|
+
this._defaultBucket = bucket;
|
|
351
|
+
}
|
|
352
|
+
if (!metadata.bucket) {
|
|
353
|
+
metadata.bucket = bucket;
|
|
354
|
+
}
|
|
355
|
+
const key = toStorageKey(bucket, path);
|
|
242
356
|
const tx = this.db.transaction([BLOBS_STORE, METADATA_STORE], 'readwrite');
|
|
243
|
-
tx.objectStore(BLOBS_STORE).put(blob,
|
|
244
|
-
tx.objectStore(METADATA_STORE).put(metadata,
|
|
357
|
+
tx.objectStore(BLOBS_STORE).put(blob, key);
|
|
358
|
+
tx.objectStore(METADATA_STORE).put(metadata, key);
|
|
245
359
|
await awaitTransaction(tx);
|
|
246
360
|
}
|
|
247
361
|
|
|
248
|
-
async getBlob(path: string): Promise<Blob | undefined> {
|
|
362
|
+
async getBlob(path: string, bucket?: string): Promise<Blob | undefined> {
|
|
249
363
|
const tx = this.db.transaction(BLOBS_STORE, 'readonly');
|
|
250
|
-
|
|
364
|
+
const store = tx.objectStore(BLOBS_STORE);
|
|
365
|
+
if (bucket) {
|
|
366
|
+
return awaitRequest<Blob | undefined>(store.get(toStorageKey(bucket, path)));
|
|
367
|
+
}
|
|
368
|
+
const targetBucket = this._defaultBucket ?? DEFAULT_BUCKET;
|
|
369
|
+
const direct = await awaitRequest<Blob | undefined>(store.get(toStorageKey(targetBucket, path)));
|
|
370
|
+
if (direct !== undefined) return direct;
|
|
371
|
+
if (targetBucket !== DEFAULT_BUCKET) {
|
|
372
|
+
const def = await awaitRequest<Blob | undefined>(store.get(toStorageKey(DEFAULT_BUCKET, path)));
|
|
373
|
+
if (def !== undefined) return def;
|
|
374
|
+
}
|
|
375
|
+
return new Promise((resolve, reject) => {
|
|
376
|
+
const req = store.openCursor();
|
|
377
|
+
req.addEventListener('success', () => {
|
|
378
|
+
const cursor = req.result;
|
|
379
|
+
if (!cursor) {
|
|
380
|
+
resolve(undefined);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const key = String(cursor.key);
|
|
384
|
+
const parsed = parseStorageKey(key);
|
|
385
|
+
if (parsed && parsed.path === path) {
|
|
386
|
+
resolve(cursor.value as Blob);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
cursor.continue();
|
|
390
|
+
});
|
|
391
|
+
req.addEventListener('error', () => reject(req.error));
|
|
392
|
+
});
|
|
251
393
|
}
|
|
252
394
|
|
|
253
|
-
async getMetadata(path: string): Promise<StoredMetadata | undefined> {
|
|
395
|
+
async getMetadata(path: string, bucket?: string): Promise<StoredMetadata | undefined> {
|
|
254
396
|
const tx = this.db.transaction(METADATA_STORE, 'readonly');
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
397
|
+
const store = tx.objectStore(METADATA_STORE);
|
|
398
|
+
if (bucket) {
|
|
399
|
+
return awaitRequest<StoredMetadata | undefined>(store.get(toStorageKey(bucket, path)));
|
|
400
|
+
}
|
|
401
|
+
const targetBucket = this._defaultBucket ?? DEFAULT_BUCKET;
|
|
402
|
+
const direct = await awaitRequest<StoredMetadata | undefined>(store.get(toStorageKey(targetBucket, path)));
|
|
403
|
+
if (direct !== undefined) return direct;
|
|
404
|
+
if (targetBucket !== DEFAULT_BUCKET) {
|
|
405
|
+
const def = await awaitRequest<StoredMetadata | undefined>(store.get(toStorageKey(DEFAULT_BUCKET, path)));
|
|
406
|
+
if (def !== undefined) return def;
|
|
407
|
+
}
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
const req = store.openCursor();
|
|
410
|
+
req.addEventListener('success', () => {
|
|
411
|
+
const cursor = req.result;
|
|
412
|
+
if (!cursor) {
|
|
413
|
+
resolve(undefined);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const key = String(cursor.key);
|
|
417
|
+
const parsed = parseStorageKey(key);
|
|
418
|
+
if (parsed && parsed.path === path) {
|
|
419
|
+
resolve(cursor.value as StoredMetadata);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
cursor.continue();
|
|
423
|
+
});
|
|
424
|
+
req.addEventListener('error', () => reject(req.error));
|
|
425
|
+
});
|
|
258
426
|
}
|
|
259
427
|
|
|
260
|
-
async putMetadata(path: string, metadata: StoredMetadata): Promise<void> {
|
|
428
|
+
async putMetadata(path: string, metadata: StoredMetadata, bucket?: string): Promise<void> {
|
|
429
|
+
const b = bucket ?? metadata.bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
430
|
+
if (!metadata.bucket) metadata.bucket = b;
|
|
431
|
+
const key = toStorageKey(b, path);
|
|
261
432
|
const tx = this.db.transaction(METADATA_STORE, 'readwrite');
|
|
262
|
-
tx.objectStore(METADATA_STORE).put(metadata,
|
|
433
|
+
tx.objectStore(METADATA_STORE).put(metadata, key);
|
|
263
434
|
await awaitTransaction(tx);
|
|
264
435
|
}
|
|
265
436
|
|
|
266
|
-
async delete(path: string): Promise<void> {
|
|
437
|
+
async delete(path: string, bucket?: string): Promise<void> {
|
|
438
|
+
const targetBucket = bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
439
|
+
const key = toStorageKey(targetBucket, path);
|
|
267
440
|
const tx = this.db.transaction([BLOBS_STORE, METADATA_STORE], 'readwrite');
|
|
268
|
-
tx.objectStore(BLOBS_STORE).delete(
|
|
269
|
-
tx.objectStore(METADATA_STORE).delete(
|
|
441
|
+
tx.objectStore(BLOBS_STORE).delete(key);
|
|
442
|
+
tx.objectStore(METADATA_STORE).delete(key);
|
|
270
443
|
await awaitTransaction(tx);
|
|
271
444
|
}
|
|
272
445
|
|
|
273
|
-
async listByPrefix(prefix: string): Promise<StoredMetadata[]> {
|
|
446
|
+
async listByPrefix(prefix: string, bucket?: string): Promise<StoredMetadata[]> {
|
|
447
|
+
const targetBucket = bucket ?? this._defaultBucket ?? DEFAULT_BUCKET;
|
|
274
448
|
const tx = this.db.transaction(METADATA_STORE, 'readonly');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// code point, so any path whose first char beyond `prefix` is
|
|
278
|
-
// legal will sort below it. Matches the survey's recommended
|
|
279
|
-
// prefix-query pattern (Section 6).
|
|
280
|
-
const range = IDBKeyRange.bound(prefix, prefix + '', false, false);
|
|
449
|
+
const keyPrefix = `${targetBucket}/${prefix}`;
|
|
450
|
+
const range = IDBKeyRange.bound(keyPrefix, keyPrefix + '\ufffd', false, false);
|
|
281
451
|
return awaitRequest<StoredMetadata[]>(
|
|
282
452
|
tx.objectStore(METADATA_STORE).getAll(range),
|
|
283
453
|
);
|
|
284
454
|
}
|
|
285
455
|
|
|
286
|
-
async reset(): Promise<void> {
|
|
456
|
+
async reset(bucket?: string): Promise<void> {
|
|
287
457
|
const tx = this.db.transaction([BLOBS_STORE, METADATA_STORE], 'readwrite');
|
|
288
|
-
|
|
289
|
-
|
|
458
|
+
if (!bucket) {
|
|
459
|
+
tx.objectStore(BLOBS_STORE).clear();
|
|
460
|
+
tx.objectStore(METADATA_STORE).clear();
|
|
461
|
+
this._defaultBucket = undefined;
|
|
462
|
+
} else {
|
|
463
|
+
const bucketPrefix = `${bucket}/`;
|
|
464
|
+
const range = IDBKeyRange.bound(bucketPrefix, bucketPrefix + '\ufffd', false, false);
|
|
465
|
+
tx.objectStore(BLOBS_STORE).delete(range);
|
|
466
|
+
tx.objectStore(METADATA_STORE).delete(range);
|
|
467
|
+
}
|
|
290
468
|
await awaitTransaction(tx);
|
|
291
469
|
}
|
|
292
470
|
|
|
293
471
|
close(): void {
|
|
294
472
|
this.db.close();
|
|
295
473
|
}
|
|
474
|
+
|
|
475
|
+
scoped(bucket: string): StorageBackend {
|
|
476
|
+
return new ScopedStorageBackend(this, bucket);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export class ScopedStorageBackend implements StorageBackend {
|
|
481
|
+
constructor(
|
|
482
|
+
private readonly underlying: StorageBackend,
|
|
483
|
+
readonly bucket: string,
|
|
484
|
+
) {}
|
|
485
|
+
|
|
486
|
+
put(path: string, blob: Blob, metadata: StoredMetadata): Promise<void> {
|
|
487
|
+
const meta = metadata.bucket ? metadata : { ...metadata, bucket: this.bucket };
|
|
488
|
+
return this.underlying.put(path, blob, meta);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
getBlob(path: string, bucket?: string): Promise<Blob | undefined> {
|
|
492
|
+
return this.underlying.getBlob(path, bucket ?? this.bucket);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getMetadata(path: string, bucket?: string): Promise<StoredMetadata | undefined> {
|
|
496
|
+
return this.underlying.getMetadata(path, bucket ?? this.bucket);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
putMetadata(path: string, metadata: StoredMetadata, bucket?: string): Promise<void> {
|
|
500
|
+
const meta = metadata.bucket ? metadata : { ...metadata, bucket: this.bucket };
|
|
501
|
+
return this.underlying.putMetadata(path, meta, bucket ?? this.bucket);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
delete(path: string, bucket?: string): Promise<void> {
|
|
505
|
+
return this.underlying.delete(path, bucket ?? this.bucket);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
listByPrefix(prefix: string, bucket?: string): Promise<StoredMetadata[]> {
|
|
509
|
+
return this.underlying.listByPrefix(prefix, bucket ?? this.bucket);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
reset(bucket?: string): Promise<void> {
|
|
513
|
+
return this.underlying.reset(bucket ?? this.bucket);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
close(): void {
|
|
517
|
+
this.underlying.close();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
scoped(bucket: string): StorageBackend {
|
|
521
|
+
if (bucket === this.bucket) return this;
|
|
522
|
+
return this.underlying.scoped ? this.underlying.scoped(bucket) : new ScopedStorageBackend(this.underlying, bucket);
|
|
523
|
+
}
|
|
296
524
|
}
|
|
297
525
|
|
|
298
526
|
function openDatabase(dbName: string): Promise<IDBDatabase> {
|