@gravito/nebula 4.0.0 → 4.1.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/dist/index.cjs CHANGED
@@ -38,13 +38,32 @@ var StorageRepository = class {
38
38
  this.store = store;
39
39
  this.hooks = hooks;
40
40
  }
41
- async put(key, data) {
42
- const finalData = this.hooks ? await this.hooks.applyFilter("storage:upload", data, { key }) : data;
43
- await this.store.put(key, finalData);
41
+ /**
42
+ * Stores content and triggers upload hooks.
43
+ *
44
+ * Applies the `storage:upload` filter to the data before storage and
45
+ * fires the `storage:uploaded` action upon success.
46
+ *
47
+ * @param key - The destination path
48
+ * @param data - The content to store
49
+ * @param options - Optional upload options (content-type, metadata, cache-control, etc.)
50
+ * @throws {Error} If the underlying store fails to persist the data
51
+ */
52
+ async put(key, data, options) {
53
+ const finalData = this.hooks ? await this.hooks.applyFilter("storage:upload", data, { key, options }) : data;
54
+ await this.store.put(key, finalData, options);
44
55
  if (this.hooks) {
45
- await this.hooks.doAction("storage:uploaded", { key });
56
+ await this.hooks.doAction("storage:uploaded", { key, options });
46
57
  }
47
58
  }
59
+ /**
60
+ * Retrieves content and triggers hit/miss hooks.
61
+ *
62
+ * Fires `storage:hit` if the file exists, or `storage:miss` otherwise.
63
+ *
64
+ * @param key - The path of the file to retrieve
65
+ * @returns The file content as a Blob, or null if not found
66
+ */
48
67
  async get(key) {
49
68
  const data = await this.store.get(key);
50
69
  if (this.hooks) {
@@ -56,6 +75,14 @@ var StorageRepository = class {
56
75
  }
57
76
  return data;
58
77
  }
78
+ /**
79
+ * Deletes a file and triggers the deleted hook.
80
+ *
81
+ * Fires `storage:deleted` only if the file was actually removed.
82
+ *
83
+ * @param key - The path of the file to delete
84
+ * @returns True if deleted, false if file didn't exist
85
+ */
59
86
  async delete(key) {
60
87
  const deleted = await this.store.delete(key);
61
88
  if (deleted && this.hooks) {
@@ -63,39 +90,168 @@ var StorageRepository = class {
63
90
  }
64
91
  return deleted;
65
92
  }
93
+ /**
94
+ * Checks for file existence.
95
+ *
96
+ * @param key - The path to check
97
+ * @returns True if exists, false otherwise
98
+ */
66
99
  async exists(key) {
67
100
  return this.store.exists(key);
68
101
  }
102
+ /**
103
+ * Copies a file and triggers the copied hook.
104
+ *
105
+ * Fires `storage:copied` upon successful completion.
106
+ *
107
+ * @param from - Source path
108
+ * @param to - Destination path
109
+ * @throws {Error} If source missing or operation fails
110
+ */
69
111
  async copy(from, to) {
70
112
  await this.store.copy(from, to);
71
113
  if (this.hooks) {
72
114
  await this.hooks.doAction("storage:copied", { from, to });
73
115
  }
74
116
  }
117
+ /**
118
+ * Moves a file and triggers the moved hook.
119
+ *
120
+ * Fires `storage:moved` upon successful completion.
121
+ *
122
+ * @param from - Current path
123
+ * @param to - New path
124
+ * @throws {Error} If source missing or operation fails
125
+ */
75
126
  async move(from, to) {
76
127
  await this.store.move(from, to);
77
128
  if (this.hooks) {
78
129
  await this.hooks.doAction("storage:moved", { from, to });
79
130
  }
80
131
  }
132
+ /**
133
+ * Lists files using an async generator.
134
+ *
135
+ * @param prefix - Path prefix to filter by
136
+ * @returns Async iterable of storage items
137
+ * @throws {Error} If the underlying driver does not support listing
138
+ */
81
139
  async *list(prefix) {
82
140
  if (!this.store.list) {
83
141
  throw new Error("[StorageRepository] This storage driver does not support listing files.");
84
142
  }
85
143
  yield* this.store.list(prefix);
86
144
  }
145
+ /**
146
+ * Lists files with pagination support.
147
+ *
148
+ * 分頁列舉檔案
149
+ * Recommended for large storage backends to prevent OOM.
150
+ * Provides cursor-based pagination for efficient enumeration.
151
+ *
152
+ * @param prefix - Path prefix to filter by
153
+ * @param options - Pagination and filtering options
154
+ * @returns Paginated list result with cursor for next page
155
+ * @throws {Error} If the underlying driver does not support paginated listing
156
+ */
157
+ async listPaginated(prefix, options) {
158
+ if (!this.store.listPaginated) {
159
+ throw new Error("[StorageRepository] This storage driver does not support paginated listing.");
160
+ }
161
+ return this.store.listPaginated(prefix, options);
162
+ }
163
+ /**
164
+ * Retrieves file metadata.
165
+ *
166
+ * @param key - Path of the file
167
+ * @returns Metadata object or null if not found
168
+ */
87
169
  async getMetadata(key) {
88
170
  return this.store.getMetadata(key);
89
171
  }
172
+ /**
173
+ * Updates custom metadata for a file.
174
+ *
175
+ * 更新自定義 Metadata
176
+ *
177
+ * @param key - Path of the file
178
+ * @param metadata - Custom metadata to set
179
+ * @throws {Error} If the underlying driver does not support metadata updates
180
+ */
181
+ async setMetadata(key, metadata) {
182
+ if (!this.store.setMetadata) {
183
+ throw new Error("[StorageRepository] This storage driver does not support metadata updates.");
184
+ }
185
+ await this.store.setMetadata(key, metadata);
186
+ }
187
+ /**
188
+ * Generates a public URL.
189
+ *
190
+ * @param key - Path of the file
191
+ * @returns URL string
192
+ */
90
193
  getUrl(key) {
91
194
  return this.store.getUrl(key);
92
195
  }
196
+ /**
197
+ * Generates a temporary signed URL.
198
+ *
199
+ * @param key - Path of the file
200
+ * @param expiresIn - Expiration in seconds
201
+ * @returns Signed URL string
202
+ * @throws {Error} If the underlying driver does not support signed URLs
203
+ */
93
204
  async getSignedUrl(key, expiresIn) {
94
205
  if (!this.store.getSignedUrl) {
95
206
  throw new Error("[StorageRepository] This storage driver does not support signed URLs.");
96
207
  }
97
208
  return this.store.getSignedUrl(key, expiresIn);
98
209
  }
210
+ /**
211
+ * Stores content from a readable stream and triggers upload hooks.
212
+ *
213
+ * 串流上傳並觸發 hooks
214
+ * Useful for handling large files without loading entire content into memory.
215
+ * Applies the `storage:upload` filter and fires the `storage:uploaded` action.
216
+ *
217
+ * @param key - The destination path
218
+ * @param stream - Readable stream containing the data
219
+ * @throws {Error} If the underlying driver does not support stream writing
220
+ */
221
+ async putStream(key, stream) {
222
+ if (!this.store.putStream) {
223
+ throw new Error("[StorageRepository] This storage driver does not support stream writing.");
224
+ }
225
+ await this.store.putStream(key, stream);
226
+ if (this.hooks) {
227
+ await this.hooks.doAction("storage:uploaded", { key });
228
+ }
229
+ }
230
+ /**
231
+ * Retrieves content as a readable stream and triggers hit/miss hooks.
232
+ *
233
+ * 串流讀取並觸發 hooks
234
+ * Useful for handling large files without loading entire content into memory.
235
+ * Fires `storage:hit` if the file exists, or `storage:miss` otherwise.
236
+ *
237
+ * @param key - Path of the file
238
+ * @returns Readable stream of file content, or null if not found
239
+ * @throws {Error} If the underlying driver does not support stream reading
240
+ */
241
+ async getStream(key) {
242
+ if (!this.store.getStream) {
243
+ throw new Error("[StorageRepository] This storage driver does not support stream reading.");
244
+ }
245
+ const stream = await this.store.getStream(key);
246
+ if (this.hooks) {
247
+ if (stream) {
248
+ await this.hooks.doAction("storage:hit", { key });
249
+ } else {
250
+ await this.hooks.doAction("storage:miss", { key });
251
+ }
252
+ }
253
+ return stream;
254
+ }
99
255
  };
100
256
 
101
257
  // src/StorageManager.ts
@@ -107,6 +263,16 @@ var StorageManager = class {
107
263
  }
108
264
  stores = /* @__PURE__ */ new Map();
109
265
  repositories = /* @__PURE__ */ new Map();
266
+ /**
267
+ * Accesses a specific storage disk by name.
268
+ *
269
+ * If no name is provided, the default disk configured during initialization is returned.
270
+ * Repositories are lazily initialized and cached for subsequent access.
271
+ *
272
+ * @param name - The unique identifier of the disk to access
273
+ * @returns A repository instance for the requested disk
274
+ * @throws {Error} If the requested disk is not configured or factory fails
275
+ */
110
276
  disk(name) {
111
277
  const diskName = name ?? this.options.default;
112
278
  if (!this.repositories.has(diskName)) {
@@ -121,36 +287,156 @@ var StorageManager = class {
121
287
  }
122
288
  return this.stores.get(name);
123
289
  }
124
- put(key, data) {
125
- return this.disk().put(key, data);
290
+ /**
291
+ * Stores content at the specified key on the default disk.
292
+ *
293
+ * @param key - The destination path/identifier
294
+ * @param data - The content to store
295
+ * @param options - Optional upload options (content-type, metadata, cache-control, etc.)
296
+ * @returns A promise that resolves when the operation completes
297
+ * @throws {Error} If the storage operation fails on the default disk
298
+ */
299
+ put(key, data, options) {
300
+ return this.disk().put(key, data, options);
126
301
  }
302
+ /**
303
+ * Retrieves content from the specified key on the default disk.
304
+ *
305
+ * @param key - The path/identifier of the file to retrieve
306
+ * @returns The file content as a Blob, or null if not found
307
+ */
127
308
  get(key) {
128
309
  return this.disk().get(key);
129
310
  }
311
+ /**
312
+ * Removes a file from the default disk.
313
+ *
314
+ * @param key - The path/identifier of the file to delete
315
+ * @returns True if the file was deleted, false if it didn't exist
316
+ */
130
317
  delete(key) {
131
318
  return this.disk().delete(key);
132
319
  }
320
+ /**
321
+ * Checks if a file exists on the default disk.
322
+ *
323
+ * @param key - The path/identifier to check
324
+ * @returns True if the file exists, false otherwise
325
+ */
133
326
  exists(key) {
134
327
  return this.disk().exists(key);
135
328
  }
329
+ /**
330
+ * Creates a copy of a file on the default disk.
331
+ *
332
+ * @param from - The source path
333
+ * @param to - The destination path
334
+ * @throws {Error} If the source file does not exist or copy fails
335
+ */
136
336
  copy(from, to) {
137
337
  return this.disk().copy(from, to);
138
338
  }
339
+ /**
340
+ * Moves or renames a file on the default disk.
341
+ *
342
+ * @param from - The current path
343
+ * @param to - The new path
344
+ * @throws {Error} If the source file does not exist or move fails
345
+ */
139
346
  move(from, to) {
140
347
  return this.disk().move(from, to);
141
348
  }
349
+ /**
350
+ * Lists files and directories under a given prefix on the default disk.
351
+ *
352
+ * @param prefix - The directory or path prefix to list
353
+ * @returns An async iterable of storage items
354
+ * @throws {Error} If the default disk driver does not support listing
355
+ */
142
356
  list(prefix) {
143
357
  return this.disk().list(prefix);
144
358
  }
359
+ /**
360
+ * Retrieves metadata for a specific file on the default disk.
361
+ *
362
+ * @param key - The path/identifier of the file
363
+ * @returns Metadata object or null if file not found
364
+ */
145
365
  getMetadata(key) {
146
366
  return this.disk().getMetadata(key);
147
367
  }
368
+ /**
369
+ * Generates a public URL for a file on the default disk.
370
+ *
371
+ * @param key - The path/identifier of the file
372
+ * @returns The absolute or relative URL string
373
+ */
148
374
  getUrl(key) {
149
375
  return this.disk().getUrl(key);
150
376
  }
377
+ /**
378
+ * Generates a temporary, signed URL for a file on the default disk.
379
+ *
380
+ * @param key - The path/identifier of the file
381
+ * @param expiresIn - Expiration time in seconds
382
+ * @returns A promise resolving to the signed URL string
383
+ * @throws {Error} If the default disk driver does not support signed URLs
384
+ */
151
385
  getSignedUrl(key, expiresIn) {
152
386
  return this.disk().getSignedUrl(key, expiresIn);
153
387
  }
388
+ /**
389
+ * Stores content from a readable stream on the default disk.
390
+ *
391
+ * 串流上傳到預設磁碟
392
+ * Useful for handling large files without loading entire content into memory.
393
+ *
394
+ * @param key - The destination path
395
+ * @param stream - Readable stream containing the data
396
+ * @throws {Error} If the default disk driver does not support stream writing
397
+ */
398
+ putStream(key, stream) {
399
+ return this.disk().putStream(key, stream);
400
+ }
401
+ /**
402
+ * Retrieves content as a readable stream from the default disk.
403
+ *
404
+ * 從預設磁碟串流讀取
405
+ * Useful for handling large files without loading entire content into memory.
406
+ *
407
+ * @param key - The path/identifier of the file
408
+ * @returns Readable stream of file content, or null if not found
409
+ * @throws {Error} If the default disk driver does not support stream reading
410
+ */
411
+ getStream(key) {
412
+ return this.disk().getStream(key);
413
+ }
414
+ /**
415
+ * Lists files with pagination support on the default disk.
416
+ *
417
+ * 在預設磁碟上分頁列舉檔案
418
+ * Recommended for large storage backends to prevent OOM.
419
+ *
420
+ * @param prefix - Path prefix to filter by
421
+ * @param options - Pagination and filtering options
422
+ * @returns Paginated list result with cursor for next page
423
+ * @throws {Error} If the default disk driver does not support paginated listing
424
+ */
425
+ listPaginated(prefix, options) {
426
+ return this.disk().listPaginated(prefix, options);
427
+ }
428
+ /**
429
+ * Updates custom metadata for a file on the default disk.
430
+ *
431
+ * 更新預設磁碟上檔案的自定義 Metadata
432
+ *
433
+ * @param key - The path/identifier of the file
434
+ * @param metadata - Custom metadata to set
435
+ * @throws {Error} If the default disk driver does not support metadata updates
436
+ */
437
+ setMetadata(key, metadata) {
438
+ return this.disk().setMetadata(key, metadata);
439
+ }
154
440
  };
155
441
 
156
442
  // src/stores/LocalStore.ts
@@ -163,11 +449,28 @@ var LocalStore = class {
163
449
  this.baseUrl = baseUrl;
164
450
  }
165
451
  runtime = (0, import_core.getRuntimeAdapter)();
166
- async put(key, data) {
452
+ /**
453
+ * Writes data to a file on the local disk.
454
+ *
455
+ * Automatically creates parent directories if they don't exist.
456
+ *
457
+ * @param key - Relative path from the root directory
458
+ * @param data - Content to write
459
+ * @param options - Optional upload options (note: customMetadata not persisted in LocalStore)
460
+ * @throws {Error} If the key is invalid or path is outside root
461
+ */
462
+ async put(key, data, options) {
167
463
  const path = this.resolvePath(key);
168
464
  await this.ensureDirectory(path);
169
465
  await this.runtime.writeFile(path, data);
170
466
  }
467
+ /**
468
+ * Reads a file from the local disk as a Blob.
469
+ *
470
+ * @param key - Relative path from the root directory
471
+ * @returns File content as Blob, or null if not found
472
+ * @throws {Error} If the key is invalid or path is outside root
473
+ */
171
474
  async get(key) {
172
475
  if (!await this.exists(key)) {
173
476
  return null;
@@ -175,6 +478,13 @@ var LocalStore = class {
175
478
  const path = this.resolvePath(key);
176
479
  return this.runtime.readFileAsBlob(path);
177
480
  }
481
+ /**
482
+ * Deletes a file from the local disk.
483
+ *
484
+ * @param key - Relative path from the root directory
485
+ * @returns True if deleted, false if file didn't exist
486
+ * @throws {Error} If the key is invalid or path is outside root
487
+ */
178
488
  async delete(key) {
179
489
  if (!await this.exists(key)) {
180
490
  return false;
@@ -183,10 +493,23 @@ var LocalStore = class {
183
493
  await this.runtime.deleteFile(path);
184
494
  return true;
185
495
  }
496
+ /**
497
+ * Checks if a file exists on the local disk.
498
+ *
499
+ * @param key - Relative path from the root directory
500
+ * @returns True if exists, false otherwise
501
+ */
186
502
  async exists(key) {
187
503
  const path = this.resolvePath(key);
188
504
  return this.runtime.exists(path);
189
505
  }
506
+ /**
507
+ * Copies a file on the local disk.
508
+ *
509
+ * @param from - Source relative path
510
+ * @param to - Destination relative path
511
+ * @throws {Error} If source missing or operation fails
512
+ */
190
513
  async copy(from, to) {
191
514
  const data = await this.get(from);
192
515
  if (!data) {
@@ -194,15 +517,32 @@ var LocalStore = class {
194
517
  }
195
518
  await this.put(to, data);
196
519
  }
520
+ /**
521
+ * Moves a file on the local disk.
522
+ *
523
+ * @param from - Current relative path
524
+ * @param to - New relative path
525
+ * @throws {Error} If source missing or operation fails
526
+ */
197
527
  async move(from, to) {
198
528
  await this.copy(from, to);
199
529
  await this.delete(from);
200
530
  }
201
- list(prefix = "") {
531
+ /**
532
+ * @internal
533
+ * @throws {Error} Always, as not yet implemented
534
+ */
535
+ list(_prefix = "") {
202
536
  throw new Error(
203
537
  "[LocalStore] list() is not yet implemented. Requires RuntimeAdapter.readDir() support in @gravito/core."
204
538
  );
205
539
  }
540
+ /**
541
+ * Retrieves file metadata from the local filesystem.
542
+ *
543
+ * @param key - Relative path from the root directory
544
+ * @returns Metadata object or null if not found
545
+ */
206
546
  async getMetadata(key) {
207
547
  if (!await this.exists(key)) {
208
548
  return null;
@@ -216,10 +556,63 @@ var LocalStore = class {
216
556
  lastModified: /* @__PURE__ */ new Date()
217
557
  };
218
558
  }
559
+ /**
560
+ * Generates a public URL based on the configured base URL.
561
+ *
562
+ * @param key - Relative path from the root directory
563
+ * @returns URL string
564
+ */
219
565
  getUrl(key) {
220
566
  const safeKey = this.normalizeKey(key);
221
567
  return `${this.baseUrl}/${safeKey}`;
222
568
  }
569
+ /**
570
+ * Writes data from a readable stream to a file on the local disk.
571
+ *
572
+ * 串流寫入檔案
573
+ * Automatically creates parent directories if they don't exist.
574
+ * Useful for handling large files without loading entire content into memory.
575
+ *
576
+ * @param key - Relative path from the root directory
577
+ * @param stream - Readable stream containing the data
578
+ * @throws {Error} If the key is invalid or path is outside root
579
+ */
580
+ async putStream(key, stream) {
581
+ const path = this.resolvePath(key);
582
+ await this.ensureDirectory(path);
583
+ const file = Bun.file(path);
584
+ const writer = file.writer();
585
+ const reader = stream.getReader();
586
+ try {
587
+ while (true) {
588
+ const { done, value } = await reader.read();
589
+ if (done) break;
590
+ writer.write(value);
591
+ }
592
+ await writer.end();
593
+ } catch (error) {
594
+ reader.releaseLock();
595
+ throw new Error(`[LocalStore] Failed to write stream: ${error}`);
596
+ }
597
+ }
598
+ /**
599
+ * Reads a file from the local disk as a readable stream.
600
+ *
601
+ * 串流讀取檔案
602
+ * Useful for handling large files without loading entire content into memory.
603
+ *
604
+ * @param key - Relative path from the root directory
605
+ * @returns Readable stream of file content, or null if not found
606
+ * @throws {Error} If the key is invalid or path is outside root
607
+ */
608
+ async getStream(key) {
609
+ if (!await this.exists(key)) {
610
+ return null;
611
+ }
612
+ const path = this.resolvePath(key);
613
+ const file = Bun.file(path);
614
+ return file.stream();
615
+ }
223
616
  normalizeKey(key) {
224
617
  if (!key || key.includes("\0")) {
225
618
  throw new Error("[LocalStore] Invalid storage key: empty or contains null byte.");
@@ -269,34 +662,70 @@ var LocalStore = class {
269
662
  // src/stores/MemoryStore.ts
270
663
  var MemoryStore = class {
271
664
  files = /* @__PURE__ */ new Map();
272
- async put(key, data) {
665
+ /**
666
+ * Stores data in the internal Map.
667
+ *
668
+ * Converts input data to a Blob before storage.
669
+ *
670
+ * @param key - Unique identifier for the file
671
+ * @param data - Content to store
672
+ * @param options - Optional upload options (content-type, metadata, etc.)
673
+ */
674
+ async put(key, data, options) {
273
675
  let blob;
274
676
  if (data instanceof Blob) {
275
677
  blob = data;
276
678
  } else if (typeof data === "string") {
277
- blob = new Blob([data]);
679
+ blob = new Blob([data], { type: options?.contentType });
278
680
  } else {
279
- blob = new Blob([new Uint8Array(data)]);
681
+ blob = new Blob([new Uint8Array(data)], { type: options?.contentType });
280
682
  }
281
683
  this.files.set(key, {
282
684
  data: blob,
685
+ options,
283
686
  metadata: {
284
687
  key,
285
688
  size: blob.size,
286
- mimeType: blob.type || "application/octet-stream",
287
- lastModified: /* @__PURE__ */ new Date()
689
+ mimeType: options?.contentType || blob.type || "application/octet-stream",
690
+ lastModified: /* @__PURE__ */ new Date(),
691
+ customMetadata: options?.metadata
288
692
  }
289
693
  });
290
694
  }
695
+ /**
696
+ * Retrieves a Blob from the internal Map.
697
+ *
698
+ * @param key - Unique identifier for the file
699
+ * @returns The Blob content, or null if not found
700
+ */
291
701
  async get(key) {
292
702
  return this.files.get(key)?.data ?? null;
293
703
  }
704
+ /**
705
+ * Removes a file from memory.
706
+ *
707
+ * @param key - Unique identifier for the file
708
+ * @returns True if deleted, false if not found
709
+ */
294
710
  async delete(key) {
295
711
  return this.files.delete(key);
296
712
  }
713
+ /**
714
+ * Checks if a key exists in the internal Map.
715
+ *
716
+ * @param key - Unique identifier to check
717
+ * @returns True if exists, false otherwise
718
+ */
297
719
  async exists(key) {
298
720
  return this.files.has(key);
299
721
  }
722
+ /**
723
+ * Copies a file within memory.
724
+ *
725
+ * @param from - Source key
726
+ * @param to - Destination key
727
+ * @throws {Error} If source file is missing
728
+ */
300
729
  async copy(from, to) {
301
730
  const file = this.files.get(from);
302
731
  if (!file) {
@@ -307,10 +736,23 @@ var MemoryStore = class {
307
736
  metadata: { ...file.metadata, key: to, lastModified: /* @__PURE__ */ new Date() }
308
737
  });
309
738
  }
739
+ /**
740
+ * Moves a file within memory.
741
+ *
742
+ * @param from - Current key
743
+ * @param to - New key
744
+ * @throws {Error} If source file is missing
745
+ */
310
746
  async move(from, to) {
311
747
  await this.copy(from, to);
312
748
  await this.delete(from);
313
749
  }
750
+ /**
751
+ * Lists all files currently in memory.
752
+ *
753
+ * @param prefix - Optional key prefix to filter by
754
+ * @returns Async iterable of storage items
755
+ */
314
756
  async *list(prefix = "") {
315
757
  for (const [key, file] of this.files.entries()) {
316
758
  if (key.startsWith(prefix)) {
@@ -323,36 +765,182 @@ var MemoryStore = class {
323
765
  }
324
766
  }
325
767
  }
768
+ /**
769
+ * Retrieves metadata for an in-memory file.
770
+ *
771
+ * @param key - Unique identifier for the file
772
+ * @returns Metadata object or null if not found
773
+ */
326
774
  async getMetadata(key) {
327
775
  return this.files.get(key)?.metadata ?? null;
328
776
  }
777
+ /**
778
+ * Generates a dummy URL for the in-memory file.
779
+ *
780
+ * @param key - Unique identifier for the file
781
+ * @returns A string starting with /memory/
782
+ */
329
783
  getUrl(key) {
330
784
  return `/memory/${key}`;
331
785
  }
786
+ /**
787
+ * Stores data from a readable stream in memory.
788
+ *
789
+ * 串流寫入記憶體
790
+ * Reads the entire stream into memory before storing.
791
+ *
792
+ * @param key - Unique identifier for the file
793
+ * @param stream - Readable stream containing the data
794
+ */
795
+ async putStream(key, stream) {
796
+ const reader = stream.getReader();
797
+ const chunks = [];
798
+ try {
799
+ while (true) {
800
+ const { done, value } = await reader.read();
801
+ if (done) break;
802
+ chunks.push(value);
803
+ }
804
+ const buffer = Buffer.concat(chunks);
805
+ const blob = new Blob([buffer]);
806
+ await this.put(key, blob);
807
+ } catch (error) {
808
+ reader.releaseLock();
809
+ throw new Error(`[MemoryStore] Failed to write stream: ${error}`);
810
+ }
811
+ }
812
+ /**
813
+ * Reads an in-memory file as a readable stream.
814
+ *
815
+ * 串流讀取記憶體檔案
816
+ *
817
+ * @param key - Unique identifier for the file
818
+ * @returns Readable stream of file content, or null if not found
819
+ */
820
+ async getStream(key) {
821
+ const blob = await this.get(key);
822
+ if (!blob) {
823
+ return null;
824
+ }
825
+ return blob.stream();
826
+ }
827
+ /**
828
+ * Updates custom metadata for an in-memory file.
829
+ *
830
+ * 更新自定義 Metadata
831
+ *
832
+ * @param key - Unique identifier for the file
833
+ * @param metadata - Custom metadata to set
834
+ * @throws {Error} If file does not exist
835
+ */
836
+ async setMetadata(key, metadata) {
837
+ const file = this.files.get(key);
838
+ if (!file) {
839
+ throw new Error(`[MemoryStore] File not found: ${key}`);
840
+ }
841
+ file.metadata.customMetadata = {
842
+ ...file.metadata.customMetadata,
843
+ ...metadata
844
+ };
845
+ file.metadata.lastModified = /* @__PURE__ */ new Date();
846
+ }
847
+ /**
848
+ * Lists files with pagination support.
849
+ *
850
+ * 分頁列舉記憶體中的檔案
851
+ * Provides cursor-based pagination for consistent API with other drivers.
852
+ *
853
+ * @param prefix - Key prefix to filter by
854
+ * @param options - Pagination and filtering options
855
+ * @returns Paginated list result
856
+ */
857
+ async listPaginated(prefix = "", options) {
858
+ const maxResults = options?.maxResults ?? 1e3;
859
+ const cursorKey = options?.cursor;
860
+ const allKeys = Array.from(this.files.keys()).filter((key) => key.startsWith(prefix)).sort();
861
+ let startIndex = 0;
862
+ if (cursorKey) {
863
+ startIndex = allKeys.findIndex((key) => key > cursorKey);
864
+ if (startIndex === -1) {
865
+ return {
866
+ items: [],
867
+ nextCursor: null,
868
+ hasMore: false,
869
+ count: 0
870
+ };
871
+ }
872
+ }
873
+ const endIndex = startIndex + maxResults;
874
+ const pageKeys = allKeys.slice(startIndex, endIndex);
875
+ const hasMore = endIndex < allKeys.length;
876
+ const items = pageKeys.map((key) => {
877
+ const file = this.files.get(key);
878
+ return {
879
+ key,
880
+ isDirectory: false,
881
+ size: file.metadata.size,
882
+ lastModified: file.metadata.lastModified
883
+ };
884
+ });
885
+ const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].key : null;
886
+ return {
887
+ items,
888
+ nextCursor,
889
+ hasMore,
890
+ count: items.length
891
+ };
892
+ }
332
893
  };
333
894
 
334
895
  // src/stores/NullStore.ts
335
896
  var NullStore = class {
897
+ /**
898
+ * No-op: does not store any data.
899
+ */
336
900
  async put(_key, _data) {
337
901
  }
902
+ /**
903
+ * Always returns null.
904
+ */
338
905
  async get(_key) {
339
906
  return null;
340
907
  }
908
+ /**
909
+ * Always returns false.
910
+ */
341
911
  async delete(_key) {
342
912
  return false;
343
913
  }
914
+ /**
915
+ * Always returns false.
916
+ */
344
917
  async exists(_key) {
345
918
  return false;
346
919
  }
920
+ /**
921
+ * No-op: does not copy anything.
922
+ */
347
923
  async copy(_from, _to) {
348
924
  }
925
+ /**
926
+ * No-op: does not move anything.
927
+ */
349
928
  async move(_from, _to) {
350
929
  }
930
+ /**
931
+ * Always yields nothing.
932
+ */
351
933
  async *list(_prefix) {
352
934
  }
935
+ /**
936
+ * Always returns null.
937
+ */
353
938
  async getMetadata(_key) {
354
939
  return null;
355
940
  }
941
+ /**
942
+ * Generates a dummy URL starting with /null/.
943
+ */
356
944
  getUrl(key) {
357
945
  return `/null/${key}`;
358
946
  }
@@ -365,9 +953,13 @@ var OrbitNebula = class {
365
953
  }
366
954
  manager;
367
955
  /**
368
- * Install storage service into PlanetCore.
956
+ * Bootstraps the storage service and registers it with PlanetCore.
957
+ *
958
+ * This method initializes the StorageManager, configures the default disk,
959
+ * and registers the storage service in the IoC container and middleware.
369
960
  *
370
- * @param core - The PlanetCore instance.
961
+ * @param core - The PlanetCore instance to install into
962
+ * @throws {Error} If configuration is missing or default disk cannot be initialized
371
963
  */
372
964
  install(core) {
373
965
  const config = this.options || core.config.get("storage");
@@ -380,9 +972,13 @@ var OrbitNebula = class {
380
972
  core.logger.info(`[OrbitNebula] Initializing Storage (Exposed as: ${exposeAs})`);
381
973
  let defaultDisk = config.default;
382
974
  if (!defaultDisk) {
383
- if (config.local) defaultDisk = "local";
384
- else if (config.provider) defaultDisk = "custom";
385
- else defaultDisk = "local";
975
+ if (config.local) {
976
+ defaultDisk = "local";
977
+ } else if (config.provider) {
978
+ defaultDisk = "custom";
979
+ } else {
980
+ defaultDisk = "local";
981
+ }
386
982
  }
387
983
  const managerOptions = {
388
984
  default: defaultDisk
@@ -403,10 +999,13 @@ var OrbitNebula = class {
403
999
  core.hooks.doAction("storage:init", { manager: this.manager });
404
1000
  }
405
1001
  /**
406
- * Get the installed StorageManager instance.
1002
+ * Retrieves the initialized StorageManager instance.
1003
+ *
1004
+ * Use this to access storage operations directly from the orbit instance
1005
+ * after it has been installed.
407
1006
  *
408
- * @returns The StorageManager instance.
409
- * @throws {Error} If not installed.
1007
+ * @returns The active StorageManager instance
1008
+ * @throws {Error} If called before the orbit is installed
410
1009
  */
411
1010
  getStorage() {
412
1011
  if (!this.manager) {