nuxt-filer 0.0.13 → 0.0.15

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/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
3
  "configKey": "filer",
4
- "version": "0.0.13",
4
+ "version": "0.0.15",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,11 +1,25 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ async function mapWithConcurrency(items, limit, fn) {
3
+ const results = new Array(items.length);
4
+ let next = 0;
5
+ const worker = async () => {
6
+ while (next < items.length) {
7
+ const index = next++;
8
+ results[index] = await fn(items[index], index);
9
+ }
10
+ };
11
+ const workers = Array.from({ length: Math.min(limit, items.length) || 1 }, worker);
12
+ await Promise.all(workers);
13
+ return results;
14
+ }
2
15
  export function createS3Provider(options) {
3
16
  const prefix = options.prefix ? options.prefix.replace(/\/+$/, "") + "/" : "";
4
17
  let clientPromise;
5
18
  const getClient = () => clientPromise ??= options.client ? Promise.resolve(options.client) : createAwsS3Client(options);
6
- const dataKey = (groupId, id) => `${prefix}${groupId}/data/${id}`;
7
- const metaKey = (groupId, id) => `${prefix}${groupId}/meta/${id}`;
8
- const metaPrefix = (groupId) => `${prefix}${groupId}/meta/`;
19
+ const seg = (value) => value.replace(/^\/+|\/+$/g, "");
20
+ const dataKey = (groupId, id) => `${prefix}${seg(groupId)}/data/${seg(id)}`;
21
+ const metaKey = (groupId, id) => `${prefix}${seg(groupId)}/meta/${seg(id)}`;
22
+ const metaPrefix = (groupId) => `${prefix}${seg(groupId)}/meta/`;
9
23
  const groupIdFromMetaKey = (key) => {
10
24
  const rel = key.slice(prefix.length);
11
25
  return rel.slice(0, rel.lastIndexOf("/meta/"));
@@ -66,11 +80,14 @@ export function createS3Provider(options) {
66
80
  },
67
81
  async list(groupId) {
68
82
  const client = await getClient();
83
+ const keys = [];
84
+ for await (const key of client.listKeys(metaPrefix(groupId))) keys.push(key);
85
+ const metas = await mapWithConcurrency(keys, 32, (key) => readMeta(client, key));
69
86
  const files = [];
70
- for await (const key of client.listKeys(metaPrefix(groupId))) {
71
- const meta = await readMeta(client, key);
87
+ for (let i = 0; i < keys.length; i++) {
88
+ const meta = metas[i];
72
89
  if (!meta) continue;
73
- files.push(toStoredFile(idFromKey(key), groupId, meta));
90
+ files.push(toStoredFile(idFromKey(keys[i]), groupId, meta));
74
91
  }
75
92
  return files;
76
93
  },
@@ -99,7 +116,7 @@ export function createS3Provider(options) {
99
116
  },
100
117
  async clear(groupId) {
101
118
  const client = await getClient();
102
- for await (const key of client.listKeys(`${prefix}${groupId}/`)) {
119
+ for await (const key of client.listKeys(`${prefix}${seg(groupId)}/`)) {
103
120
  await client.delete(key);
104
121
  }
105
122
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "File storage module for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,6 +38,31 @@ export interface S3ProviderOptions {
38
38
 
39
39
  type InternalMeta = FileMeta & { _createdAt?: string; _updatedAt?: string };
40
40
 
41
+ /**
42
+ * Map over `items` with at most `limit` promises in flight, preserving order.
43
+ * Used to read many meta objects concurrently instead of one-at-a-time — on a
44
+ * remote store (R2/S3) the per-object round-trip latency dominates, so a
45
+ * sequential `for await` over N files is ~N × RTT. Bounded so a huge group
46
+ * doesn't open thousands of sockets at once.
47
+ */
48
+ async function mapWithConcurrency<T, R>(
49
+ items: T[],
50
+ limit: number,
51
+ fn: (item: T, index: number) => Promise<R>,
52
+ ): Promise<R[]> {
53
+ const results = new Array<R>(items.length);
54
+ let next = 0;
55
+ const worker = async () => {
56
+ while (next < items.length) {
57
+ const index = next++;
58
+ results[index] = await fn(items[index], index);
59
+ }
60
+ };
61
+ const workers = Array.from({ length: Math.min(limit, items.length) || 1 }, worker);
62
+ await Promise.all(workers);
63
+ return results;
64
+ }
65
+
41
66
  /**
42
67
  * S3-backed {@link FileStorageProvider}. Binary data and a JSON metadata
43
68
  * sidecar are stored as separate objects per file, mirroring the built-in
@@ -56,9 +81,13 @@ export function createS3Provider(options: S3ProviderOptions): FileStorageProvide
56
81
  ? Promise.resolve(options.client)
57
82
  : createAwsS3Client(options));
58
83
 
59
- const dataKey = (groupId: string, id: string) => `${prefix}${groupId}/data/${id}`;
60
- const metaKey = (groupId: string, id: string) => `${prefix}${groupId}/meta/${id}`;
61
- const metaPrefix = (groupId: string) => `${prefix}${groupId}/meta/`;
84
+ // Strip leading/trailing slashes so callers can pass `studio` or `/studio`
85
+ // interchangeably (the IPX integration hands group ids through with a leading
86
+ // slash). Without this, keys like `/studio/data/x` miss the stored object.
87
+ const seg = (value: string) => value.replace(/^\/+|\/+$/g, '');
88
+ const dataKey = (groupId: string, id: string) => `${prefix}${seg(groupId)}/data/${seg(id)}`;
89
+ const metaKey = (groupId: string, id: string) => `${prefix}${seg(groupId)}/meta/${seg(id)}`;
90
+ const metaPrefix = (groupId: string) => `${prefix}${seg(groupId)}/meta/`;
62
91
 
63
92
  /** `${prefix}${groupId}/meta/${id}` → groupId (groups may contain '/'). */
64
93
  const groupIdFromMetaKey = (key: string) => {
@@ -139,11 +168,17 @@ export function createS3Provider(options: S3ProviderOptions): FileStorageProvide
139
168
 
140
169
  async list(groupId) {
141
170
  const client = await getClient();
171
+ // Enumerate the group's meta keys (cheap paginated LIST), then read every
172
+ // meta object in parallel. The previous sequential read was one blocking
173
+ // GET per file, so listing a group cost ~N round-trips to the store.
174
+ const keys: string[] = [];
175
+ for await (const key of client.listKeys(metaPrefix(groupId))) keys.push(key);
176
+ const metas = await mapWithConcurrency(keys, 32, (key) => readMeta(client, key));
142
177
  const files: StoredFile[] = [];
143
- for await (const key of client.listKeys(metaPrefix(groupId))) {
144
- const meta = await readMeta(client, key);
178
+ for (let i = 0; i < keys.length; i++) {
179
+ const meta = metas[i];
145
180
  if (!meta) continue;
146
- files.push(toStoredFile(idFromKey(key), groupId, meta));
181
+ files.push(toStoredFile(idFromKey(keys[i]), groupId, meta));
147
182
  }
148
183
  return files;
149
184
  },
@@ -175,7 +210,7 @@ export function createS3Provider(options: S3ProviderOptions): FileStorageProvide
175
210
 
176
211
  async clear(groupId) {
177
212
  const client = await getClient();
178
- for await (const key of client.listKeys(`${prefix}${groupId}/`)) {
213
+ for await (const key of client.listKeys(`${prefix}${seg(groupId)}/`)) {
179
214
  await client.delete(key);
180
215
  }
181
216
  },