@objectstack/service-storage 5.0.0 → 5.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
@@ -56,10 +56,11 @@ async function streamToBuffer(stream) {
56
56
  }
57
57
  return Buffer.concat(chunks);
58
58
  }
59
- var S3StorageAdapter;
59
+ var import_observability2, S3StorageAdapter;
60
60
  var init_s3_storage_adapter = __esm({
61
61
  "src/s3-storage-adapter.ts"() {
62
62
  "use strict";
63
+ import_observability2 = require("@objectstack/observability");
63
64
  S3StorageAdapter = class {
64
65
  constructor(options) {
65
66
  this.options = options;
@@ -72,6 +73,34 @@ var init_s3_storage_adapter = __esm({
72
73
  this.region = options.region;
73
74
  this.endpoint = options.endpoint;
74
75
  this.forcePathStyle = options.forcePathStyle ?? false;
76
+ this.metrics = options.metrics ?? new import_observability2.NoopMetricsRegistry();
77
+ }
78
+ /**
79
+ * Wrap a storage operation with metrics instrumentation.
80
+ * Records ok/error counters, a duration histogram, and an error counter
81
+ * keyed by error class on failure. Never swallows the underlying error.
82
+ */
83
+ async track(op, fn) {
84
+ const started = Date.now();
85
+ const baseLabels = { adapter: "s3", op };
86
+ try {
87
+ const out = await fn();
88
+ try {
89
+ this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
90
+ this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
91
+ } catch {
92
+ }
93
+ return out;
94
+ } catch (err) {
95
+ try {
96
+ this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
97
+ this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
98
+ const errorClass = err?.name || err?.constructor?.name || "Error";
99
+ this.metrics.counter(import_observability2.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
100
+ } catch {
101
+ }
102
+ throw err;
103
+ }
75
104
  }
76
105
  /**
77
106
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
@@ -121,67 +150,79 @@ var init_s3_storage_adapter = __esm({
121
150
  // Basic operations
122
151
  // ---------------------------------------------------------------------------
123
152
  async upload(key, data, options) {
124
- const client = await this.getClient();
125
- const s3 = await this.s3Mod();
126
- const body = data instanceof Buffer ? data : await streamToBuffer(data);
127
- const cmd = new s3.PutObjectCommand({
128
- Bucket: this.bucket,
129
- Key: key,
130
- Body: body,
131
- ContentType: options?.contentType,
132
- Metadata: options?.metadata,
133
- ACL: options?.acl === "public-read" ? "public-read" : void 0
153
+ return this.track("put", async () => {
154
+ const client = await this.getClient();
155
+ const s3 = await this.s3Mod();
156
+ const body = data instanceof Buffer ? data : await streamToBuffer(data);
157
+ const cmd = new s3.PutObjectCommand({
158
+ Bucket: this.bucket,
159
+ Key: key,
160
+ Body: body,
161
+ ContentType: options?.contentType,
162
+ Metadata: options?.metadata,
163
+ ACL: options?.acl === "public-read" ? "public-read" : void 0
164
+ });
165
+ await client.send(cmd);
134
166
  });
135
- await client.send(cmd);
136
167
  }
137
168
  async download(key) {
138
- const client = await this.getClient();
139
- const s3 = await this.s3Mod();
140
- const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
141
- const res = await client.send(cmd);
142
- return streamToBuffer(res.Body);
169
+ return this.track("get", async () => {
170
+ const client = await this.getClient();
171
+ const s3 = await this.s3Mod();
172
+ const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
173
+ const res = await client.send(cmd);
174
+ return streamToBuffer(res.Body);
175
+ });
143
176
  }
144
177
  async delete(key) {
145
- const client = await this.getClient();
146
- const s3 = await this.s3Mod();
147
- const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
148
- await client.send(cmd);
178
+ return this.track("delete", async () => {
179
+ const client = await this.getClient();
180
+ const s3 = await this.s3Mod();
181
+ const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
182
+ await client.send(cmd);
183
+ });
149
184
  }
150
185
  async exists(key) {
151
- const client = await this.getClient();
152
- const s3 = await this.s3Mod();
153
- try {
154
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
155
- await client.send(cmd);
156
- return true;
157
- } catch (err) {
158
- if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
159
- throw err;
160
- }
186
+ return this.track("head", async () => {
187
+ const client = await this.getClient();
188
+ const s3 = await this.s3Mod();
189
+ try {
190
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
191
+ await client.send(cmd);
192
+ return true;
193
+ } catch (err) {
194
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
195
+ throw err;
196
+ }
197
+ });
161
198
  }
162
199
  async getInfo(key) {
163
- const client = await this.getClient();
164
- const s3 = await this.s3Mod();
165
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
166
- const res = await client.send(cmd);
167
- return {
168
- key,
169
- size: res.ContentLength ?? 0,
170
- contentType: res.ContentType,
171
- lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
172
- metadata: res.Metadata
173
- };
200
+ return this.track("head", async () => {
201
+ const client = await this.getClient();
202
+ const s3 = await this.s3Mod();
203
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
204
+ const res = await client.send(cmd);
205
+ return {
206
+ key,
207
+ size: res.ContentLength ?? 0,
208
+ contentType: res.ContentType,
209
+ lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
210
+ metadata: res.Metadata
211
+ };
212
+ });
174
213
  }
175
214
  async list(prefix) {
176
- const client = await this.getClient();
177
- const s3 = await this.s3Mod();
178
- const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
179
- const res = await client.send(cmd);
180
- return (res.Contents ?? []).map((item) => ({
181
- key: item.Key,
182
- size: item.Size ?? 0,
183
- lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
184
- }));
215
+ return this.track("list", async () => {
216
+ const client = await this.getClient();
217
+ const s3 = await this.s3Mod();
218
+ const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
219
+ const res = await client.send(cmd);
220
+ return (res.Contents ?? []).map((item) => ({
221
+ key: item.Key,
222
+ size: item.Size ?? 0,
223
+ lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
224
+ }));
225
+ });
185
226
  }
186
227
  // ---------------------------------------------------------------------------
187
228
  // Presigned URLs
@@ -312,6 +353,7 @@ module.exports = __toCommonJS(index_exports);
312
353
  var import_node_fs = require("fs");
313
354
  var import_node_path = require("path");
314
355
  var import_node_crypto = require("crypto");
356
+ var import_observability = require("@objectstack/observability");
315
357
  var LocalStorageAdapter = class {
316
358
  constructor(options) {
317
359
  this.rootDir = options.rootDir;
@@ -319,6 +361,33 @@ var LocalStorageAdapter = class {
319
361
  this.baseUrl = options.baseUrl ?? "";
320
362
  this.basePath = options.basePath ?? "/api/v1/storage";
321
363
  this.signingSecret = options.signingSecret ?? (0, import_node_crypto.randomUUID)();
364
+ this.metrics = options.metrics ?? new import_observability.NoopMetricsRegistry();
365
+ }
366
+ /**
367
+ * Wrap a storage operation with metrics instrumentation. Never swallows
368
+ * the underlying error; instrumentation failures are silently ignored.
369
+ */
370
+ async track(op, fn) {
371
+ const started = Date.now();
372
+ const baseLabels = { adapter: "local", op };
373
+ try {
374
+ const out = await fn();
375
+ try {
376
+ this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
377
+ this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
378
+ } catch {
379
+ }
380
+ return out;
381
+ } catch (err) {
382
+ try {
383
+ this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
384
+ this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
385
+ const errorClass = err?.name || err?.constructor?.name || "Error";
386
+ this.metrics.counter(import_observability.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
387
+ } catch {
388
+ }
389
+ throw err;
390
+ }
322
391
  }
323
392
  // ---------------------------------------------------------------------------
324
393
  // Path helpers
@@ -339,61 +408,72 @@ var LocalStorageAdapter = class {
339
408
  // Basic file operations
340
409
  // ---------------------------------------------------------------------------
341
410
  async upload(key, data, _options) {
342
- const filePath = this.resolvePath(key);
343
- await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
344
- if (data instanceof Buffer) {
345
- await import_node_fs.promises.writeFile(filePath, data);
346
- return;
347
- }
348
- const chunks = [];
349
- const reader = data.getReader();
350
- let done = false;
351
- while (!done) {
352
- const result = await reader.read();
353
- done = result.done;
354
- if (result.value) chunks.push(result.value);
355
- }
356
- await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
411
+ return this.track("put", async () => {
412
+ const filePath = this.resolvePath(key);
413
+ await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
414
+ if (data instanceof Buffer) {
415
+ await import_node_fs.promises.writeFile(filePath, data);
416
+ return;
417
+ }
418
+ const chunks = [];
419
+ const reader = data.getReader();
420
+ let done = false;
421
+ while (!done) {
422
+ const result = await reader.read();
423
+ done = result.done;
424
+ if (result.value) chunks.push(result.value);
425
+ }
426
+ await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
427
+ });
357
428
  }
358
429
  async download(key) {
359
- return import_node_fs.promises.readFile(this.resolvePath(key));
430
+ return this.track("get", async () => import_node_fs.promises.readFile(this.resolvePath(key)));
360
431
  }
361
432
  async delete(key) {
362
- await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
363
- if (err && err.code === "ENOENT") return;
364
- throw err;
433
+ return this.track("delete", async () => {
434
+ await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
435
+ if (err && err.code === "ENOENT") return;
436
+ throw err;
437
+ });
365
438
  });
366
439
  }
367
440
  async exists(key) {
368
- try {
369
- await import_node_fs.promises.access(this.resolvePath(key));
370
- return true;
371
- } catch {
372
- return false;
373
- }
441
+ return this.track("head", async () => {
442
+ try {
443
+ await import_node_fs.promises.access(this.resolvePath(key));
444
+ return true;
445
+ } catch {
446
+ return false;
447
+ }
448
+ });
374
449
  }
375
450
  async getInfo(key) {
376
- const filePath = this.resolvePath(key);
377
- const stat = await import_node_fs.promises.stat(filePath);
378
- return { key, size: stat.size, lastModified: stat.mtime };
451
+ return this.track("head", async () => {
452
+ const filePath = this.resolvePath(key);
453
+ const stat = await import_node_fs.promises.stat(filePath);
454
+ return { key, size: stat.size, lastModified: stat.mtime };
455
+ });
379
456
  }
380
457
  async list(prefix) {
381
- const dirPath = this.resolvePath(prefix);
382
- try {
383
- const entries = await import_node_fs.promises.readdir(dirPath);
384
- const results = [];
385
- for (const entry of entries) {
386
- if (entry.startsWith(".")) continue;
387
- const fullKey = prefix ? `${prefix}/${entry}` : entry;
388
- try {
389
- results.push(await this.getInfo(fullKey));
390
- } catch {
458
+ return this.track("list", async () => {
459
+ const dirPath = this.resolvePath(prefix);
460
+ try {
461
+ const entries = await import_node_fs.promises.readdir(dirPath);
462
+ const results = [];
463
+ for (const entry of entries) {
464
+ if (entry.startsWith(".")) continue;
465
+ const fullKey = prefix ? `${prefix}/${entry}` : entry;
466
+ try {
467
+ const stat = await import_node_fs.promises.stat(this.resolvePath(fullKey));
468
+ results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
469
+ } catch {
470
+ }
391
471
  }
472
+ return results;
473
+ } catch {
474
+ return [];
392
475
  }
393
- return results;
394
- } catch {
395
- return [];
396
- }
476
+ });
397
477
  }
398
478
  // ---------------------------------------------------------------------------
399
479
  // Presigned URL helpers