@hot-updater/standalone 1.2.0-rc.7 → 1.2.0-rc.9

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
@@ -12,11 +12,13 @@ var StandaloneDatabaseError = class extends Error {
12
12
  }
13
13
  };
14
14
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
15
+ /** `headers` with each lazy value resolved. */
16
+ const resolveHeaders = async (headers = {}) => Object.fromEntries(await Promise.all(Object.entries(headers).map(async ([name, value]) => [name, await (0, _hot_updater_plugin_core.resolveLazy)(value)])));
15
17
  const createStandaloneHttp = (config) => {
16
18
  const buildUrl = (path) => `${config.baseUrl.replace(/\/+$/, "")}${path}`;
17
- const headers = (routeHeaders) => ({
19
+ const headers = async (routeHeaders) => ({
18
20
  "Content-Type": "application/json",
19
- ...config.commonHeaders,
21
+ ...await resolveHeaders(config.commonHeaders),
20
22
  ...routeHeaders
21
23
  });
22
24
  const requestFailed = async (response) => {
@@ -108,7 +110,7 @@ const createStandaloneCoreApi = (config) => {
108
110
  const http = createStandaloneHttp(config);
109
111
  let checked;
110
112
  const checkProtocol = async () => {
111
- const response = await fetch(http.buildUrl("/version"), { headers: http.headers({ "Cache-Control": "no-cache" }) });
113
+ const response = await fetch(http.buildUrl("/version"), { headers: await http.headers({ "Cache-Control": "no-cache" }) });
112
114
  const body = response.status === 404 ? {} : await http.parseJson(response);
113
115
  const protocol = isRecord(body) ? body.adminProtocol : void 0;
114
116
  if (typeof protocol !== "number" || protocol < 2) throw new StandaloneDatabaseError("request-failed", `The server at ${config.baseUrl} does not report Hot Updater admin API protocol 2 at /version. Set baseUrl to the path where the server mounts handlers.admin, such as https://example.com/hot-updater/admin.`, response.status);
@@ -125,7 +127,7 @@ const createStandaloneCoreApi = (config) => {
125
127
  for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
126
128
  const response = await fetch(url, {
127
129
  method,
128
- headers: http.headers(method === "GET" ? { "Cache-Control": "no-cache" } : void 0),
130
+ headers: await http.headers(method === "GET" ? { "Cache-Control": "no-cache" } : void 0),
129
131
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
130
132
  });
131
133
  if (response.status === 204) return null;
@@ -252,8 +254,8 @@ const standaloneRepository = (config) => {
252
254
  return Object.freeze({
253
255
  name: "standalone-repository",
254
256
  core: createStandaloneCoreApi(config),
255
- fetchAdmin: (path, init = {}) => {
256
- const headers = new Headers(http.headers({ "Cache-Control": "no-cache" }));
257
+ fetchAdmin: async (path, init = {}) => {
258
+ const headers = new Headers(await http.headers({ "Cache-Control": "no-cache" }));
257
259
  new Headers(init.headers).forEach((value, name) => {
258
260
  headers.set(name, value);
259
261
  });
@@ -270,8 +272,12 @@ const defaultRoutes = {
270
272
  put: () => ({ path: "/upload" }),
271
273
  get: () => ({ path: "/get" }),
272
274
  exists: () => ({ path: "/exists" }),
273
- delete: () => ({ path: "/delete" })
275
+ delete: () => ({ path: "/delete" }),
276
+ listObjects: () => ({ path: "/list" }),
277
+ deleteObjects: () => ({ path: "/delete-objects" })
274
278
  };
279
+ /** Keys sent in one `deleteObjects` request. */
280
+ const DELETE_BATCH_SIZE = 1e3;
275
281
  const createRoute = (defaultRoute, customRoute) => ({
276
282
  path: customRoute?.path ?? defaultRoute.path,
277
283
  headers: {
@@ -280,8 +286,8 @@ const createRoute = (defaultRoute, customRoute) => ({
280
286
  }
281
287
  });
282
288
  const standaloneStorage = (config) => {
283
- const getHeaders = (routeHeaders) => ({
284
- ...config.commonHeaders,
289
+ const getHeaders = async (routeHeaders) => ({
290
+ ...await resolveHeaders(config.commonHeaders),
285
291
  ...routeHeaders
286
292
  });
287
293
  const resolveRoute = (name, args) => {
@@ -289,6 +295,14 @@ const standaloneStorage = (config) => {
289
295
  const custom = config.routes?.[name];
290
296
  return createRoute(fallback(...args), custom?.(...args));
291
297
  };
298
+ const postJson = async (path, headers, body) => fetch(`${config.baseUrl}${path}`, {
299
+ method: "POST",
300
+ headers: {
301
+ "content-type": "application/json",
302
+ ...await getHeaders(headers)
303
+ },
304
+ body: JSON.stringify(body)
305
+ });
292
306
  const requestStorageUri = async (routeName, storageUri) => {
293
307
  (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, config.protocol);
294
308
  const { path, headers } = resolveRoute(routeName, [{ storageUri }]);
@@ -296,7 +310,7 @@ const standaloneStorage = (config) => {
296
310
  method: routeName === "delete" ? "DELETE" : "POST",
297
311
  headers: {
298
312
  "content-type": "application/json",
299
- ...getHeaders(headers)
313
+ ...await getHeaders(headers)
300
314
  },
301
315
  body: JSON.stringify({ storageUri })
302
316
  });
@@ -319,7 +333,7 @@ const standaloneStorage = (config) => {
319
333
  formData.append("key", key);
320
334
  const response = await fetch(`${config.baseUrl}${path}`, {
321
335
  method: "POST",
322
- headers: getHeaders(headers),
336
+ headers: await getHeaders(headers),
323
337
  body: formData
324
338
  });
325
339
  if (!response.ok) throw new Error(`Failed to upload bundle: ${response.statusText}`);
@@ -340,6 +354,39 @@ const standaloneStorage = (config) => {
340
354
  async delete({ storageUri }) {
341
355
  await requestStorageUri("delete", storageUri);
342
356
  return { deleted: true };
357
+ },
358
+ async listObjects(prefix) {
359
+ const { path, headers } = resolveRoute("listObjects", [{ prefix }]);
360
+ const objects = [];
361
+ let cursor;
362
+ do {
363
+ const response = await postJson(path, headers, {
364
+ prefix,
365
+ cursor
366
+ });
367
+ if (response.status === 404) throw new Error(`The storage service has no ${path} route, which storage prune needs.`);
368
+ if (!response.ok) throw new Error(`Failed to list storage objects: ${response.statusText}`);
369
+ const page = await response.json();
370
+ for (const object of page.objects) {
371
+ (0, _hot_updater_plugin_core.parseStorageUri)(object.storageUri, config.protocol);
372
+ objects.push({
373
+ key: object.key,
374
+ storageUri: object.storageUri,
375
+ size: object.size,
376
+ ...object.lastModifiedAt === void 0 ? {} : { lastModifiedAt: new Date(object.lastModifiedAt) }
377
+ });
378
+ }
379
+ cursor = page.cursor;
380
+ } while (cursor !== void 0);
381
+ return objects;
382
+ },
383
+ async deleteObjects(keys) {
384
+ for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
385
+ const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
386
+ const { path, headers } = resolveRoute("deleteObjects", [{ keys: batch }]);
387
+ const response = await postJson(path, headers, { keys: batch });
388
+ if (!response.ok) throw new Error(`Failed to delete storage objects: ${response.statusText}`);
389
+ }
343
390
  }
344
391
  });
345
392
  };
package/dist/index.d.cts CHANGED
@@ -1,10 +1,14 @@
1
- import { HotUpdaterCoreApi, RemoteDatabase, StorageDeleteInput, StorageExistsInput, StorageGetInput, StoragePutInput } from "@hot-updater/plugin-core";
1
+ import { HotUpdaterCoreApi, Lazy, RemoteDatabase, StorageDeleteInput, StorageExistsInput, StorageGetInput, StorageObject, StoragePutInput } from "@hot-updater/plugin-core";
2
2
  //#region src/standaloneRoutes.d.ts
3
3
  interface StandaloneRepositoryConfig {
4
4
  /** Base URL of the Hot Updater admin handler. */
5
5
  readonly baseUrl: string;
6
- /** Headers sent with every admin request, such as its authorization. */
7
- readonly commonHeaders?: Readonly<Record<string, string>>;
6
+ /**
7
+ * Headers sent with every admin request, such as its authorization. A
8
+ * value may be a function that returns it, such as `secret("<name>")` in
9
+ * `hot-updater.config.ts`, called on the first request.
10
+ */
11
+ readonly commonHeaders?: Readonly<Record<string, Lazy<string>>>;
8
12
  }
9
13
  //#endregion
10
14
  //#region src/standaloneCore.d.ts
@@ -48,12 +52,25 @@ interface StorageRoutes {
48
52
  get: (input: StorageGetInput) => RouteConfig;
49
53
  exists: (input: StorageExistsInput) => RouteConfig;
50
54
  delete: (input: StorageDeleteInput) => RouteConfig;
55
+ /** Lists objects for `hot-updater storage prune`. */
56
+ listObjects: (input: {
57
+ readonly prefix?: string;
58
+ }) => RouteConfig;
59
+ /** Deletes exact keys for `hot-updater storage prune --yes`. */
60
+ deleteObjects: (input: {
61
+ readonly keys: readonly string[];
62
+ }) => RouteConfig;
51
63
  }
52
64
  interface StandaloneStorageConfig {
53
65
  baseUrl: string;
54
66
  /** Storage URI protocol owned by the remote implementation. */
55
67
  protocol: string;
56
- commonHeaders?: Record<string, string>;
68
+ /**
69
+ * Headers sent with every request. A value may be a function that returns
70
+ * it, such as `secret("<name>")` in `hot-updater.config.ts`, called on
71
+ * the first request.
72
+ */
73
+ commonHeaders?: Record<string, Lazy<string>>;
57
74
  routes?: Partial<StorageRoutes>;
58
75
  }
59
76
  declare const standaloneStorage: (config: StandaloneStorageConfig) => {
@@ -71,6 +88,8 @@ declare const standaloneStorage: (config: StandaloneStorageConfig) => {
71
88
  readonly delete: ({ storageUri }: StorageDeleteInput) => Promise<{
72
89
  deleted: true;
73
90
  }>;
91
+ readonly listObjects: (prefix?: string) => Promise<StorageObject[]>;
92
+ readonly deleteObjects: (keys: readonly string[]) => Promise<void>;
74
93
  };
75
94
  //#endregion
76
95
  export { RouteConfig, STANDALONE_ADMIN_PROTOCOL, StandaloneDatabaseError, StandaloneRepository, type StandaloneRepositoryConfig, StandaloneStorageConfig, StorageRoutes, createStandaloneCoreApi, standaloneRepository, standaloneStorage };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,14 @@
1
- import { HotUpdaterCoreApi, RemoteDatabase, StorageDeleteInput, StorageExistsInput, StorageGetInput, StoragePutInput } from "@hot-updater/plugin-core";
1
+ import { HotUpdaterCoreApi, Lazy, RemoteDatabase, StorageDeleteInput, StorageExistsInput, StorageGetInput, StorageObject, StoragePutInput } from "@hot-updater/plugin-core";
2
2
  //#region src/standaloneRoutes.d.ts
3
3
  interface StandaloneRepositoryConfig {
4
4
  /** Base URL of the Hot Updater admin handler. */
5
5
  readonly baseUrl: string;
6
- /** Headers sent with every admin request, such as its authorization. */
7
- readonly commonHeaders?: Readonly<Record<string, string>>;
6
+ /**
7
+ * Headers sent with every admin request, such as its authorization. A
8
+ * value may be a function that returns it, such as `secret("<name>")` in
9
+ * `hot-updater.config.ts`, called on the first request.
10
+ */
11
+ readonly commonHeaders?: Readonly<Record<string, Lazy<string>>>;
8
12
  }
9
13
  //#endregion
10
14
  //#region src/standaloneCore.d.ts
@@ -48,12 +52,25 @@ interface StorageRoutes {
48
52
  get: (input: StorageGetInput) => RouteConfig;
49
53
  exists: (input: StorageExistsInput) => RouteConfig;
50
54
  delete: (input: StorageDeleteInput) => RouteConfig;
55
+ /** Lists objects for `hot-updater storage prune`. */
56
+ listObjects: (input: {
57
+ readonly prefix?: string;
58
+ }) => RouteConfig;
59
+ /** Deletes exact keys for `hot-updater storage prune --yes`. */
60
+ deleteObjects: (input: {
61
+ readonly keys: readonly string[];
62
+ }) => RouteConfig;
51
63
  }
52
64
  interface StandaloneStorageConfig {
53
65
  baseUrl: string;
54
66
  /** Storage URI protocol owned by the remote implementation. */
55
67
  protocol: string;
56
- commonHeaders?: Record<string, string>;
68
+ /**
69
+ * Headers sent with every request. A value may be a function that returns
70
+ * it, such as `secret("<name>")` in `hot-updater.config.ts`, called on
71
+ * the first request.
72
+ */
73
+ commonHeaders?: Record<string, Lazy<string>>;
57
74
  routes?: Partial<StorageRoutes>;
58
75
  }
59
76
  declare const standaloneStorage: (config: StandaloneStorageConfig) => {
@@ -71,6 +88,8 @@ declare const standaloneStorage: (config: StandaloneStorageConfig) => {
71
88
  readonly delete: ({ storageUri }: StorageDeleteInput) => Promise<{
72
89
  deleted: true;
73
90
  }>;
91
+ readonly listObjects: (prefix?: string) => Promise<StorageObject[]>;
92
+ readonly deleteObjects: (keys: readonly string[]) => Promise<void>;
74
93
  };
75
94
  //#endregion
76
95
  export { RouteConfig, STANDALONE_ADMIN_PROTOCOL, StandaloneDatabaseError, StandaloneRepository, type StandaloneRepositoryConfig, StandaloneStorageConfig, StorageRoutes, createStandaloneCoreApi, standaloneRepository, standaloneStorage };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { DatabaseBundleNotFoundError, ReleaseCatalogMutationError, ReleaseManagementError, createStorageAdapter, parseStorageUri } from "@hot-updater/plugin-core";
1
+ import { DatabaseBundleNotFoundError, ReleaseCatalogMutationError, ReleaseManagementError, createStorageAdapter, parseStorageUri, resolveLazy } from "@hot-updater/plugin-core";
2
2
  //#region src/standaloneHttp.ts
3
3
  var StandaloneDatabaseError = class extends Error {
4
4
  code;
@@ -11,11 +11,13 @@ var StandaloneDatabaseError = class extends Error {
11
11
  }
12
12
  };
13
13
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ /** `headers` with each lazy value resolved. */
15
+ const resolveHeaders = async (headers = {}) => Object.fromEntries(await Promise.all(Object.entries(headers).map(async ([name, value]) => [name, await resolveLazy(value)])));
14
16
  const createStandaloneHttp = (config) => {
15
17
  const buildUrl = (path) => `${config.baseUrl.replace(/\/+$/, "")}${path}`;
16
- const headers = (routeHeaders) => ({
18
+ const headers = async (routeHeaders) => ({
17
19
  "Content-Type": "application/json",
18
- ...config.commonHeaders,
20
+ ...await resolveHeaders(config.commonHeaders),
19
21
  ...routeHeaders
20
22
  });
21
23
  const requestFailed = async (response) => {
@@ -107,7 +109,7 @@ const createStandaloneCoreApi = (config) => {
107
109
  const http = createStandaloneHttp(config);
108
110
  let checked;
109
111
  const checkProtocol = async () => {
110
- const response = await fetch(http.buildUrl("/version"), { headers: http.headers({ "Cache-Control": "no-cache" }) });
112
+ const response = await fetch(http.buildUrl("/version"), { headers: await http.headers({ "Cache-Control": "no-cache" }) });
111
113
  const body = response.status === 404 ? {} : await http.parseJson(response);
112
114
  const protocol = isRecord(body) ? body.adminProtocol : void 0;
113
115
  if (typeof protocol !== "number" || protocol < 2) throw new StandaloneDatabaseError("request-failed", `The server at ${config.baseUrl} does not report Hot Updater admin API protocol 2 at /version. Set baseUrl to the path where the server mounts handlers.admin, such as https://example.com/hot-updater/admin.`, response.status);
@@ -124,7 +126,7 @@ const createStandaloneCoreApi = (config) => {
124
126
  for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
125
127
  const response = await fetch(url, {
126
128
  method,
127
- headers: http.headers(method === "GET" ? { "Cache-Control": "no-cache" } : void 0),
129
+ headers: await http.headers(method === "GET" ? { "Cache-Control": "no-cache" } : void 0),
128
130
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
129
131
  });
130
132
  if (response.status === 204) return null;
@@ -251,8 +253,8 @@ const standaloneRepository = (config) => {
251
253
  return Object.freeze({
252
254
  name: "standalone-repository",
253
255
  core: createStandaloneCoreApi(config),
254
- fetchAdmin: (path, init = {}) => {
255
- const headers = new Headers(http.headers({ "Cache-Control": "no-cache" }));
256
+ fetchAdmin: async (path, init = {}) => {
257
+ const headers = new Headers(await http.headers({ "Cache-Control": "no-cache" }));
256
258
  new Headers(init.headers).forEach((value, name) => {
257
259
  headers.set(name, value);
258
260
  });
@@ -269,8 +271,12 @@ const defaultRoutes = {
269
271
  put: () => ({ path: "/upload" }),
270
272
  get: () => ({ path: "/get" }),
271
273
  exists: () => ({ path: "/exists" }),
272
- delete: () => ({ path: "/delete" })
274
+ delete: () => ({ path: "/delete" }),
275
+ listObjects: () => ({ path: "/list" }),
276
+ deleteObjects: () => ({ path: "/delete-objects" })
273
277
  };
278
+ /** Keys sent in one `deleteObjects` request. */
279
+ const DELETE_BATCH_SIZE = 1e3;
274
280
  const createRoute = (defaultRoute, customRoute) => ({
275
281
  path: customRoute?.path ?? defaultRoute.path,
276
282
  headers: {
@@ -279,8 +285,8 @@ const createRoute = (defaultRoute, customRoute) => ({
279
285
  }
280
286
  });
281
287
  const standaloneStorage = (config) => {
282
- const getHeaders = (routeHeaders) => ({
283
- ...config.commonHeaders,
288
+ const getHeaders = async (routeHeaders) => ({
289
+ ...await resolveHeaders(config.commonHeaders),
284
290
  ...routeHeaders
285
291
  });
286
292
  const resolveRoute = (name, args) => {
@@ -288,6 +294,14 @@ const standaloneStorage = (config) => {
288
294
  const custom = config.routes?.[name];
289
295
  return createRoute(fallback(...args), custom?.(...args));
290
296
  };
297
+ const postJson = async (path, headers, body) => fetch(`${config.baseUrl}${path}`, {
298
+ method: "POST",
299
+ headers: {
300
+ "content-type": "application/json",
301
+ ...await getHeaders(headers)
302
+ },
303
+ body: JSON.stringify(body)
304
+ });
291
305
  const requestStorageUri = async (routeName, storageUri) => {
292
306
  parseStorageUri(storageUri, config.protocol);
293
307
  const { path, headers } = resolveRoute(routeName, [{ storageUri }]);
@@ -295,7 +309,7 @@ const standaloneStorage = (config) => {
295
309
  method: routeName === "delete" ? "DELETE" : "POST",
296
310
  headers: {
297
311
  "content-type": "application/json",
298
- ...getHeaders(headers)
312
+ ...await getHeaders(headers)
299
313
  },
300
314
  body: JSON.stringify({ storageUri })
301
315
  });
@@ -318,7 +332,7 @@ const standaloneStorage = (config) => {
318
332
  formData.append("key", key);
319
333
  const response = await fetch(`${config.baseUrl}${path}`, {
320
334
  method: "POST",
321
- headers: getHeaders(headers),
335
+ headers: await getHeaders(headers),
322
336
  body: formData
323
337
  });
324
338
  if (!response.ok) throw new Error(`Failed to upload bundle: ${response.statusText}`);
@@ -339,6 +353,39 @@ const standaloneStorage = (config) => {
339
353
  async delete({ storageUri }) {
340
354
  await requestStorageUri("delete", storageUri);
341
355
  return { deleted: true };
356
+ },
357
+ async listObjects(prefix) {
358
+ const { path, headers } = resolveRoute("listObjects", [{ prefix }]);
359
+ const objects = [];
360
+ let cursor;
361
+ do {
362
+ const response = await postJson(path, headers, {
363
+ prefix,
364
+ cursor
365
+ });
366
+ if (response.status === 404) throw new Error(`The storage service has no ${path} route, which storage prune needs.`);
367
+ if (!response.ok) throw new Error(`Failed to list storage objects: ${response.statusText}`);
368
+ const page = await response.json();
369
+ for (const object of page.objects) {
370
+ parseStorageUri(object.storageUri, config.protocol);
371
+ objects.push({
372
+ key: object.key,
373
+ storageUri: object.storageUri,
374
+ size: object.size,
375
+ ...object.lastModifiedAt === void 0 ? {} : { lastModifiedAt: new Date(object.lastModifiedAt) }
376
+ });
377
+ }
378
+ cursor = page.cursor;
379
+ } while (cursor !== void 0);
380
+ return objects;
381
+ },
382
+ async deleteObjects(keys) {
383
+ for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
384
+ const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
385
+ const { path, headers } = resolveRoute("deleteObjects", [{ keys: batch }]);
386
+ const response = await postJson(path, headers, { keys: batch });
387
+ if (!response.ok) throw new Error(`Failed to delete storage objects: ${response.statusText}`);
388
+ }
342
389
  }
343
390
  });
344
391
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/standalone",
3
- "version": "1.2.0-rc.7",
3
+ "version": "1.2.0-rc.9",
4
4
  "type": "module",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "sideEffects": false,
@@ -42,8 +42,8 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@hot-updater/protocol": "1.2.0-rc.7",
46
- "@hot-updater/plugin-core": "1.2.0-rc.7"
45
+ "@hot-updater/protocol": "1.2.0-rc.9",
46
+ "@hot-updater/plugin-core": "1.2.0-rc.9"
47
47
  },
48
48
  "devDependencies": {
49
49
  "mime": "2.6.0",
@@ -52,12 +52,7 @@
52
52
  "msw": "^2.7.0",
53
53
  "verkit": "0.4.0",
54
54
  "vitest": "4.1.4",
55
- "@hot-updater/test-utils": "1.2.0-rc.7"
56
- },
57
- "nx": {
58
- "implicitDependencies": [
59
- "!@hot-updater/test-utils"
60
- ]
55
+ "@hot-updater/test-utils": "1.2.0-rc.9"
61
56
  },
62
57
  "scripts": {
63
58
  "build": "tsdown",