@hot-updater/protocol 1.0.0-rc.40 → 1.0.0-rc.41

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
@@ -170,7 +170,7 @@ const MAX_STORAGE_VALUE_LENGTH = 65536;
170
170
  const pluginStorageKey = (pluginId, key) => `plugins/${encodeURIComponent(pluginId)}/${key}`;
171
171
  const createPluginHost = (environment) => {
172
172
  let config = null;
173
- /** Hooks by plugin, so a plugin configured again is not set up twice. */
173
+ /** Hooks and API by plugin, so a plugin configured again is not set up twice. */
174
174
  const setUpPlugins = /* @__PURE__ */ new WeakMap();
175
175
  let activePlugins = [];
176
176
  /** What plugins read and wrote, so a read follows a write before native does. */
@@ -186,7 +186,7 @@ const createPluginHost = (environment) => {
186
186
  console.warn(report.message, error);
187
187
  };
188
188
  const fetchFromServer = async (path, init = {}) => {
189
- if (config === null) throw new Error("[HotUpdater] Plugins can fetch only after init or wrap.");
189
+ if (config === null) throw new Error("[HotUpdater] Plugins can fetch only after HotUpdater.init.");
190
190
  if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//")) throw new TypeError("[HotUpdater] Plugin fetch takes a path relative to baseURL.");
191
191
  const { requestHeaders, requestTimeout } = config;
192
192
  const url = `${await resolveBaseURL(config.baseURL)}/${path.replace(/^\/+/, "")}`;
@@ -264,6 +264,28 @@ const createPluginHost = (environment) => {
264
264
  storage: createStorage(pluginId),
265
265
  now: () => environment.now()
266
266
  });
267
+ const setUpPlugin = (plugin) => {
268
+ let result;
269
+ try {
270
+ result = plugin.setup(createContext(plugin.id));
271
+ } catch (error) {
272
+ reportHookError(`Plugin "${plugin.id}" failed in setup`, error);
273
+ return {
274
+ hooks: {},
275
+ api: void 0
276
+ };
277
+ }
278
+ if (result === void 0 || result === null) return {
279
+ hooks: {},
280
+ api: void 0
281
+ };
282
+ const unknownKeys = Object.keys(result).filter((key) => key !== "hooks" && key !== "api");
283
+ if (unknownKeys.length > 0) reportHookError(`Plugin "${plugin.id}" setup returned ${unknownKeys.map((key) => `"${key}"`).join(", ")}; setup returns { hooks, api }`, /* @__PURE__ */ new TypeError("Unexpected setup result"));
284
+ return {
285
+ hooks: result.hooks ?? {},
286
+ api: result.api
287
+ };
288
+ };
267
289
  const configurePlugins = (plugins, nextConfig) => {
268
290
  const ids = /* @__PURE__ */ new Set();
269
291
  for (const plugin of plugins ?? []) {
@@ -272,22 +294,20 @@ const createPluginHost = (environment) => {
272
294
  ids.add(plugin.id);
273
295
  }
274
296
  config = nextConfig;
297
+ const apis = [];
275
298
  activePlugins = (plugins ?? []).map((plugin) => {
276
- let hooks = setUpPlugins.get(plugin);
277
- if (hooks === void 0) {
278
- try {
279
- hooks = plugin.setup(createContext(plugin.id)) ?? {};
280
- } catch (error) {
281
- reportHookError(`Plugin "${plugin.id}" failed in setup`, error);
282
- hooks = {};
283
- }
284
- setUpPlugins.set(plugin, hooks);
299
+ let setUp = setUpPlugins.get(plugin);
300
+ if (setUp === void 0) {
301
+ setUp = setUpPlugin(plugin);
302
+ setUpPlugins.set(plugin, setUp);
285
303
  }
304
+ if (setUp.api !== void 0) apis.push([plugin.id, setUp.api]);
286
305
  return {
287
306
  plugin,
288
- hooks
307
+ hooks: setUp.hooks
289
308
  };
290
309
  });
310
+ return Object.freeze(Object.fromEntries(apis));
291
311
  };
292
312
  const hasPluginHook = (name) => activePlugins.some(({ hooks }) => typeof hooks[name] === "function");
293
313
  const buildPluginEvent = (name, createPayload) => {
package/dist/index.d.cts CHANGED
@@ -247,10 +247,10 @@ interface UpdateHttpResponse {
247
247
  readonly bodyTruncated: boolean;
248
248
  }
249
249
  /**
250
- * Lifecycle hooks a plugin returns from `setup`. Hooks observe: the SDK
251
- * never waits for one and ignores what it returns, and a hook that throws or
252
- * rejects is reported through `onError`, or a console warning without one,
253
- * so a plugin can never delay or change an update.
250
+ * Lifecycle hooks a plugin returns from `setup` as `hooks`. Hooks observe:
251
+ * the SDK never waits for one and ignores what it returns, and a hook that
252
+ * throws or rejects is reported through `onError`, or a console warning
253
+ * without one, so a plugin can never delay or change an update.
254
254
  */
255
255
  interface HotUpdaterClientHooks {
256
256
  /** Called once per JavaScript runtime, after native reports the launch. */
@@ -303,22 +303,46 @@ interface HotUpdaterClientContext {
303
303
  /** The current time in milliseconds since the epoch. */
304
304
  now(): number;
305
305
  }
306
- interface HotUpdaterClientPlugin {
307
- /** Unique among an app's plugins; two plugins with one id throw at init. */
308
- readonly id: string;
306
+ /**
307
+ * What a plugin's `setup` returns: the hooks it observes the SDK with, and
308
+ * the API `HotUpdater.init` puts on the instance it returns, under the
309
+ * plugin's id, as `hotUpdater.<id>`.
310
+ */
311
+ interface HotUpdaterClientSetup<TApi = unknown> {
312
+ readonly hooks?: HotUpdaterClientHooks;
313
+ readonly api?: TApi;
314
+ }
315
+ /**
316
+ * A client plugin with the id `TId` and the API `TApi` it adds to the
317
+ * instance `HotUpdater.init` returns.
318
+ */
319
+ interface HotUpdaterClientPlugin<TId extends string = string, TApi = unknown> {
320
+ /**
321
+ * Unique among an app's plugins; two plugins with one id throw at init.
322
+ * It names the plugin's API on the instance, so it cannot be the name of
323
+ * one of the instance's own methods.
324
+ */
325
+ readonly id: TId;
309
326
  /**
310
- * Called once, when `HotUpdater.init` or `HotUpdater.wrap` first
311
- * configures the runtime with this plugin.
327
+ * Called once for each plugin host it is configured in. The React Native
328
+ * SDK creates a host for each instance `HotUpdater.init` returns.
312
329
  */
313
- setup(context: HotUpdaterClientContext): HotUpdaterClientHooks | void;
330
+ setup(context: HotUpdaterClientContext): HotUpdaterClientSetup<TApi> | void;
314
331
  }
332
+ type SetupResult<P> = P extends {
333
+ setup(context: HotUpdaterClientContext): infer R;
334
+ } ? Exclude<R, void | undefined> : never;
335
+ /** The API a plugin's `setup` returns, or `never` for a plugin without one. */
336
+ type ClientPluginApi<P> = "api" extends keyof SetupResult<P> ? Exclude<SetupResult<P>["api" & keyof SetupResult<P>], undefined> : never;
337
+ /** The plugins' APIs by plugin id, as the instance `HotUpdater.init` returns has them. */
338
+ type ClientPluginApis<TPlugins extends readonly HotUpdaterClientPlugin[]> = { readonly [P in TPlugins[number] as [ClientPluginApi<P>] extends [never] ? never : P["id"]]: ClientPluginApi<P>; };
315
339
  /** Declares a client plugin; built-in and third-party plugins use the same contract. */
316
340
  declare const defineClientPlugin: <const P extends HotUpdaterClientPlugin>(plugin: P) => P;
317
341
  //#endregion
318
342
  //#region src/createPluginHost.d.ts
319
343
  type PluginHookName = keyof HotUpdaterClientHooks;
320
344
  type PluginHookPayload<K extends PluginHookName> = Parameters<NonNullable<HotUpdaterClientHooks[K]>>[0];
321
- /** The init or wrap settings plugins see through their context. */
345
+ /** The init settings plugins see through their context. */
322
346
  interface PluginHostConfig {
323
347
  readonly baseURL: HotUpdaterBaseURL;
324
348
  readonly requestHeaders?: Record<string, string>;
@@ -348,11 +372,11 @@ interface PluginHostEnvironment {
348
372
  }
349
373
  interface PluginHost {
350
374
  /**
351
- * Sets up the plugins of an init or wrap call. A plugin passed to an
352
- * earlier call keeps its hooks, and a plugin left out stops receiving
353
- * events.
375
+ * Sets up the plugins of an init call and returns their APIs by plugin
376
+ * id. A plugin passed to an earlier call keeps its hooks and its API, and
377
+ * a plugin left out stops receiving events.
354
378
  */
355
- configurePlugins(plugins: readonly HotUpdaterClientPlugin[] | undefined, config: PluginHostConfig): void;
379
+ configurePlugins(plugins: readonly HotUpdaterClientPlugin[] | undefined, config: PluginHostConfig): Readonly<Record<string, unknown>>;
356
380
  /** Whether any plugin listens to this hook, so callers build events only when one does. */
357
381
  hasPluginHook(name: PluginHookName): boolean;
358
382
  /**
@@ -552,4 +576,4 @@ declare function isCohortEligibleForUpdate(bundleId: string, cohort: string | nu
552
576
  declare const NIL_UUID = "00000000-0000-0000-0000-000000000000";
553
577
  declare const isUUIDv7: (value: unknown) => value is string;
554
578
  //#endregion
555
- export { ARTIFACT_PROTOCOL_VERSION, AppReadyResult, ArtifactAsset, ArtifactAssetFile, ArtifactInfo, Bundle, BundleDownloadedInfo, BundleMetadata, BundlePatchArtifact, CatalogAcceptance, CatalogHighWater, ChangedAssetPatch, DEFAULT_ROLLOUT_COHORT_COUNT, DesiredRelease, ExpectedReleaseCatalogScope, HotUpdaterBaseURL, HotUpdaterClientContext, HotUpdaterClientHooks, HotUpdaterClientPlugin, HotUpdaterClientStorage, INVALID_COHORT_ERROR_MESSAGE, MAX_COHORT_LENGTH, MAX_COMPILED_CATALOG_BYTES, MAX_CRASHED_BUNDLES, MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE, MAX_RELEASE_CATALOG_WIRE_BYTES, MAX_TARGET_COHORTS_PER_RELEASE, ManifestArchive, NIL_UUID, NUMERIC_COHORT_SIZE, PersistedSelectionReceipt, Platform, PluginHookName, PluginHookPayload, PluginHost, PluginHostConfig, PluginHostEnvironment, RELEASE_CATALOG_FALLBACK_POLICY, RELEASE_CATALOG_SCHEMA_VERSION, Release, ReleaseCatalog, ReleaseCatalogDescriptor, ReleaseCatalogScopeKeyInput, ReleaseKind, ReleaseOperation, ReleaseSelectionContextInput, ReleaseSelectionInput, ReleaseStrategy, ReleaseTransitionAuthorization, ReleaseTransitionKind, SelectionKind, UpdateCheckResult, UpdateError, UpdateErrorReason, UpdateErrorResource, UpdateErrorStage, UpdateErrorTransport, UpdateHttpResponse, UpdateStatus, UpdateStrategy, assertCanonicalChannelName, assessCatalogAcceptance, authorizeReleaseTransition, canonicalizeAppVersion, createPluginHost, createReleaseCatalogScopeKey, createReleaseSelectionContextHash, decodeChannelKey, defineClientPlugin, encodeChannelKey, getAssetBaseStorageUri, getBundlePatch, getBundlePatches, getDefaultNumericCohort, getManifestFileHash, getManifestStorageUri, getNumericCohortRolloutPosition, getNumericCohortValue, getPatchBaseBundleId, getPatchBaseFileHash, getPatchFileHash, getPatchStorageUri, getRolledOutNumericCohorts, getUtf8ByteLength, hasExpectedReleaseCatalogScope, isCohortEligibleForUpdate, isCustomCohort, isNumericCohort, isReleaseEligibleForCohort, isUUIDv7, isValidCohort, normalizeChannelName, normalizeCohortValue, normalizeRolloutCohortCount, parseReleaseCatalog, parseReleaseCatalogScopeKey, pluginStorageKey, resolveBaseURL, selectDesiredRelease, stripBundleArtifactMetadata };
579
+ export { ARTIFACT_PROTOCOL_VERSION, AppReadyResult, ArtifactAsset, ArtifactAssetFile, ArtifactInfo, Bundle, BundleDownloadedInfo, BundleMetadata, BundlePatchArtifact, CatalogAcceptance, CatalogHighWater, ChangedAssetPatch, ClientPluginApi, ClientPluginApis, DEFAULT_ROLLOUT_COHORT_COUNT, DesiredRelease, ExpectedReleaseCatalogScope, HotUpdaterBaseURL, HotUpdaterClientContext, HotUpdaterClientHooks, HotUpdaterClientPlugin, HotUpdaterClientSetup, HotUpdaterClientStorage, INVALID_COHORT_ERROR_MESSAGE, MAX_COHORT_LENGTH, MAX_COMPILED_CATALOG_BYTES, MAX_CRASHED_BUNDLES, MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE, MAX_RELEASE_CATALOG_WIRE_BYTES, MAX_TARGET_COHORTS_PER_RELEASE, ManifestArchive, NIL_UUID, NUMERIC_COHORT_SIZE, PersistedSelectionReceipt, Platform, PluginHookName, PluginHookPayload, PluginHost, PluginHostConfig, PluginHostEnvironment, RELEASE_CATALOG_FALLBACK_POLICY, RELEASE_CATALOG_SCHEMA_VERSION, Release, ReleaseCatalog, ReleaseCatalogDescriptor, ReleaseCatalogScopeKeyInput, ReleaseKind, ReleaseOperation, ReleaseSelectionContextInput, ReleaseSelectionInput, ReleaseStrategy, ReleaseTransitionAuthorization, ReleaseTransitionKind, SelectionKind, UpdateCheckResult, UpdateError, UpdateErrorReason, UpdateErrorResource, UpdateErrorStage, UpdateErrorTransport, UpdateHttpResponse, UpdateStatus, UpdateStrategy, assertCanonicalChannelName, assessCatalogAcceptance, authorizeReleaseTransition, canonicalizeAppVersion, createPluginHost, createReleaseCatalogScopeKey, createReleaseSelectionContextHash, decodeChannelKey, defineClientPlugin, encodeChannelKey, getAssetBaseStorageUri, getBundlePatch, getBundlePatches, getDefaultNumericCohort, getManifestFileHash, getManifestStorageUri, getNumericCohortRolloutPosition, getNumericCohortValue, getPatchBaseBundleId, getPatchBaseFileHash, getPatchFileHash, getPatchStorageUri, getRolledOutNumericCohorts, getUtf8ByteLength, hasExpectedReleaseCatalogScope, isCohortEligibleForUpdate, isCustomCohort, isNumericCohort, isReleaseEligibleForCohort, isUUIDv7, isValidCohort, normalizeChannelName, normalizeCohortValue, normalizeRolloutCohortCount, parseReleaseCatalog, parseReleaseCatalogScopeKey, pluginStorageKey, resolveBaseURL, selectDesiredRelease, stripBundleArtifactMetadata };
package/dist/index.d.mts CHANGED
@@ -247,10 +247,10 @@ interface UpdateHttpResponse {
247
247
  readonly bodyTruncated: boolean;
248
248
  }
249
249
  /**
250
- * Lifecycle hooks a plugin returns from `setup`. Hooks observe: the SDK
251
- * never waits for one and ignores what it returns, and a hook that throws or
252
- * rejects is reported through `onError`, or a console warning without one,
253
- * so a plugin can never delay or change an update.
250
+ * Lifecycle hooks a plugin returns from `setup` as `hooks`. Hooks observe:
251
+ * the SDK never waits for one and ignores what it returns, and a hook that
252
+ * throws or rejects is reported through `onError`, or a console warning
253
+ * without one, so a plugin can never delay or change an update.
254
254
  */
255
255
  interface HotUpdaterClientHooks {
256
256
  /** Called once per JavaScript runtime, after native reports the launch. */
@@ -303,22 +303,46 @@ interface HotUpdaterClientContext {
303
303
  /** The current time in milliseconds since the epoch. */
304
304
  now(): number;
305
305
  }
306
- interface HotUpdaterClientPlugin {
307
- /** Unique among an app's plugins; two plugins with one id throw at init. */
308
- readonly id: string;
306
+ /**
307
+ * What a plugin's `setup` returns: the hooks it observes the SDK with, and
308
+ * the API `HotUpdater.init` puts on the instance it returns, under the
309
+ * plugin's id, as `hotUpdater.<id>`.
310
+ */
311
+ interface HotUpdaterClientSetup<TApi = unknown> {
312
+ readonly hooks?: HotUpdaterClientHooks;
313
+ readonly api?: TApi;
314
+ }
315
+ /**
316
+ * A client plugin with the id `TId` and the API `TApi` it adds to the
317
+ * instance `HotUpdater.init` returns.
318
+ */
319
+ interface HotUpdaterClientPlugin<TId extends string = string, TApi = unknown> {
320
+ /**
321
+ * Unique among an app's plugins; two plugins with one id throw at init.
322
+ * It names the plugin's API on the instance, so it cannot be the name of
323
+ * one of the instance's own methods.
324
+ */
325
+ readonly id: TId;
309
326
  /**
310
- * Called once, when `HotUpdater.init` or `HotUpdater.wrap` first
311
- * configures the runtime with this plugin.
327
+ * Called once for each plugin host it is configured in. The React Native
328
+ * SDK creates a host for each instance `HotUpdater.init` returns.
312
329
  */
313
- setup(context: HotUpdaterClientContext): HotUpdaterClientHooks | void;
330
+ setup(context: HotUpdaterClientContext): HotUpdaterClientSetup<TApi> | void;
314
331
  }
332
+ type SetupResult<P> = P extends {
333
+ setup(context: HotUpdaterClientContext): infer R;
334
+ } ? Exclude<R, void | undefined> : never;
335
+ /** The API a plugin's `setup` returns, or `never` for a plugin without one. */
336
+ type ClientPluginApi<P> = "api" extends keyof SetupResult<P> ? Exclude<SetupResult<P>["api" & keyof SetupResult<P>], undefined> : never;
337
+ /** The plugins' APIs by plugin id, as the instance `HotUpdater.init` returns has them. */
338
+ type ClientPluginApis<TPlugins extends readonly HotUpdaterClientPlugin[]> = { readonly [P in TPlugins[number] as [ClientPluginApi<P>] extends [never] ? never : P["id"]]: ClientPluginApi<P>; };
315
339
  /** Declares a client plugin; built-in and third-party plugins use the same contract. */
316
340
  declare const defineClientPlugin: <const P extends HotUpdaterClientPlugin>(plugin: P) => P;
317
341
  //#endregion
318
342
  //#region src/createPluginHost.d.ts
319
343
  type PluginHookName = keyof HotUpdaterClientHooks;
320
344
  type PluginHookPayload<K extends PluginHookName> = Parameters<NonNullable<HotUpdaterClientHooks[K]>>[0];
321
- /** The init or wrap settings plugins see through their context. */
345
+ /** The init settings plugins see through their context. */
322
346
  interface PluginHostConfig {
323
347
  readonly baseURL: HotUpdaterBaseURL;
324
348
  readonly requestHeaders?: Record<string, string>;
@@ -348,11 +372,11 @@ interface PluginHostEnvironment {
348
372
  }
349
373
  interface PluginHost {
350
374
  /**
351
- * Sets up the plugins of an init or wrap call. A plugin passed to an
352
- * earlier call keeps its hooks, and a plugin left out stops receiving
353
- * events.
375
+ * Sets up the plugins of an init call and returns their APIs by plugin
376
+ * id. A plugin passed to an earlier call keeps its hooks and its API, and
377
+ * a plugin left out stops receiving events.
354
378
  */
355
- configurePlugins(plugins: readonly HotUpdaterClientPlugin[] | undefined, config: PluginHostConfig): void;
379
+ configurePlugins(plugins: readonly HotUpdaterClientPlugin[] | undefined, config: PluginHostConfig): Readonly<Record<string, unknown>>;
356
380
  /** Whether any plugin listens to this hook, so callers build events only when one does. */
357
381
  hasPluginHook(name: PluginHookName): boolean;
358
382
  /**
@@ -552,4 +576,4 @@ declare function isCohortEligibleForUpdate(bundleId: string, cohort: string | nu
552
576
  declare const NIL_UUID = "00000000-0000-0000-0000-000000000000";
553
577
  declare const isUUIDv7: (value: unknown) => value is string;
554
578
  //#endregion
555
- export { ARTIFACT_PROTOCOL_VERSION, AppReadyResult, ArtifactAsset, ArtifactAssetFile, ArtifactInfo, Bundle, BundleDownloadedInfo, BundleMetadata, BundlePatchArtifact, CatalogAcceptance, CatalogHighWater, ChangedAssetPatch, DEFAULT_ROLLOUT_COHORT_COUNT, DesiredRelease, ExpectedReleaseCatalogScope, HotUpdaterBaseURL, HotUpdaterClientContext, HotUpdaterClientHooks, HotUpdaterClientPlugin, HotUpdaterClientStorage, INVALID_COHORT_ERROR_MESSAGE, MAX_COHORT_LENGTH, MAX_COMPILED_CATALOG_BYTES, MAX_CRASHED_BUNDLES, MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE, MAX_RELEASE_CATALOG_WIRE_BYTES, MAX_TARGET_COHORTS_PER_RELEASE, ManifestArchive, NIL_UUID, NUMERIC_COHORT_SIZE, PersistedSelectionReceipt, Platform, PluginHookName, PluginHookPayload, PluginHost, PluginHostConfig, PluginHostEnvironment, RELEASE_CATALOG_FALLBACK_POLICY, RELEASE_CATALOG_SCHEMA_VERSION, Release, ReleaseCatalog, ReleaseCatalogDescriptor, ReleaseCatalogScopeKeyInput, ReleaseKind, ReleaseOperation, ReleaseSelectionContextInput, ReleaseSelectionInput, ReleaseStrategy, ReleaseTransitionAuthorization, ReleaseTransitionKind, SelectionKind, UpdateCheckResult, UpdateError, UpdateErrorReason, UpdateErrorResource, UpdateErrorStage, UpdateErrorTransport, UpdateHttpResponse, UpdateStatus, UpdateStrategy, assertCanonicalChannelName, assessCatalogAcceptance, authorizeReleaseTransition, canonicalizeAppVersion, createPluginHost, createReleaseCatalogScopeKey, createReleaseSelectionContextHash, decodeChannelKey, defineClientPlugin, encodeChannelKey, getAssetBaseStorageUri, getBundlePatch, getBundlePatches, getDefaultNumericCohort, getManifestFileHash, getManifestStorageUri, getNumericCohortRolloutPosition, getNumericCohortValue, getPatchBaseBundleId, getPatchBaseFileHash, getPatchFileHash, getPatchStorageUri, getRolledOutNumericCohorts, getUtf8ByteLength, hasExpectedReleaseCatalogScope, isCohortEligibleForUpdate, isCustomCohort, isNumericCohort, isReleaseEligibleForCohort, isUUIDv7, isValidCohort, normalizeChannelName, normalizeCohortValue, normalizeRolloutCohortCount, parseReleaseCatalog, parseReleaseCatalogScopeKey, pluginStorageKey, resolveBaseURL, selectDesiredRelease, stripBundleArtifactMetadata };
579
+ export { ARTIFACT_PROTOCOL_VERSION, AppReadyResult, ArtifactAsset, ArtifactAssetFile, ArtifactInfo, Bundle, BundleDownloadedInfo, BundleMetadata, BundlePatchArtifact, CatalogAcceptance, CatalogHighWater, ChangedAssetPatch, ClientPluginApi, ClientPluginApis, DEFAULT_ROLLOUT_COHORT_COUNT, DesiredRelease, ExpectedReleaseCatalogScope, HotUpdaterBaseURL, HotUpdaterClientContext, HotUpdaterClientHooks, HotUpdaterClientPlugin, HotUpdaterClientSetup, HotUpdaterClientStorage, INVALID_COHORT_ERROR_MESSAGE, MAX_COHORT_LENGTH, MAX_COMPILED_CATALOG_BYTES, MAX_CRASHED_BUNDLES, MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE, MAX_RELEASE_CATALOG_WIRE_BYTES, MAX_TARGET_COHORTS_PER_RELEASE, ManifestArchive, NIL_UUID, NUMERIC_COHORT_SIZE, PersistedSelectionReceipt, Platform, PluginHookName, PluginHookPayload, PluginHost, PluginHostConfig, PluginHostEnvironment, RELEASE_CATALOG_FALLBACK_POLICY, RELEASE_CATALOG_SCHEMA_VERSION, Release, ReleaseCatalog, ReleaseCatalogDescriptor, ReleaseCatalogScopeKeyInput, ReleaseKind, ReleaseOperation, ReleaseSelectionContextInput, ReleaseSelectionInput, ReleaseStrategy, ReleaseTransitionAuthorization, ReleaseTransitionKind, SelectionKind, UpdateCheckResult, UpdateError, UpdateErrorReason, UpdateErrorResource, UpdateErrorStage, UpdateErrorTransport, UpdateHttpResponse, UpdateStatus, UpdateStrategy, assertCanonicalChannelName, assessCatalogAcceptance, authorizeReleaseTransition, canonicalizeAppVersion, createPluginHost, createReleaseCatalogScopeKey, createReleaseSelectionContextHash, decodeChannelKey, defineClientPlugin, encodeChannelKey, getAssetBaseStorageUri, getBundlePatch, getBundlePatches, getDefaultNumericCohort, getManifestFileHash, getManifestStorageUri, getNumericCohortRolloutPosition, getNumericCohortValue, getPatchBaseBundleId, getPatchBaseFileHash, getPatchFileHash, getPatchStorageUri, getRolledOutNumericCohorts, getUtf8ByteLength, hasExpectedReleaseCatalogScope, isCohortEligibleForUpdate, isCustomCohort, isNumericCohort, isReleaseEligibleForCohort, isUUIDv7, isValidCohort, normalizeChannelName, normalizeCohortValue, normalizeRolloutCohortCount, parseReleaseCatalog, parseReleaseCatalogScopeKey, pluginStorageKey, resolveBaseURL, selectDesiredRelease, stripBundleArtifactMetadata };
package/dist/index.mjs CHANGED
@@ -169,7 +169,7 @@ const MAX_STORAGE_VALUE_LENGTH = 65536;
169
169
  const pluginStorageKey = (pluginId, key) => `plugins/${encodeURIComponent(pluginId)}/${key}`;
170
170
  const createPluginHost = (environment) => {
171
171
  let config = null;
172
- /** Hooks by plugin, so a plugin configured again is not set up twice. */
172
+ /** Hooks and API by plugin, so a plugin configured again is not set up twice. */
173
173
  const setUpPlugins = /* @__PURE__ */ new WeakMap();
174
174
  let activePlugins = [];
175
175
  /** What plugins read and wrote, so a read follows a write before native does. */
@@ -185,7 +185,7 @@ const createPluginHost = (environment) => {
185
185
  console.warn(report.message, error);
186
186
  };
187
187
  const fetchFromServer = async (path, init = {}) => {
188
- if (config === null) throw new Error("[HotUpdater] Plugins can fetch only after init or wrap.");
188
+ if (config === null) throw new Error("[HotUpdater] Plugins can fetch only after HotUpdater.init.");
189
189
  if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//")) throw new TypeError("[HotUpdater] Plugin fetch takes a path relative to baseURL.");
190
190
  const { requestHeaders, requestTimeout } = config;
191
191
  const url = `${await resolveBaseURL(config.baseURL)}/${path.replace(/^\/+/, "")}`;
@@ -263,6 +263,28 @@ const createPluginHost = (environment) => {
263
263
  storage: createStorage(pluginId),
264
264
  now: () => environment.now()
265
265
  });
266
+ const setUpPlugin = (plugin) => {
267
+ let result;
268
+ try {
269
+ result = plugin.setup(createContext(plugin.id));
270
+ } catch (error) {
271
+ reportHookError(`Plugin "${plugin.id}" failed in setup`, error);
272
+ return {
273
+ hooks: {},
274
+ api: void 0
275
+ };
276
+ }
277
+ if (result === void 0 || result === null) return {
278
+ hooks: {},
279
+ api: void 0
280
+ };
281
+ const unknownKeys = Object.keys(result).filter((key) => key !== "hooks" && key !== "api");
282
+ if (unknownKeys.length > 0) reportHookError(`Plugin "${plugin.id}" setup returned ${unknownKeys.map((key) => `"${key}"`).join(", ")}; setup returns { hooks, api }`, /* @__PURE__ */ new TypeError("Unexpected setup result"));
283
+ return {
284
+ hooks: result.hooks ?? {},
285
+ api: result.api
286
+ };
287
+ };
266
288
  const configurePlugins = (plugins, nextConfig) => {
267
289
  const ids = /* @__PURE__ */ new Set();
268
290
  for (const plugin of plugins ?? []) {
@@ -271,22 +293,20 @@ const createPluginHost = (environment) => {
271
293
  ids.add(plugin.id);
272
294
  }
273
295
  config = nextConfig;
296
+ const apis = [];
274
297
  activePlugins = (plugins ?? []).map((plugin) => {
275
- let hooks = setUpPlugins.get(plugin);
276
- if (hooks === void 0) {
277
- try {
278
- hooks = plugin.setup(createContext(plugin.id)) ?? {};
279
- } catch (error) {
280
- reportHookError(`Plugin "${plugin.id}" failed in setup`, error);
281
- hooks = {};
282
- }
283
- setUpPlugins.set(plugin, hooks);
298
+ let setUp = setUpPlugins.get(plugin);
299
+ if (setUp === void 0) {
300
+ setUp = setUpPlugin(plugin);
301
+ setUpPlugins.set(plugin, setUp);
284
302
  }
303
+ if (setUp.api !== void 0) apis.push([plugin.id, setUp.api]);
285
304
  return {
286
305
  plugin,
287
- hooks
306
+ hooks: setUp.hooks
288
307
  };
289
308
  });
309
+ return Object.freeze(Object.fromEntries(apis));
290
310
  };
291
311
  const hasPluginHook = (name) => activePlugins.some(({ hooks }) => typeof hooks[name] === "function");
292
312
  const buildPluginEvent = (name, createPayload) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/protocol",
3
- "version": "1.0.0-rc.40",
3
+ "version": "1.0.0-rc.41",
4
4
  "type": "module",
5
5
  "description": "The data formats and pure computations that Hot Updater's app and server share",
6
6
  "sideEffects": false,