@blac/core 2.0.22 → 2.1.1

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.
Files changed (76) hide show
  1. package/dist/StateContainer-BUyuxKb3.cjs +667 -0
  2. package/dist/StateContainer-BUyuxKb3.cjs.map +1 -0
  3. package/dist/StateContainer-B_7UHc3I.js +632 -0
  4. package/dist/StateContainer-B_7UHc3I.js.map +1 -0
  5. package/dist/StateContainer-Bh2ipNYj.js +632 -0
  6. package/dist/StateContainer-Bh2ipNYj.js.map +1 -0
  7. package/dist/StateContainer-ClzwKnQZ.cjs +667 -0
  8. package/dist/StateContainer-ClzwKnQZ.cjs.map +1 -0
  9. package/dist/StateContainer-DiQATvZh.js +632 -0
  10. package/dist/StateContainer-DiQATvZh.js.map +1 -0
  11. package/dist/StateContainer-j16zciex.cjs +667 -0
  12. package/dist/StateContainer-j16zciex.cjs.map +1 -0
  13. package/dist/StateContainerRegistry-BX-DlHXO.js +747 -0
  14. package/dist/StateContainerRegistry-BX-DlHXO.js.map +1 -0
  15. package/dist/StateContainerRegistry-Bf5VuVjm.js +747 -0
  16. package/dist/StateContainerRegistry-Bf5VuVjm.js.map +1 -0
  17. package/dist/StateContainerRegistry-BvSRfhKv.cjs +866 -0
  18. package/dist/StateContainerRegistry-BvSRfhKv.cjs.map +1 -0
  19. package/dist/StateContainerRegistry-C8jxrcqk.cjs +866 -0
  20. package/dist/StateContainerRegistry-C8jxrcqk.cjs.map +1 -0
  21. package/dist/StateContainerRegistry-DjQQtlCW.cjs +870 -0
  22. package/dist/StateContainerRegistry-DjQQtlCW.cjs.map +1 -0
  23. package/dist/StateContainerRegistry-gQbEutkN.js +751 -0
  24. package/dist/StateContainerRegistry-gQbEutkN.js.map +1 -0
  25. package/dist/config.d.cts +10 -6
  26. package/dist/config.d.ts +10 -6
  27. package/dist/constants.d.cts +12 -17
  28. package/dist/constants.d.ts +12 -17
  29. package/dist/core/StateContainer.d.ts +16 -34
  30. package/dist/core/StateContainerRegistry.d.ts +57 -174
  31. package/dist/core/symbols.d.ts +7 -4
  32. package/dist/decorators/blac.d.ts +5 -2
  33. package/dist/ensure-Bi_yXk6f.js +46 -0
  34. package/dist/ensure-Bi_yXk6f.js.map +1 -0
  35. package/dist/ensure-Bx41d-il.cjs +63 -0
  36. package/dist/ensure-Bx41d-il.cjs.map +1 -0
  37. package/dist/ensure-C7UQ_mKM.js +46 -0
  38. package/dist/ensure-C7UQ_mKM.js.map +1 -0
  39. package/dist/ensure-D1D4WjYf.cjs +63 -0
  40. package/dist/ensure-D1D4WjYf.cjs.map +1 -0
  41. package/dist/ensure-DBnuRJ0R.cjs +63 -0
  42. package/dist/ensure-DBnuRJ0R.cjs.map +1 -0
  43. package/dist/ensure-NTB9nnnj.js +46 -0
  44. package/dist/ensure-NTB9nnnj.js.map +1 -0
  45. package/dist/index.cjs +70 -49
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +2 -7
  48. package/dist/index.d.ts +2 -7
  49. package/dist/index.js +53 -26
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal.cjs +10 -0
  52. package/dist/internal.d.cts +8 -0
  53. package/dist/internal.d.ts +8 -0
  54. package/dist/internal.js +3 -0
  55. package/dist/plugin/BlacPlugin.d.ts +8 -24
  56. package/dist/plugin/PluginManager.d.ts +41 -64
  57. package/dist/plugins.cjs +67 -87
  58. package/dist/plugins.cjs.map +1 -1
  59. package/dist/plugins.d.cts +7 -6
  60. package/dist/plugins.d.ts +7 -6
  61. package/dist/plugins.js +67 -87
  62. package/dist/plugins.js.map +1 -1
  63. package/dist/registry/config.d.ts +3 -0
  64. package/dist/testing.cjs +37 -35
  65. package/dist/testing.cjs.map +1 -1
  66. package/dist/testing.d.cts +5 -11
  67. package/dist/testing.d.ts +5 -11
  68. package/dist/testing.js +28 -26
  69. package/dist/testing.js.map +1 -1
  70. package/dist/types/branded.d.ts +3 -10
  71. package/dist/types/utilities.d.ts +0 -18
  72. package/dist/utils/idGenerator.d.ts +6 -13
  73. package/dist/utils/static-props.d.ts +9 -35
  74. package/dist/utils/structural-key.d.ts +0 -5
  75. package/dist/watch/watch.d.ts +4 -9
  76. package/package.json +15 -2
package/dist/plugins.js CHANGED
@@ -1,28 +1,19 @@
1
- import { l as getBlacName, m as IS_DEV, n as globalRegistry } from "./StateContainerRegistry-BsH_s0pJ.js";
1
+ import { g as readNodeEnv, h as IS_DEV, n as globalRegistry, u as getBlacName } from "./StateContainerRegistry-gQbEutkN.js";
2
2
  import { ALL_PATHS } from "@dirtytalk/structural";
3
3
  //#region src/plugin/PluginManager.ts
4
4
  /**
5
5
  * Manages plugin lifecycle for the BlaC state management system.
6
6
  *
7
- * The manager hooks into two surfaces:
7
+ * Hooks into registry lifecycle events (synchronous) and per-container
8
+ * channel flushes (microtask-coalesced): while at least one installed
9
+ * plugin implements `onStateChange`, the manager subscribes each container
10
+ * with `ALL_PATHS` interest and dispatches
11
+ * `onStateChange(ctx, prev, next, paths)` on every flush.
8
12
  *
9
- * 1. **Registry lifecycle events** (`created`, `disposed`, `refAcquired`,
10
- * `refReleased`, `depsChanged`, `activated`, `deactivated`) — synchronous,
11
- * fired by the registry.
12
- *
13
- * 2. **Per-container channel flushes** — microtask-coalesced. For each
14
- * container, the manager subscribes once at create-time with
15
- * `ALL_PATHS` interest and stashes `prevState`. On every flush it
16
- * captures the new state + the changed `PathSet` and dispatches
17
- * `onStateChange(ctx, prev, next, paths)` to every enabled plugin.
18
- *
19
- * The `ALL_PATHS` subscription cost: plugins counted as a consumer with
20
- * `ALL_PATHS` interest will defeat the single-consumer-skip optimization
21
- * in `StructuralContainer`. This is the intended trade-off — devtools /
22
- * persist plugins genuinely want every change, and stateful plugins
23
- * (logging) can decode `paths` via `container.interner.lookup(id)` to log
24
- * only relevant fields. Plugins that want low overhead should remain
25
- * uninstalled or environment-gated.
13
+ * `ALL_PATHS` interest defeats the single-consumer-skip optimization in
14
+ * `StructuralContainer` — an intended trade-off for plugins that genuinely
15
+ * want every change (devtools/persist). Plugins that want low overhead
16
+ * should remain uninstalled or environment-gated.
26
17
  *
27
18
  * @example
28
19
  * ```ts
@@ -61,19 +52,18 @@ var PluginManager = class {
61
52
  }
62
53
  if (this.plugins.has(plugin.name)) throw new Error(`Plugin "${plugin.name}" is already installed`);
63
54
  const installContext = this.buildContext(void 0);
64
- this.plugins.set(plugin.name, {
65
- plugin,
66
- config: effectiveConfig,
67
- installContext
68
- });
69
- this.backfillPlugin(this.plugins.get(plugin.name));
70
55
  if (plugin.onInstall) try {
71
56
  plugin.onInstall(installContext);
72
57
  } catch (error) {
73
58
  console.error(`[BlaC] Error installing plugin "${plugin.name}":`, error);
74
- this.plugins.delete(plugin.name);
75
59
  throw error;
76
60
  }
61
+ this.plugins.set(plugin.name, {
62
+ plugin,
63
+ config: effectiveConfig,
64
+ installContext
65
+ });
66
+ this.backfillPlugin(plugin);
77
67
  if (IS_DEV) console.log(`[BlaC] Plugin "${plugin.name}" v${plugin.version} installed`);
78
68
  }
79
69
  /**
@@ -90,6 +80,7 @@ var PluginManager = class {
90
80
  console.error(`[BlaC] Error uninstalling plugin "${pluginName}":`, error);
91
81
  }
92
82
  this.plugins.delete(pluginName);
83
+ if (!this.hasStateChangePlugin()) this.detachAllStateBridges();
93
84
  if (IS_DEV) console.log(`[BlaC] Plugin "${pluginName}" uninstalled`);
94
85
  }
95
86
  /**
@@ -125,21 +116,19 @@ var PluginManager = class {
125
116
  this.clear();
126
117
  for (const unsub of this.lifecycleUnsubscribers) unsub();
127
118
  this.lifecycleUnsubscribers = [];
128
- for (const bridge of this.containerBridges.values()) bridge.unsub();
129
- this.containerBridges.clear();
130
119
  }
131
120
  /**
132
121
  * Wire registry lifecycle events into plugin dispatch.
133
122
  *
134
- * `onStateChange` is NOT wired through `registry.on('stateChanged', …)` —
135
- * that event lacks the `PathSet` payload. Instead, on each `created` we
136
- * subscribe to the container's channel directly so we get
137
- * `(paths)` and capture `(prev, next)` via the per-container snapshot.
123
+ * `onStateChange` is not wired through `registry.on('stateChanged', …)` —
124
+ * that event lacks the `PathSet` payload. Instead each `created` subscribes
125
+ * to the container's channel directly for `paths`, pairing it with
126
+ * `(prev, next)` from the per-container snapshot.
138
127
  */
139
128
  setupLifecycleHooks() {
140
129
  this.lifecycleUnsubscribers = [
141
130
  this.registry.on("created", (instance) => {
142
- this.attachStateBridge(instance);
131
+ if (this.hasStateChangePlugin()) this.attachStateBridge(instance);
143
132
  this.notifyPlugins("onCreated", instance);
144
133
  }),
145
134
  this.registry.on("disposed", (instance) => {
@@ -167,14 +156,10 @@ var PluginManager = class {
167
156
  ];
168
157
  }
169
158
  /**
170
- * Subscribe to the container's channel with `ALL_PATHS` interest, so we
171
- * fire on every flush. Capture `prev` from the snapshot taken on the
172
- * previous flush (or at create-time for the first flush) and pass the
173
- * channel's `paths` argument straight through to plugins.
174
- *
175
- * Per-container bookkeeping is stored in `containerBridges`, a strong `Map`
176
- * keyed by the container. Entries are removed explicitly — on the
177
- * container's `disposed` event, or for all of them in `destroy()`.
159
+ * Subscribe to the container's channel with `ALL_PATHS` interest so it
160
+ * fires on every flush. `prev` is the snapshot from the previous flush (or
161
+ * create-time for the first); the channel's `paths` is passed straight
162
+ * through to plugins.
178
163
  */
179
164
  attachStateBridge(container) {
180
165
  if (this.containerBridges.has(container)) return;
@@ -191,21 +176,25 @@ var PluginManager = class {
191
176
  bridge.unsub();
192
177
  this.containerBridges.delete(container);
193
178
  }
179
+ hasStateChangePlugin() {
180
+ for (const { plugin } of this.plugins.values()) if (plugin.onStateChange) return true;
181
+ return false;
182
+ }
183
+ detachAllStateBridges() {
184
+ for (const bridge of this.containerBridges.values()) bridge.unsub();
185
+ this.containerBridges.clear();
186
+ }
194
187
  /**
195
- * Backfill a newly-installed plugin with the app's existing instances.
196
- *
197
- * Iterates every registered `Type` and its live instances (`getAll`
198
- * already skips disposed), attaching the state bridge (idempotent — see
199
- * `attachStateBridge`) and, if the plugin implements `onCreated`, invoking
200
- * it directly for each instance. Scoped to this single plugin only — the
201
- * broadcast `notifyPlugins` would re-notify *every* plugin of *every*
202
- * instance, which is wrong here.
188
+ * Backfill a newly-installed plugin with existing instances: attach the
189
+ * state bridge (idempotent) if it implements `onStateChange`, and invoke
190
+ * `onCreated` for each. Scoped to this plugin only — `notifyPlugins` would
191
+ * wrongly re-notify every plugin of every instance.
203
192
  */
204
- backfillPlugin(installed) {
205
- const { plugin } = installed;
193
+ backfillPlugin(plugin) {
194
+ if (!plugin.onStateChange && !plugin.onCreated) return;
206
195
  for (const Type of this.registry.getTypes()) for (const instance of this.registry.getAll(Type)) {
207
196
  const container = instance;
208
- this.attachStateBridge(container);
197
+ if (plugin.onStateChange) this.attachStateBridge(container);
209
198
  if (!plugin.onCreated) continue;
210
199
  try {
211
200
  plugin.onCreated(this.buildContext(container));
@@ -215,13 +204,10 @@ var PluginManager = class {
215
204
  }
216
205
  }
217
206
  /**
218
- * Channel-flush callback. Snapshots `next`, hands `(prev, next, paths)`
219
- * to every enabled plugin's `onStateChange`, then updates `prevState`
220
- * for the next flush.
221
- *
222
- * Note: `prev` is captured once and reused across plugins — every
223
- * plugin sees the same `prev`/`next`/`paths` regardless of dispatch
224
- * order, matching the "snapshot prev once per flush" invariant.
207
+ * Channel-flush callback. Hands `(prev, next, paths)` to every enabled
208
+ * plugin's `onStateChange`, then updates `prevState` for the next flush.
209
+ * `prev` is captured once and reused across plugins, so every plugin sees
210
+ * the same values regardless of dispatch order.
225
211
  */
226
212
  dispatchStateChange(container, paths) {
227
213
  const bridge = this.containerBridges.get(container);
@@ -243,13 +229,9 @@ var PluginManager = class {
243
229
  }
244
230
  }
245
231
  /**
246
- * Notify all plugins of a lifecycle event.
247
- *
248
- * Builds a fresh `PluginContext` per dispatch (instance becomes
249
- * `ctx.container`), so plugins can reach the focal bloc through
250
- * `ctx.container`. The context is built lazily on the first matching
251
- * hook and reused for every other plugin in this dispatch — zero
252
- * builds if no enabled plugin implements `hookName`.
232
+ * Notify all plugins of a lifecycle event. Builds a `PluginContext` lazily
233
+ * on the first matching hook and reuses it for the rest of this dispatch —
234
+ * zero builds if no enabled plugin implements `hookName`.
253
235
  */
254
236
  notifyPlugins(hookName, instance, ...extraArgs) {
255
237
  let ctx;
@@ -267,8 +249,8 @@ var PluginManager = class {
267
249
  }
268
250
  /**
269
251
  * Get the `PluginContext` for a focal container, building it once and
270
- * reusing it thereafter. Eviction is handled by the `WeakMap`: the context
271
- * dies with the container. `undefined` (install-time) is never cached.
252
+ * reusing it thereafter (evicted via the `WeakMap` when the container
253
+ * dies). `undefined` (install-time) is never cached.
272
254
  */
273
255
  buildContext(container) {
274
256
  if (container !== void 0) {
@@ -342,15 +324,12 @@ var PluginManager = class {
342
324
  return this.getCurrentEnvironment() === config.environment;
343
325
  }
344
326
  /**
345
- * Get current environment
327
+ * Same rule as `IS_DEV`: an unknown environment counts as production.
346
328
  */
347
329
  getCurrentEnvironment() {
348
- if (typeof process !== "undefined") {
349
- if (process.env.NODE_ENV === "test") return "test";
350
- if (process.env.NODE_ENV === "production") return "production";
351
- return "development";
352
- }
353
- return "development";
330
+ const env = readNodeEnv();
331
+ if (env === void 0 || env === "production") return "production";
332
+ return env === "test" ? "test" : "development";
354
333
  }
355
334
  };
356
335
  /**
@@ -364,10 +343,8 @@ function createPluginManager(registry) {
364
343
  //#endregion
365
344
  //#region src/plugins.ts
366
345
  /**
367
- * Plugins Subpath Export
368
- *
369
346
  * Plugin system utilities for extending BlaC functionality.
370
- * Import from `@blac/core/plugins`
347
+ * Import from `@blac/core/plugins`.
371
348
  *
372
349
  * @example
373
350
  * ```typescript
@@ -380,21 +357,24 @@ function createPluginManager(registry) {
380
357
  * },
381
358
  * };
382
359
  *
383
- * getPluginManager().register(myPlugin);
360
+ * getPluginManager().install(myPlugin);
384
361
  * ```
385
362
  *
386
363
  * @packageDocumentation
387
364
  */
365
+ const pluginManagers = /* @__PURE__ */ new WeakMap();
388
366
  /**
389
- * Global plugin manager (initialized lazily)
367
+ * Get the plugin manager for a registry (the global one by default). Plugins
368
+ * only observe the registry they are installed on, so install them again on
369
+ * a scoped registry (e.g. one passed to `RegistryProvider`).
390
370
  */
391
- let _globalPluginManager = null;
392
- /**
393
- * Get the global plugin manager
394
- */
395
- function getPluginManager() {
396
- if (!_globalPluginManager) _globalPluginManager = createPluginManager(globalRegistry);
397
- return _globalPluginManager;
371
+ function getPluginManager(registry = globalRegistry) {
372
+ let manager = pluginManagers.get(registry);
373
+ if (!manager) {
374
+ manager = createPluginManager(registry);
375
+ pluginManagers.set(registry, manager);
376
+ }
377
+ return manager;
398
378
  }
399
379
  //#endregion
400
380
  export { PluginManager, getPluginManager };
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.js","names":[],"sources":["../src/plugin/PluginManager.ts","../src/plugins.ts"],"sourcesContent":["import { ALL_PATHS, type PathSet } from '@dirtytalk/structural';\nimport type { StateContainer } from '../core/StateContainer';\nimport { IS_DEV } from '../constants';\nimport { getBlacName } from '../utils/static-props';\nimport type { StateContainerConstructor } from '../types/utilities';\nimport type { StateContainerRegistry } from '../core/StateContainerRegistry';\nimport type {\n BlacPlugin,\n PluginContext,\n PluginConfig,\n InstanceMetadata,\n} from './BlacPlugin';\n\n/**\n * Internal structure for tracking installed plugins.\n *\n * `installContext` is the context handed to `onInstall` — it has no\n * `container`. Per-container contexts are built on demand by\n * `buildContext()` so each event carries the right focal container\n * without mutating a shared object.\n * @internal\n */\ninterface InstalledPlugin {\n plugin: BlacPlugin;\n config: PluginConfig;\n installContext: PluginContext;\n}\n\n/**\n * Per-container bookkeeping for the channel-bridge plugin dispatcher.\n *\n * - `unsub` tears down the channel subscription on dispose.\n * - `prevState` is the state snapshot the manager will hand plugins as\n * `prev` on the next flush; it is updated to the post-flush state after\n * each dispatch.\n * @internal\n */\ninterface ContainerBridge {\n unsub: () => void;\n prevState: any;\n}\n\n/**\n * Manages plugin lifecycle for the BlaC state management system.\n *\n * The manager hooks into two surfaces:\n *\n * 1. **Registry lifecycle events** (`created`, `disposed`, `refAcquired`,\n * `refReleased`, `depsChanged`, `activated`, `deactivated`) — synchronous,\n * fired by the registry.\n *\n * 2. **Per-container channel flushes** — microtask-coalesced. For each\n * container, the manager subscribes once at create-time with\n * `ALL_PATHS` interest and stashes `prevState`. On every flush it\n * captures the new state + the changed `PathSet` and dispatches\n * `onStateChange(ctx, prev, next, paths)` to every enabled plugin.\n *\n * The `ALL_PATHS` subscription cost: plugins counted as a consumer with\n * `ALL_PATHS` interest will defeat the single-consumer-skip optimization\n * in `StructuralContainer`. This is the intended trade-off — devtools /\n * persist plugins genuinely want every change, and stateful plugins\n * (logging) can decode `paths` via `container.interner.lookup(id)` to log\n * only relevant fields. Plugins that want low overhead should remain\n * uninstalled or environment-gated.\n *\n * @example\n * ```ts\n * const manager = createPluginManager(registry);\n * manager.install(myPlugin, { environment: 'development' });\n * ```\n */\nexport class PluginManager {\n private plugins = new Map<string, InstalledPlugin>();\n private registry: StateContainerRegistry;\n private lifecycleUnsubscribers: (() => void)[] = [];\n\n /**\n * Per-container channel-bridge bookkeeping. Subscribed at `created`,\n * torn down at `disposed` and at `destroy()`. Holds the rolling `prevState`\n * snapshot the manager hands plugins on each flush.\n *\n * Strong, not a WeakMap, because `destroy()` has to enumerate the live\n * bridges to unsubscribe them. Safe: entries are removed on `disposed`, and\n * the owning registry holds its own strong reference over that same window.\n */\n private containerBridges = new Map<\n StateContainer<any, any, any>,\n ContainerBridge\n >();\n\n /**\n * One `PluginContext` per container. The context closes over `registry` and\n * `container` only — both stable for the container's lifetime — so it is\n * safe to reuse instead of rebuilding its method table on every dispatch.\n */\n private contextCache = new WeakMap<\n StateContainer<any, any, any>,\n PluginContext\n >();\n\n /**\n * Create a new PluginManager\n * @param registry - The StateContainerRegistry to monitor for lifecycle events\n */\n constructor(registry: StateContainerRegistry) {\n this.registry = registry;\n this.setupLifecycleHooks();\n }\n\n /**\n * Install a plugin with optional configuration\n * @param plugin - The plugin to install\n * @param config - Optional plugin configuration\n * @throws Error if plugin is already installed\n */\n install(plugin: BlacPlugin, config: PluginConfig = {}): void {\n const effectiveConfig: PluginConfig = {\n enabled: true,\n environment: 'all',\n ...config,\n };\n\n if (!this.shouldEnablePlugin(effectiveConfig)) {\n if (IS_DEV) {\n console.log(\n `[BlaC] Plugin \"${plugin.name}\" skipped (environment mismatch)`,\n );\n }\n return;\n }\n\n if (this.plugins.has(plugin.name)) {\n throw new Error(`Plugin \"${plugin.name}\" is already installed`);\n }\n\n const installContext = this.buildContext(undefined);\n\n this.plugins.set(plugin.name, {\n plugin,\n config: effectiveConfig,\n installContext,\n });\n\n this.backfillPlugin(this.plugins.get(plugin.name)!);\n\n if (plugin.onInstall) {\n try {\n plugin.onInstall(installContext);\n } catch (error) {\n console.error(\n `[BlaC] Error installing plugin \"${plugin.name}\":`,\n error,\n );\n this.plugins.delete(plugin.name);\n throw error;\n }\n }\n\n if (IS_DEV) {\n console.log(\n `[BlaC] Plugin \"${plugin.name}\" v${plugin.version} installed`,\n );\n }\n }\n\n /**\n * Uninstall a plugin by name\n * @param pluginName - The name of the plugin to uninstall\n * @throws Error if plugin is not installed\n */\n uninstall(pluginName: string): void {\n const installed = this.plugins.get(pluginName);\n if (!installed) {\n throw new Error(`Plugin \"${pluginName}\" is not installed`);\n }\n\n if (installed.plugin.onUninstall) {\n try {\n installed.plugin.onUninstall();\n } catch (error) {\n console.error(\n `[BlaC] Error uninstalling plugin \"${pluginName}\":`,\n error,\n );\n }\n }\n\n this.plugins.delete(pluginName);\n if (IS_DEV) {\n console.log(`[BlaC] Plugin \"${pluginName}\" uninstalled`);\n }\n }\n\n /**\n * Get an installed plugin by name\n * @param pluginName - The name of the plugin to retrieve\n * @returns The plugin instance or undefined if not found\n */\n getPlugin(pluginName: string): BlacPlugin | undefined {\n return this.plugins.get(pluginName)?.plugin;\n }\n\n /**\n * Get all installed plugins\n * @returns Array of all installed plugins\n */\n getAllPlugins(): BlacPlugin[] {\n return Array.from(this.plugins.values()).map((p) => p.plugin);\n }\n\n /**\n * Check if a plugin is installed\n * @param pluginName - The name of the plugin to check\n * @returns true if the plugin is installed\n */\n hasPlugin(pluginName: string): boolean {\n return this.plugins.has(pluginName);\n }\n\n /**\n * Uninstall all plugins\n */\n clear(): void {\n for (const name of this.plugins.keys()) {\n this.uninstall(name);\n }\n }\n\n destroy(): void {\n this.clear();\n for (const unsub of this.lifecycleUnsubscribers) {\n unsub();\n }\n this.lifecycleUnsubscribers = [];\n // Without this each container keeps its ALL_PATHS bridge subscriber — and\n // the single-consumer-skip penalty that comes with it — forever.\n for (const bridge of this.containerBridges.values()) {\n bridge.unsub();\n }\n this.containerBridges.clear();\n }\n\n /**\n * Wire registry lifecycle events into plugin dispatch.\n *\n * `onStateChange` is NOT wired through `registry.on('stateChanged', …)` —\n * that event lacks the `PathSet` payload. Instead, on each `created` we\n * subscribe to the container's channel directly so we get\n * `(paths)` and capture `(prev, next)` via the per-container snapshot.\n */\n private setupLifecycleHooks(): void {\n this.lifecycleUnsubscribers = [\n this.registry.on('created', (instance) => {\n this.attachStateBridge(instance);\n this.notifyPlugins('onCreated', instance);\n }),\n this.registry.on('disposed', (instance) => {\n this.notifyPlugins('onDestroyed', instance);\n this.detachStateBridge(instance);\n }),\n this.registry.on('refAcquired', (instance, refId) => {\n this.notifyPlugins('onRefAcquired', instance, refId);\n }),\n this.registry.on('refReleased', (instance, refId) => {\n this.notifyPlugins('onRefReleased', instance, refId);\n }),\n this.registry.on('depsChanged', (instance, previousDeps, currentDeps) => {\n this.notifyPlugins(\n 'onDepsChanged',\n instance,\n previousDeps,\n currentDeps,\n );\n }),\n this.registry.on(\n 'hydrationChanged',\n (instance, status, previousStatus) => {\n this.notifyPlugins(\n 'onHydrationChange',\n instance,\n status,\n previousStatus,\n );\n },\n ),\n this.registry.on('activated', (instance, signal) => {\n this.notifyPlugins('onActivate', instance, signal);\n }),\n this.registry.on('deactivated', (instance) => {\n this.notifyPlugins('onDeactivate', instance);\n }),\n ];\n }\n\n /**\n * Subscribe to the container's channel with `ALL_PATHS` interest, so we\n * fire on every flush. Capture `prev` from the snapshot taken on the\n * previous flush (or at create-time for the first flush) and pass the\n * channel's `paths` argument straight through to plugins.\n *\n * Per-container bookkeeping is stored in `containerBridges`, a strong `Map`\n * keyed by the container. Entries are removed explicitly — on the\n * container's `disposed` event, or for all of them in `destroy()`.\n */\n private attachStateBridge(container: StateContainer<any, any, any>): void {\n // Defensive: if a container is somehow created twice (it shouldn't be),\n // don't double-subscribe — the existing bridge is canonical.\n if (this.containerBridges.has(container)) return;\n\n const bridge: ContainerBridge = {\n unsub: () => {},\n prevState: container.state,\n };\n this.containerBridges.set(container, bridge);\n\n bridge.unsub = container.channel.subscribe(\n () => ALL_PATHS,\n (paths) => this.dispatchStateChange(container, paths),\n );\n }\n\n private detachStateBridge(container: StateContainer<any, any, any>): void {\n const bridge = this.containerBridges.get(container);\n if (!bridge) return;\n bridge.unsub();\n this.containerBridges.delete(container);\n }\n\n /**\n * Backfill a newly-installed plugin with the app's existing instances.\n *\n * Iterates every registered `Type` and its live instances (`getAll`\n * already skips disposed), attaching the state bridge (idempotent — see\n * `attachStateBridge`) and, if the plugin implements `onCreated`, invoking\n * it directly for each instance. Scoped to this single plugin only — the\n * broadcast `notifyPlugins` would re-notify *every* plugin of *every*\n * instance, which is wrong here.\n */\n private backfillPlugin(installed: InstalledPlugin): void {\n const { plugin } = installed;\n for (const Type of this.registry.getTypes()) {\n for (const instance of this.registry.getAll(Type)) {\n // getAll returns the readonly public view; internal bridging needs the\n // real container (same friction the `as any` in queryInstances handles).\n const container = instance as StateContainer<any, any, any>;\n this.attachStateBridge(container);\n\n if (!plugin.onCreated) continue;\n try {\n plugin.onCreated(this.buildContext(container));\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" onCreated (backfill):`,\n error,\n );\n }\n }\n }\n }\n\n /**\n * Channel-flush callback. Snapshots `next`, hands `(prev, next, paths)`\n * to every enabled plugin's `onStateChange`, then updates `prevState`\n * for the next flush.\n *\n * Note: `prev` is captured once and reused across plugins — every\n * plugin sees the same `prev`/`next`/`paths` regardless of dispatch\n * order, matching the \"snapshot prev once per flush\" invariant.\n */\n private dispatchStateChange(\n container: StateContainer<any, any, any>,\n paths: PathSet,\n ): void {\n const bridge = this.containerBridges.get(container);\n if (!bridge) return;\n\n const prev = bridge.prevState;\n const next = container.state;\n bridge.prevState = next;\n\n let ctx: PluginContext | undefined;\n for (const { plugin, config } of this.plugins.values()) {\n if (!config.enabled) continue;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- invoked via .call below\n const hook = plugin.onStateChange;\n if (typeof hook !== 'function') continue;\n\n try {\n ctx ??= this.buildContext(container);\n hook.call(plugin, ctx, prev, next, paths);\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" onStateChange:`,\n error,\n );\n }\n }\n }\n\n /**\n * Notify all plugins of a lifecycle event.\n *\n * Builds a fresh `PluginContext` per dispatch (instance becomes\n * `ctx.container`), so plugins can reach the focal bloc through\n * `ctx.container`. The context is built lazily on the first matching\n * hook and reused for every other plugin in this dispatch — zero\n * builds if no enabled plugin implements `hookName`.\n */\n private notifyPlugins(\n hookName: Exclude<keyof BlacPlugin, 'onStateChange'>,\n instance: StateContainer<any, any, any>,\n ...extraArgs: any[]\n ): void {\n let ctx: PluginContext | undefined;\n for (const { plugin, config } of this.plugins.values()) {\n if (!config.enabled) continue;\n\n const hook = plugin[hookName];\n if (typeof hook !== 'function') continue;\n\n try {\n ctx ??= this.buildContext(instance);\n (hook as any).call(plugin, ctx, ...extraArgs);\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" ${hookName}:`,\n error,\n );\n }\n }\n }\n\n /**\n * Get the `PluginContext` for a focal container, building it once and\n * reusing it thereafter. Eviction is handled by the `WeakMap`: the context\n * dies with the container. `undefined` (install-time) is never cached.\n */\n private buildContext(\n container: StateContainer<any, any, any> | undefined,\n ): PluginContext {\n if (container !== undefined) {\n const cached = this.contextCache.get(container);\n if (cached) return cached;\n }\n const context = this.createContext(container);\n if (container !== undefined) this.contextCache.set(container, context);\n return context;\n }\n\n private createContext(\n container: StateContainer<any, any, any> | undefined,\n ): PluginContext {\n const registry = this.registry;\n return {\n container,\n\n getInstanceMetadata: (\n instance: StateContainer<any, any, any>,\n ): InstanceMetadata => {\n return {\n id: instance.$blac.id,\n className: getBlacName(\n instance.constructor as StateContainerConstructor,\n ),\n isDisposed: instance.$blac.disposed,\n name: instance.$blac.name,\n createdAt: instance.$blac.createdAt,\n state: instance.state,\n hydrationStatus: instance.$blac.hydration.status,\n isHydrated: instance.$blac.hydration.isHydrated,\n hydrationError: instance.$blac.hydration.error,\n changedWhileHydrating: instance.$blac.hydration.changedWhileHydrating,\n args: instance.args,\n };\n },\n\n getState: <S extends object = any>(instance: StateContainer<S>): S => {\n return instance.state;\n },\n\n getHydrationStatus: (instance: StateContainer<any, any, any>) => {\n return instance.$blac.hydration.status;\n },\n\n startHydration: (instance: StateContainer<any, any, any>) => {\n instance.$blac.hydration.begin();\n },\n\n applyHydratedState: <S extends object = any>(\n instance: StateContainer<S>,\n state: S,\n ): boolean => {\n return instance.$blac.hydration.apply(state);\n },\n\n finishHydration: (instance: StateContainer<any, any, any>) => {\n instance.$blac.hydration.finish();\n },\n\n failHydration: (\n instance: StateContainer<any, any, any>,\n error: Error,\n ) => {\n instance.$blac.hydration.fail(error);\n },\n\n waitForHydration: (instance: StateContainer<any, any, any>) => {\n return instance.$blac.hydration.wait();\n },\n\n queryInstances: <T extends StateContainer<any, any, any>>(\n typeClass: new (...args: any[]) => T,\n ): T[] => {\n return registry.getAll(typeClass as any);\n },\n\n getAllTypes: () => {\n return registry.getTypes();\n },\n\n getStats: () => {\n return registry.getStats();\n },\n\n getRefIds: (instanceId: string): string[] => {\n return registry.getRefIdsById(instanceId);\n },\n };\n }\n\n /**\n * Check if plugin should be enabled based on environment\n */\n private shouldEnablePlugin(config: PluginConfig): boolean {\n if (!config.enabled) return false;\n if (config.environment === 'all') return true;\n\n const currentEnv = this.getCurrentEnvironment();\n return currentEnv === config.environment;\n }\n\n /**\n * Get current environment\n */\n private getCurrentEnvironment(): 'development' | 'production' | 'test' {\n if (typeof process !== 'undefined') {\n if (process.env.NODE_ENV === 'test') return 'test';\n if (process.env.NODE_ENV === 'production') return 'production';\n return 'development';\n }\n return 'development';\n }\n}\n\n/**\n * Create a plugin manager instance\n * @param registry - The StateContainerRegistry to monitor for lifecycle events\n * @returns A new PluginManager instance\n */\nexport function createPluginManager(\n registry: StateContainerRegistry,\n): PluginManager {\n return new PluginManager(registry);\n}\n","/**\n * Plugins Subpath Export\n *\n * Plugin system utilities for extending BlaC functionality.\n * Import from `@blac/core/plugins`\n *\n * @example\n * ```typescript\n * import { PluginManager, getPluginManager, type BlacPlugin } from '@blac/core/plugins';\n *\n * const myPlugin: BlacPlugin = {\n * name: 'my-plugin',\n * onCreated(context) {\n * console.log('Instance created:', context.container);\n * },\n * };\n *\n * getPluginManager().register(myPlugin);\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createPluginManager, PluginManager } from './plugin/PluginManager';\nimport { globalRegistry } from './core/StateContainerRegistry';\n\nexport { PluginManager } from './plugin/PluginManager';\nexport type {\n BlacPlugin,\n BlacPluginWithInit,\n PluginContext,\n PluginConfig,\n InstanceMetadata,\n} from './plugin/BlacPlugin';\n\n/**\n * Global plugin manager (initialized lazily)\n */\nlet _globalPluginManager: PluginManager | null = null;\n\n/**\n * Get the global plugin manager\n */\nexport function getPluginManager(): PluginManager {\n if (!_globalPluginManager) {\n _globalPluginManager = createPluginManager(globalRegistry);\n }\n return _globalPluginManager;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugins.js","names":[],"sources":["../src/plugin/PluginManager.ts","../src/plugins.ts"],"sourcesContent":["import { ALL_PATHS, type PathSet } from '@dirtytalk/structural';\nimport type { StateContainer } from '../core/StateContainer';\nimport { IS_DEV, readNodeEnv } from '../constants';\nimport { getBlacName } from '../utils/static-props';\nimport type { StateContainerConstructor } from '../types/utilities';\nimport type { StateContainerRegistry } from '../core/StateContainerRegistry';\nimport type {\n BlacPlugin,\n PluginContext,\n PluginConfig,\n InstanceMetadata,\n} from './BlacPlugin';\n\n/**\n * Internal structure for tracking installed plugins.\n *\n * `installContext` is the context handed to `onInstall` — it has no\n * `container`. Per-container contexts are built on demand by\n * `buildContext()` so each event carries the right focal container\n * without mutating a shared object.\n * @internal\n */\ninterface InstalledPlugin {\n plugin: BlacPlugin;\n config: PluginConfig;\n installContext: PluginContext;\n}\n\n/**\n * Per-container bookkeeping for the channel-bridge plugin dispatcher.\n *\n * - `unsub` tears down the channel subscription on dispose.\n * - `prevState` is the state snapshot the manager will hand plugins as\n * `prev` on the next flush; it is updated to the post-flush state after\n * each dispatch.\n * @internal\n */\ninterface ContainerBridge {\n unsub: () => void;\n prevState: any;\n}\n\n/**\n * Manages plugin lifecycle for the BlaC state management system.\n *\n * Hooks into registry lifecycle events (synchronous) and per-container\n * channel flushes (microtask-coalesced): while at least one installed\n * plugin implements `onStateChange`, the manager subscribes each container\n * with `ALL_PATHS` interest and dispatches\n * `onStateChange(ctx, prev, next, paths)` on every flush.\n *\n * `ALL_PATHS` interest defeats the single-consumer-skip optimization in\n * `StructuralContainer` — an intended trade-off for plugins that genuinely\n * want every change (devtools/persist). Plugins that want low overhead\n * should remain uninstalled or environment-gated.\n *\n * @example\n * ```ts\n * const manager = createPluginManager(registry);\n * manager.install(myPlugin, { environment: 'development' });\n * ```\n */\nexport class PluginManager {\n private plugins = new Map<string, InstalledPlugin>();\n private registry: StateContainerRegistry;\n private lifecycleUnsubscribers: (() => void)[] = [];\n\n /**\n * Per-container channel-bridge bookkeeping, subscribed at `created` and\n * torn down at `disposed`/`destroy()`. Holds the rolling `prevState`\n * snapshot handed to plugins on each flush.\n *\n * Strong, not a `WeakMap`, because `destroy()` must enumerate live bridges\n * to unsubscribe them; entries are removed on `disposed`, so this never\n * outlives the registry's own reference.\n */\n private containerBridges = new Map<\n StateContainer<any, any, any>,\n ContainerBridge\n >();\n\n /**\n * One `PluginContext` per container, safe to reuse since it closes only\n * over `registry`/`container`, both stable for the container's lifetime.\n */\n private contextCache = new WeakMap<\n StateContainer<any, any, any>,\n PluginContext\n >();\n\n /**\n * Create a new PluginManager\n * @param registry - The StateContainerRegistry to monitor for lifecycle events\n */\n constructor(registry: StateContainerRegistry) {\n this.registry = registry;\n this.setupLifecycleHooks();\n }\n\n /**\n * Install a plugin with optional configuration\n * @param plugin - The plugin to install\n * @param config - Optional plugin configuration\n * @throws Error if plugin is already installed\n */\n install(plugin: BlacPlugin, config: PluginConfig = {}): void {\n const effectiveConfig: PluginConfig = {\n enabled: true,\n environment: 'all',\n ...config,\n };\n\n if (!this.shouldEnablePlugin(effectiveConfig)) {\n if (IS_DEV) {\n console.log(\n `[BlaC] Plugin \"${plugin.name}\" skipped (environment mismatch)`,\n );\n }\n return;\n }\n\n if (this.plugins.has(plugin.name)) {\n throw new Error(`Plugin \"${plugin.name}\" is already installed`);\n }\n\n const installContext = this.buildContext(undefined);\n\n if (plugin.onInstall) {\n try {\n plugin.onInstall(installContext);\n } catch (error) {\n console.error(\n `[BlaC] Error installing plugin \"${plugin.name}\":`,\n error,\n );\n throw error;\n }\n }\n\n this.plugins.set(plugin.name, {\n plugin,\n config: effectiveConfig,\n installContext,\n });\n this.backfillPlugin(plugin);\n\n if (IS_DEV) {\n console.log(\n `[BlaC] Plugin \"${plugin.name}\" v${plugin.version} installed`,\n );\n }\n }\n\n /**\n * Uninstall a plugin by name\n * @param pluginName - The name of the plugin to uninstall\n * @throws Error if plugin is not installed\n */\n uninstall(pluginName: string): void {\n const installed = this.plugins.get(pluginName);\n if (!installed) {\n throw new Error(`Plugin \"${pluginName}\" is not installed`);\n }\n\n if (installed.plugin.onUninstall) {\n try {\n installed.plugin.onUninstall();\n } catch (error) {\n console.error(\n `[BlaC] Error uninstalling plugin \"${pluginName}\":`,\n error,\n );\n }\n }\n\n this.plugins.delete(pluginName);\n if (!this.hasStateChangePlugin()) this.detachAllStateBridges();\n if (IS_DEV) {\n console.log(`[BlaC] Plugin \"${pluginName}\" uninstalled`);\n }\n }\n\n /**\n * Get an installed plugin by name\n * @param pluginName - The name of the plugin to retrieve\n * @returns The plugin instance or undefined if not found\n */\n getPlugin(pluginName: string): BlacPlugin | undefined {\n return this.plugins.get(pluginName)?.plugin;\n }\n\n /**\n * Get all installed plugins\n * @returns Array of all installed plugins\n */\n getAllPlugins(): BlacPlugin[] {\n return Array.from(this.plugins.values()).map((p) => p.plugin);\n }\n\n /**\n * Check if a plugin is installed\n * @param pluginName - The name of the plugin to check\n * @returns true if the plugin is installed\n */\n hasPlugin(pluginName: string): boolean {\n return this.plugins.has(pluginName);\n }\n\n /**\n * Uninstall all plugins\n */\n clear(): void {\n for (const name of this.plugins.keys()) {\n this.uninstall(name);\n }\n }\n\n destroy(): void {\n this.clear();\n for (const unsub of this.lifecycleUnsubscribers) {\n unsub();\n }\n this.lifecycleUnsubscribers = [];\n }\n\n /**\n * Wire registry lifecycle events into plugin dispatch.\n *\n * `onStateChange` is not wired through `registry.on('stateChanged', …)` —\n * that event lacks the `PathSet` payload. Instead each `created` subscribes\n * to the container's channel directly for `paths`, pairing it with\n * `(prev, next)` from the per-container snapshot.\n */\n private setupLifecycleHooks(): void {\n this.lifecycleUnsubscribers = [\n this.registry.on('created', (instance) => {\n if (this.hasStateChangePlugin()) this.attachStateBridge(instance);\n this.notifyPlugins('onCreated', instance);\n }),\n this.registry.on('disposed', (instance) => {\n this.notifyPlugins('onDestroyed', instance);\n this.detachStateBridge(instance);\n }),\n this.registry.on('refAcquired', (instance, refId) => {\n this.notifyPlugins('onRefAcquired', instance, refId);\n }),\n this.registry.on('refReleased', (instance, refId) => {\n this.notifyPlugins('onRefReleased', instance, refId);\n }),\n this.registry.on('depsChanged', (instance, previousDeps, currentDeps) => {\n this.notifyPlugins(\n 'onDepsChanged',\n instance,\n previousDeps,\n currentDeps,\n );\n }),\n this.registry.on(\n 'hydrationChanged',\n (instance, status, previousStatus) => {\n this.notifyPlugins(\n 'onHydrationChange',\n instance,\n status,\n previousStatus,\n );\n },\n ),\n this.registry.on('activated', (instance, signal) => {\n this.notifyPlugins('onActivate', instance, signal);\n }),\n this.registry.on('deactivated', (instance) => {\n this.notifyPlugins('onDeactivate', instance);\n }),\n ];\n }\n\n /**\n * Subscribe to the container's channel with `ALL_PATHS` interest so it\n * fires on every flush. `prev` is the snapshot from the previous flush (or\n * create-time for the first); the channel's `paths` is passed straight\n * through to plugins.\n */\n private attachStateBridge(container: StateContainer<any, any, any>): void {\n // Defensive: if a container is somehow created twice (it shouldn't be),\n // don't double-subscribe — the existing bridge is canonical.\n if (this.containerBridges.has(container)) return;\n\n const bridge: ContainerBridge = {\n unsub: () => {},\n prevState: container.state,\n };\n this.containerBridges.set(container, bridge);\n\n bridge.unsub = container.channel.subscribe(\n () => ALL_PATHS,\n (paths) => this.dispatchStateChange(container, paths),\n );\n }\n\n private detachStateBridge(container: StateContainer<any, any, any>): void {\n const bridge = this.containerBridges.get(container);\n if (!bridge) return;\n bridge.unsub();\n this.containerBridges.delete(container);\n }\n\n // Bridges cost every container its single-consumer skip, so they exist only\n // while some installed plugin implements `onStateChange`.\n private hasStateChangePlugin(): boolean {\n for (const { plugin } of this.plugins.values()) {\n if (plugin.onStateChange) return true;\n }\n return false;\n }\n\n private detachAllStateBridges(): void {\n for (const bridge of this.containerBridges.values()) {\n bridge.unsub();\n }\n this.containerBridges.clear();\n }\n\n /**\n * Backfill a newly-installed plugin with existing instances: attach the\n * state bridge (idempotent) if it implements `onStateChange`, and invoke\n * `onCreated` for each. Scoped to this plugin only — `notifyPlugins` would\n * wrongly re-notify every plugin of every instance.\n */\n private backfillPlugin(plugin: BlacPlugin): void {\n if (!plugin.onStateChange && !plugin.onCreated) return;\n for (const Type of this.registry.getTypes()) {\n for (const instance of this.registry.getAll(Type)) {\n // getAll returns the readonly public view; internal bridging needs the\n // real container (same friction the `as any` in queryInstances handles).\n const container = instance as StateContainer<any, any, any>;\n if (plugin.onStateChange) this.attachStateBridge(container);\n\n if (!plugin.onCreated) continue;\n try {\n plugin.onCreated(this.buildContext(container));\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" onCreated (backfill):`,\n error,\n );\n }\n }\n }\n }\n\n /**\n * Channel-flush callback. Hands `(prev, next, paths)` to every enabled\n * plugin's `onStateChange`, then updates `prevState` for the next flush.\n * `prev` is captured once and reused across plugins, so every plugin sees\n * the same values regardless of dispatch order.\n */\n private dispatchStateChange(\n container: StateContainer<any, any, any>,\n paths: PathSet,\n ): void {\n const bridge = this.containerBridges.get(container);\n if (!bridge) return;\n\n const prev = bridge.prevState;\n const next = container.state;\n bridge.prevState = next;\n\n let ctx: PluginContext | undefined;\n for (const { plugin, config } of this.plugins.values()) {\n if (!config.enabled) continue;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- invoked via .call below\n const hook = plugin.onStateChange;\n if (typeof hook !== 'function') continue;\n\n try {\n ctx ??= this.buildContext(container);\n hook.call(plugin, ctx, prev, next, paths);\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" onStateChange:`,\n error,\n );\n }\n }\n }\n\n /**\n * Notify all plugins of a lifecycle event. Builds a `PluginContext` lazily\n * on the first matching hook and reuses it for the rest of this dispatch —\n * zero builds if no enabled plugin implements `hookName`.\n */\n private notifyPlugins(\n hookName: Exclude<keyof BlacPlugin, 'onStateChange'>,\n instance: StateContainer<any, any, any>,\n ...extraArgs: any[]\n ): void {\n let ctx: PluginContext | undefined;\n for (const { plugin, config } of this.plugins.values()) {\n if (!config.enabled) continue;\n\n const hook = plugin[hookName];\n if (typeof hook !== 'function') continue;\n\n try {\n ctx ??= this.buildContext(instance);\n (hook as any).call(plugin, ctx, ...extraArgs);\n } catch (error) {\n console.error(\n `[BlaC] Error in plugin \"${plugin.name}\" ${hookName}:`,\n error,\n );\n }\n }\n }\n\n /**\n * Get the `PluginContext` for a focal container, building it once and\n * reusing it thereafter (evicted via the `WeakMap` when the container\n * dies). `undefined` (install-time) is never cached.\n */\n private buildContext(\n container: StateContainer<any, any, any> | undefined,\n ): PluginContext {\n if (container !== undefined) {\n const cached = this.contextCache.get(container);\n if (cached) return cached;\n }\n const context = this.createContext(container);\n if (container !== undefined) this.contextCache.set(container, context);\n return context;\n }\n\n private createContext(\n container: StateContainer<any, any, any> | undefined,\n ): PluginContext {\n const registry = this.registry;\n return {\n container,\n\n getInstanceMetadata: (\n instance: StateContainer<any, any, any>,\n ): InstanceMetadata => {\n return {\n id: instance.$blac.id,\n className: getBlacName(\n instance.constructor as StateContainerConstructor,\n ),\n isDisposed: instance.$blac.disposed,\n name: instance.$blac.name,\n createdAt: instance.$blac.createdAt,\n state: instance.state,\n hydrationStatus: instance.$blac.hydration.status,\n isHydrated: instance.$blac.hydration.isHydrated,\n hydrationError: instance.$blac.hydration.error,\n changedWhileHydrating: instance.$blac.hydration.changedWhileHydrating,\n args: instance.args,\n };\n },\n\n getState: <S extends object = any>(instance: StateContainer<S>): S => {\n return instance.state;\n },\n\n getHydrationStatus: (instance: StateContainer<any, any, any>) => {\n return instance.$blac.hydration.status;\n },\n\n startHydration: (instance: StateContainer<any, any, any>) => {\n instance.$blac.hydration.begin();\n },\n\n applyHydratedState: <S extends object = any>(\n instance: StateContainer<S>,\n state: S,\n ): boolean => {\n return instance.$blac.hydration.apply(state);\n },\n\n finishHydration: (instance: StateContainer<any, any, any>) => {\n instance.$blac.hydration.finish();\n },\n\n failHydration: (\n instance: StateContainer<any, any, any>,\n error: Error,\n ) => {\n instance.$blac.hydration.fail(error);\n },\n\n waitForHydration: (instance: StateContainer<any, any, any>) => {\n return instance.$blac.hydration.wait();\n },\n\n queryInstances: <T extends StateContainer<any, any, any>>(\n typeClass: new (...args: any[]) => T,\n ): T[] => {\n return registry.getAll(typeClass as any);\n },\n\n getAllTypes: () => {\n return registry.getTypes();\n },\n\n getStats: () => {\n return registry.getStats();\n },\n\n getRefIds: (instanceId: string): string[] => {\n return registry.getRefIdsById(instanceId);\n },\n };\n }\n\n /**\n * Check if plugin should be enabled based on environment\n */\n private shouldEnablePlugin(config: PluginConfig): boolean {\n if (!config.enabled) return false;\n if (config.environment === 'all') return true;\n\n const currentEnv = this.getCurrentEnvironment();\n return currentEnv === config.environment;\n }\n\n /**\n * Same rule as `IS_DEV`: an unknown environment counts as production.\n */\n private getCurrentEnvironment(): 'development' | 'production' | 'test' {\n const env = readNodeEnv();\n if (env === undefined || env === 'production') return 'production';\n return env === 'test' ? 'test' : 'development';\n }\n}\n\n/**\n * Create a plugin manager instance\n * @param registry - The StateContainerRegistry to monitor for lifecycle events\n * @returns A new PluginManager instance\n */\nexport function createPluginManager(\n registry: StateContainerRegistry,\n): PluginManager {\n return new PluginManager(registry);\n}\n","/**\n * Plugin system utilities for extending BlaC functionality.\n * Import from `@blac/core/plugins`.\n *\n * @example\n * ```typescript\n * import { PluginManager, getPluginManager, type BlacPlugin } from '@blac/core/plugins';\n *\n * const myPlugin: BlacPlugin = {\n * name: 'my-plugin',\n * onCreated(context) {\n * console.log('Instance created:', context.container);\n * },\n * };\n *\n * getPluginManager().install(myPlugin);\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createPluginManager, PluginManager } from './plugin/PluginManager';\nimport {\n globalRegistry,\n type StateContainerRegistry,\n} from './core/StateContainerRegistry';\n\nexport { PluginManager } from './plugin/PluginManager';\nexport type {\n BlacPlugin,\n BlacPluginWithInit,\n PluginContext,\n PluginConfig,\n InstanceMetadata,\n} from './plugin/BlacPlugin';\n\nconst pluginManagers = new WeakMap<StateContainerRegistry, PluginManager>();\n\n/**\n * Get the plugin manager for a registry (the global one by default). Plugins\n * only observe the registry they are installed on, so install them again on\n * a scoped registry (e.g. one passed to `RegistryProvider`).\n */\nexport function getPluginManager(\n registry: StateContainerRegistry = globalRegistry,\n): PluginManager {\n let manager = pluginManagers.get(registry);\n if (!manager) {\n manager = createPluginManager(registry);\n pluginManagers.set(registry, manager);\n }\n return manager;\n}\n"],"mappings":"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"}
@@ -1,5 +1,8 @@
1
1
  import { StateContainerRegistry } from '../core/StateContainerRegistry';
2
2
  /**
3
+ * The registry every plain helper (`acquire`, `ensure`, `watch`, …) uses.
4
+ * `RegistryProvider` from `@blac/react` does not change it; components use
5
+ * `useRegistry()` for the provider's registry.
3
6
  * @public
4
7
  */
5
8
  export declare function getRegistry(): StateContainerRegistry;
package/dist/testing.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_StateContainerRegistry = require("./StateContainerRegistry-CBLeHObO.cjs");
3
- const require_ensure = require("./ensure-D3w6YGp9.cjs");
2
+ const require_StateContainerRegistry = require("./StateContainerRegistry-DjQQtlCW.cjs");
3
+ const require_StateContainer = require("./StateContainer-BUyuxKb3.cjs");
4
+ const require_ensure = require("./ensure-DBnuRJ0R.cjs");
4
5
  //#region src/testing.ts
5
6
  /** Synthetic owner id used by test helpers for pre-wired deps. */
6
7
  const TESTING_DEPS_OWNER = "testing-deps";
@@ -8,39 +9,42 @@ function createTestRegistry() {
8
9
  return new require_StateContainerRegistry.StateContainerRegistry();
9
10
  }
10
11
  function withTestRegistry(fn) {
11
- const previous = require_ensure.getRegistry();
12
+ const previous = require_StateContainer.getRegistry();
12
13
  const testRegistry = createTestRegistry();
13
- require_ensure.setRegistry(testRegistry);
14
+ require_StateContainer.setRegistry(testRegistry);
14
15
  try {
15
16
  const result = fn(testRegistry);
16
17
  if (result instanceof Promise) return result.then((value) => {
17
- require_ensure.setRegistry(previous);
18
+ require_StateContainer.setRegistry(previous);
18
19
  return value;
19
20
  }, (error) => {
20
- require_ensure.setRegistry(previous);
21
+ require_StateContainer.setRegistry(previous);
21
22
  throw error;
22
23
  });
23
- require_ensure.setRegistry(previous);
24
+ require_StateContainer.setRegistry(previous);
24
25
  return result;
25
26
  } catch (error) {
26
- require_ensure.setRegistry(previous);
27
+ require_StateContainer.setRegistry(previous);
27
28
  throw error;
28
29
  }
29
30
  }
30
31
  function blacTestSetup() {
31
32
  let savedRegistry;
33
+ let testRegistry;
32
34
  beforeEach(() => {
33
- savedRegistry = require_ensure.getRegistry();
34
- require_ensure.setRegistry(new require_StateContainerRegistry.StateContainerRegistry());
35
+ savedRegistry = require_StateContainer.getRegistry();
36
+ testRegistry = createTestRegistry();
37
+ require_StateContainer.setRegistry(testRegistry);
35
38
  });
36
39
  afterEach(() => {
37
- require_ensure.setRegistry(savedRegistry);
40
+ testRegistry.clearAll();
41
+ require_StateContainer.setRegistry(savedRegistry);
38
42
  });
39
43
  }
40
44
  function registerOverride(BlocClass, instance, args) {
41
- const registry = require_ensure.getRegistry();
45
+ const registry = require_StateContainer.getRegistry();
42
46
  const key = require_ensure.resolveInstanceKey(BlocClass, args);
43
- registry.insertInstance(BlocClass, key, instance, new Map([["testing-override", 1]]));
47
+ registry[require_StateContainerRegistry.INSERT_INSTANCE](BlocClass, key, instance, new Map([["testing-override", 1]]));
44
48
  }
45
49
  function overrideEnsure(BlocClass, instance, fn, args) {
46
50
  return withTestRegistry(() => {
@@ -50,14 +54,8 @@ function overrideEnsure(BlocClass, instance, fn, args) {
50
54
  }
51
55
  function createCubitStub(BlocClass, options) {
52
56
  const instance = new BlocClass();
53
- if (options?.args != null) instance[require_StateContainerRegistry.INIT_CONFIG]({ args: options.args });
54
- if (options?.state != null) {
55
- if (instance instanceof require_ensure.Cubit) {
56
- const currentState = instance.state;
57
- if (typeof currentState === "object" && currentState !== null && typeof options.state === "object" && options.state !== null) instance.patch(options.state);
58
- else instance.emit(options.state);
59
- }
60
- }
57
+ instance[require_StateContainerRegistry.INIT_CONFIG]({ args: options?.args });
58
+ if (options?.state != null) applyState(instance, options.state);
61
59
  if (options?.methods) {
62
60
  for (const [key, impl] of Object.entries(options.methods)) if (typeof impl === "function") instance[key] = impl;
63
61
  }
@@ -66,31 +64,35 @@ function createCubitStub(BlocClass, options) {
66
64
  }
67
65
  function withBlocState(BlocClass, state, args) {
68
66
  const instance = require_ensure.ensure(BlocClass, { args });
69
- if (instance instanceof require_ensure.Cubit) {
70
- const currentState = instance.state;
71
- if (typeof currentState === "object" && currentState !== null && typeof state === "object" && state !== null) instance.patch(state);
72
- else instance.emit(state);
73
- }
67
+ applyState(instance, state);
74
68
  return instance;
75
69
  }
70
+ function applyState(instance, state) {
71
+ const target = instance;
72
+ if (typeof target.state === "object" && target.state !== null && typeof state === "object" && state !== null) target.patch(state);
73
+ else target.emit(state);
74
+ }
76
75
  function withBlocMethod(BlocClass, methodName, impl, args) {
77
76
  const instance = require_ensure.ensure(BlocClass, { args });
78
77
  instance[methodName] = impl;
79
78
  return instance;
80
79
  }
81
80
  /**
82
- * Drain pending microtasks so any channel-flushed effects (channel
83
- * subscribers, `onSystemEvent('stateChanged')` handlers, plugin hooks) run
84
- * before the next assertion.
85
- *
86
- * The default `MicrotaskScheduler` coalesces emits within a tick; tests
87
- * that emit and then assert on subscriber side-effects need `await flush()`
88
- * between the two.
81
+ * Drain pending microtasks so channel-flushed effects (subscribers,
82
+ * `onSystemEvent('stateChanged')` handlers, plugin hooks) run before the
83
+ * next assertion. A subscriber can emit again, so this keeps draining until
84
+ * a round passes with no new state change.
89
85
  */
90
86
  async function flush() {
91
- await Promise.resolve();
92
- await Promise.resolve();
87
+ for (let i = 0; i < MAX_FLUSH_ROUNDS; i++) {
88
+ const before = require_StateContainer.getStateChangeCount();
89
+ await Promise.resolve();
90
+ await Promise.resolve();
91
+ if (require_StateContainer.getStateChangeCount() === before) return;
92
+ }
93
+ throw new Error(`[blac] flush(): state was still changing after ${MAX_FLUSH_ROUNDS} rounds`);
93
94
  }
95
+ const MAX_FLUSH_ROUNDS = 100;
94
96
  //#endregion
95
97
  exports.blacTestSetup = blacTestSetup;
96
98
  exports.createCubitStub = createCubitStub;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.cjs","names":["StateContainerRegistry","getRegistry","resolveInstanceKey","INIT_CONFIG","Cubit","APPLY_DEPS","ensure"],"sources":["../src/testing.ts"],"sourcesContent":["import { Cubit } from './core/Cubit';\nimport { StateContainerRegistry } from './core/StateContainerRegistry';\nimport { APPLY_DEPS, INIT_CONFIG } from './core/symbols';\nimport { ensure, getRegistry, setRegistry } from './registry';\nimport { resolveInstanceKey } from './registry/acquire';\nimport type {\n ExtractArgs,\n ExtractDeps,\n ExtractState,\n StateContainerConstructor,\n} from './types/utilities';\n\n/** Synthetic owner id used by test helpers for pre-wired deps. */\nconst TESTING_DEPS_OWNER = 'testing-deps';\n\ndeclare const beforeEach: (fn: () => void) => void;\ndeclare const afterEach: (fn: () => void) => void;\n\n// --- createTestRegistry + withTestRegistry ---\n\nexport function createTestRegistry(): StateContainerRegistry {\n return new StateContainerRegistry();\n}\n\nexport function withTestRegistry<T>(\n fn: (registry: StateContainerRegistry) => T,\n): T {\n const previous = getRegistry();\n const testRegistry = createTestRegistry();\n setRegistry(testRegistry);\n try {\n const result = fn(testRegistry);\n if (result instanceof Promise) {\n return result.then(\n (value) => {\n setRegistry(previous);\n return value;\n },\n (error) => {\n setRegistry(previous);\n throw error;\n },\n ) as T;\n }\n setRegistry(previous);\n return result;\n } catch (error) {\n setRegistry(previous);\n throw error;\n }\n}\n\n// --- blacTestSetup ---\n\nexport function blacTestSetup(): void {\n let savedRegistry: StateContainerRegistry;\n beforeEach(() => {\n savedRegistry = getRegistry();\n setRegistry(new StateContainerRegistry());\n });\n afterEach(() => {\n setRegistry(savedRegistry);\n });\n}\n\n// --- registerOverride + overrideEnsure ---\n\nexport function registerOverride<T extends StateContainerConstructor>(\n BlocClass: T,\n instance: InstanceType<T>,\n args?: ExtractArgs<T>,\n): void {\n const registry = getRegistry();\n const key = resolveInstanceKey(BlocClass, args);\n registry.insertInstance(\n BlocClass,\n key,\n instance,\n new Map([['testing-override', 1]]),\n );\n}\n\nexport function overrideEnsure<T extends StateContainerConstructor, R>(\n BlocClass: T,\n instance: InstanceType<T>,\n fn: () => R,\n args?: ExtractArgs<T>,\n): R {\n return withTestRegistry(() => {\n registerOverride(BlocClass, instance, args);\n return fn();\n });\n}\n\n// --- createCubitStub ---\n\ntype MethodKeys<T> = {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;\n}[keyof T];\n\nexport interface CubitStubOptions<T extends StateContainerConstructor> {\n state?: ExtractState<T> extends Record<string, any>\n ? Partial<ExtractState<T>>\n : ExtractState<T>;\n methods?: Partial<\n Record<MethodKeys<InstanceType<T>>, (...args: any[]) => any>\n >;\n /**\n * Args to pass to init(). If the bloc's Args type is not void, supplying\n * args here causes `initConfig({ args })` to be called so `init()` runs.\n */\n args?: ExtractArgs<T> extends void ? never : ExtractArgs<T>;\n /**\n * Deps slice to pre-wire via the core [APPLY_DEPS] path (synthetic owner\n * \"testing-deps\"), so onDepsChanged fires during tests.\n */\n deps?: Partial<ExtractDeps<T>>;\n}\n\nexport function createCubitStub<T extends StateContainerConstructor>(\n BlocClass: T,\n options?: CubitStubOptions<T>,\n): InstanceType<T> {\n const instance = new BlocClass() as InstanceType<T>;\n\n // Run init() if args are supplied — goes through the same initConfig path\n // that the registry uses, so lifecycle hooks fire correctly.\n if (options?.args != null) {\n instance[INIT_CONFIG]({ args: options.args });\n }\n\n if (options?.state != null) {\n if (instance instanceof Cubit) {\n const currentState = instance.state;\n if (\n typeof currentState === 'object' &&\n currentState !== null &&\n typeof options.state === 'object' &&\n options.state !== null\n ) {\n instance.patch(options.state as any);\n } else {\n instance.emit(options.state as any);\n }\n }\n }\n if (options?.methods) {\n for (const [key, impl] of Object.entries(options.methods)) {\n if (typeof impl === 'function') {\n (instance as any)[key] = impl;\n }\n }\n }\n\n // Pre-wire deps via the core merge path so onDepsChanged fires in tests.\n if (options?.deps != null) {\n (instance as any)[APPLY_DEPS](TESTING_DEPS_OWNER, options.deps);\n }\n\n return instance;\n}\n\n// --- withBlocState ---\n\nexport function withBlocState<T extends StateContainerConstructor>(\n BlocClass: T,\n state: ExtractState<T> extends Record<string, any>\n ? Partial<ExtractState<T>>\n : ExtractState<T>,\n args?: ExtractArgs<T>,\n): InstanceType<T> {\n const instance = ensure(BlocClass, { args });\n if (instance instanceof Cubit) {\n const currentState = instance.state;\n if (\n typeof currentState === 'object' &&\n currentState !== null &&\n typeof state === 'object' &&\n state !== null\n ) {\n instance.patch(state as any);\n } else {\n instance.emit(state as any);\n }\n }\n return instance;\n}\n\n// --- withBlocMethod ---\n\nexport function withBlocMethod<T extends StateContainerConstructor>(\n BlocClass: T,\n methodName: keyof InstanceType<T>,\n impl: (...args: any[]) => any,\n args?: ExtractArgs<T>,\n): InstanceType<T> {\n const instance = ensure(BlocClass, { args });\n (instance as any)[methodName] = impl;\n return instance;\n}\n\n// --- flush ---\n\n/**\n * Drain pending microtasks so any channel-flushed effects (channel\n * subscribers, `onSystemEvent('stateChanged')` handlers, plugin hooks) run\n * before the next assertion.\n *\n * The default `MicrotaskScheduler` coalesces emits within a tick; tests\n * that emit and then assert on subscriber side-effects need `await flush()`\n * between the two.\n */\nexport async function flush(): Promise<void> {\n // queueMicrotask wins the race against the channel's own queued flush —\n // we resolve after the channel has drained its current pending flush.\n await Promise.resolve();\n await Promise.resolve();\n}\n"],"mappings":";;;;;AAaA,MAAM,qBAAqB;AAO3B,SAAgB,qBAA6C;CAC3D,OAAO,IAAIA,+BAAAA,uBAAuB;AACpC;AAEA,SAAgB,iBACd,IACG;CACH,MAAM,WAAWC,eAAAA,YAAY;CAC7B,MAAM,eAAe,mBAAmB;CACxC,eAAA,YAAY,YAAY;CACxB,IAAI;EACF,MAAM,SAAS,GAAG,YAAY;EAC9B,IAAI,kBAAkB,SACpB,OAAO,OAAO,MACX,UAAU;GACT,eAAA,YAAY,QAAQ;GACpB,OAAO;EACT,IACC,UAAU;GACT,eAAA,YAAY,QAAQ;GACpB,MAAM;EACR,CACF;EAEF,eAAA,YAAY,QAAQ;EACpB,OAAO;CACT,SAAS,OAAO;EACd,eAAA,YAAY,QAAQ;EACpB,MAAM;CACR;AACF;AAIA,SAAgB,gBAAsB;CACpC,IAAI;CACJ,iBAAiB;EACf,gBAAgBA,eAAAA,YAAY;EAC5B,eAAA,YAAY,IAAID,+BAAAA,uBAAuB,CAAC;CAC1C,CAAC;CACD,gBAAgB;EACd,eAAA,YAAY,aAAa;CAC3B,CAAC;AACH;AAIA,SAAgB,iBACd,WACA,UACA,MACM;CACN,MAAM,WAAWC,eAAAA,YAAY;CAC7B,MAAM,MAAMC,eAAAA,mBAAmB,WAAW,IAAI;CAC9C,SAAS,eACP,WACA,KACA,UACA,IAAI,IAAI,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CACnC;AACF;AAEA,SAAgB,eACd,WACA,UACA,IACA,MACG;CACH,OAAO,uBAAuB;EAC5B,iBAAiB,WAAW,UAAU,IAAI;EAC1C,OAAO,GAAG;CACZ,CAAC;AACH;AA2BA,SAAgB,gBACd,WACA,SACiB;CACjB,MAAM,WAAW,IAAI,UAAU;CAI/B,IAAI,SAAS,QAAQ,MACnB,SAASC,+BAAAA,aAAa,EAAE,MAAM,QAAQ,KAAK,CAAC;CAG9C,IAAI,SAAS,SAAS;MAChB,oBAAoBC,eAAAA,OAAO;GAC7B,MAAM,eAAe,SAAS;GAC9B,IACE,OAAO,iBAAiB,YACxB,iBAAiB,QACjB,OAAO,QAAQ,UAAU,YACzB,QAAQ,UAAU,MAElB,SAAS,MAAM,QAAQ,KAAY;QAEnC,SAAS,KAAK,QAAQ,KAAY;EAEtC;;CAEF,IAAI,SAAS;OACN,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,QAAQ,OAAO,GACtD,IAAI,OAAO,SAAS,YAClB,SAAkB,OAAO;CAAA;CAM/B,IAAI,SAAS,QAAQ,MACnB,SAAkBC,+BAAAA,YAAY,oBAAoB,QAAQ,IAAI;CAGhE,OAAO;AACT;AAIA,SAAgB,cACd,WACA,OAGA,MACiB;CACjB,MAAM,WAAWC,eAAAA,OAAO,WAAW,EAAE,KAAK,CAAC;CAC3C,IAAI,oBAAoBF,eAAAA,OAAO;EAC7B,MAAM,eAAe,SAAS;EAC9B,IACE,OAAO,iBAAiB,YACxB,iBAAiB,QACjB,OAAO,UAAU,YACjB,UAAU,MAEV,SAAS,MAAM,KAAY;OAE3B,SAAS,KAAK,KAAY;CAE9B;CACA,OAAO;AACT;AAIA,SAAgB,eACd,WACA,YACA,MACA,MACiB;CACjB,MAAM,WAAWE,eAAAA,OAAO,WAAW,EAAE,KAAK,CAAC;CAC3C,SAAkB,cAAc;CAChC,OAAO;AACT;;;;;;;;;;AAaA,eAAsB,QAAuB;CAG3C,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,QAAQ;AACxB"}
1
+ {"version":3,"file":"testing.cjs","names":["StateContainerRegistry","getRegistry","resolveInstanceKey","INSERT_INSTANCE","INIT_CONFIG","APPLY_DEPS","ensure","getStateChangeCount"],"sources":["../src/testing.ts"],"sourcesContent":["import type { Cubit } from './core/Cubit';\nimport { getStateChangeCount } from './core/StateContainer';\nimport { StateContainerRegistry } from './core/StateContainerRegistry';\nimport { APPLY_DEPS, INIT_CONFIG, INSERT_INSTANCE } from './core/symbols';\nimport { ensure, getRegistry, setRegistry } from './registry';\nimport { resolveInstanceKey } from './registry/acquire';\nimport type {\n ExtractArgs,\n ExtractDeps,\n ExtractState,\n StateContainerConstructor,\n} from './types/utilities';\n\n/** Synthetic owner id used by test helpers for pre-wired deps. */\nconst TESTING_DEPS_OWNER = 'testing-deps';\n\ndeclare const beforeEach: (fn: () => void) => void;\ndeclare const afterEach: (fn: () => void) => void;\n\nexport function createTestRegistry(): StateContainerRegistry {\n return new StateContainerRegistry();\n}\n\nexport function withTestRegistry<T>(\n fn: (registry: StateContainerRegistry) => T,\n): T {\n const previous = getRegistry();\n const testRegistry = createTestRegistry();\n setRegistry(testRegistry);\n try {\n const result = fn(testRegistry);\n if (result instanceof Promise) {\n return result.then(\n (value) => {\n setRegistry(previous);\n return value;\n },\n (error) => {\n setRegistry(previous);\n throw error;\n },\n ) as T;\n }\n setRegistry(previous);\n return result;\n } catch (error) {\n setRegistry(previous);\n throw error;\n }\n}\n\nexport function blacTestSetup(): void {\n let savedRegistry: StateContainerRegistry;\n let testRegistry: StateContainerRegistry;\n beforeEach(() => {\n savedRegistry = getRegistry();\n testRegistry = createTestRegistry();\n setRegistry(testRegistry);\n });\n afterEach(() => {\n testRegistry.clearAll();\n setRegistry(savedRegistry);\n });\n}\n\nexport function registerOverride<T extends StateContainerConstructor>(\n BlocClass: T,\n instance: InstanceType<T>,\n args?: ExtractArgs<T>,\n): void {\n const registry = getRegistry();\n const key = resolveInstanceKey(BlocClass, args);\n registry[INSERT_INSTANCE](\n BlocClass,\n key,\n instance,\n new Map([['testing-override', 1]]),\n );\n}\n\nexport function overrideEnsure<T extends StateContainerConstructor, R>(\n BlocClass: T,\n instance: InstanceType<T>,\n fn: () => R,\n args?: ExtractArgs<T>,\n): R {\n return withTestRegistry(() => {\n registerOverride(BlocClass, instance, args);\n return fn();\n });\n}\n\ntype MethodKeys<T> = {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;\n}[keyof T];\n\nexport interface CubitStubOptions<T extends StateContainerConstructor> {\n state?: ExtractState<T> extends Record<string, any>\n ? Partial<ExtractState<T>>\n : ExtractState<T>;\n methods?: Partial<\n Record<MethodKeys<InstanceType<T>>, (...args: any[]) => any>\n >;\n /** Args passed to init(). */\n args?: ExtractArgs<T> extends void ? never : ExtractArgs<T>;\n /**\n * Deps slice to pre-wire via the core [APPLY_DEPS] path (synthetic owner\n * \"testing-deps\"), so onDepsChanged fires during tests.\n */\n deps?: Partial<ExtractDeps<T>>;\n}\n\nexport function createCubitStub<T extends StateContainerConstructor>(\n BlocClass: T,\n options?: CubitStubOptions<T>,\n): InstanceType<T> {\n const instance = new BlocClass() as InstanceType<T>;\n instance[INIT_CONFIG]({ args: options?.args });\n\n if (options?.state != null) {\n applyState(instance, options.state);\n }\n if (options?.methods) {\n for (const [key, impl] of Object.entries(options.methods)) {\n if (typeof impl === 'function') {\n (instance as any)[key] = impl;\n }\n }\n }\n\n // Pre-wire deps via the core merge path so onDepsChanged fires in tests.\n if (options?.deps != null) {\n (instance as any)[APPLY_DEPS](TESTING_DEPS_OWNER, options.deps);\n }\n\n return instance;\n}\n\nexport function withBlocState<T extends StateContainerConstructor>(\n BlocClass: T,\n state: ExtractState<T> extends Record<string, any>\n ? Partial<ExtractState<T>>\n : ExtractState<T>,\n args?: ExtractArgs<T>,\n): InstanceType<T> {\n const instance = ensure(BlocClass, { args });\n applyState(instance, state);\n return instance;\n}\n\n// emit/patch are protected on StateContainer; Cubit only makes them public.\nfunction applyState(instance: object, state: unknown): void {\n const target = instance as Cubit<any>;\n if (\n typeof target.state === 'object' &&\n target.state !== null &&\n typeof state === 'object' &&\n state !== null\n ) {\n target.patch(state);\n } else {\n target.emit(state as any);\n }\n}\n\nexport function withBlocMethod<T extends StateContainerConstructor>(\n BlocClass: T,\n methodName: keyof InstanceType<T>,\n impl: (...args: any[]) => any,\n args?: ExtractArgs<T>,\n): InstanceType<T> {\n const instance = ensure(BlocClass, { args });\n (instance as any)[methodName] = impl;\n return instance;\n}\n\n/**\n * Drain pending microtasks so channel-flushed effects (subscribers,\n * `onSystemEvent('stateChanged')` handlers, plugin hooks) run before the\n * next assertion. A subscriber can emit again, so this keeps draining until\n * a round passes with no new state change.\n */\nexport async function flush(): Promise<void> {\n for (let i = 0; i < MAX_FLUSH_ROUNDS; i++) {\n const before = getStateChangeCount();\n // The second tick lets microtasks queued by the drain itself (registry\n // `stateChanged` delivery) run too.\n await Promise.resolve();\n await Promise.resolve();\n if (getStateChangeCount() === before) return;\n }\n throw new Error(\n `[blac] flush(): state was still changing after ${MAX_FLUSH_ROUNDS} rounds`,\n );\n}\n\nconst MAX_FLUSH_ROUNDS = 100;\n"],"mappings":";;;;;;AAcA,MAAM,qBAAqB;AAK3B,SAAgB,qBAA6C;CAC3D,OAAO,IAAIA,+BAAAA,uBAAuB;AACpC;AAEA,SAAgB,iBACd,IACG;CACH,MAAM,WAAWC,uBAAAA,YAAY;CAC7B,MAAM,eAAe,mBAAmB;CACxC,uBAAA,YAAY,YAAY;CACxB,IAAI;EACF,MAAM,SAAS,GAAG,YAAY;EAC9B,IAAI,kBAAkB,SACpB,OAAO,OAAO,MACX,UAAU;GACT,uBAAA,YAAY,QAAQ;GACpB,OAAO;EACT,IACC,UAAU;GACT,uBAAA,YAAY,QAAQ;GACpB,MAAM;EACR,CACF;EAEF,uBAAA,YAAY,QAAQ;EACpB,OAAO;CACT,SAAS,OAAO;EACd,uBAAA,YAAY,QAAQ;EACpB,MAAM;CACR;AACF;AAEA,SAAgB,gBAAsB;CACpC,IAAI;CACJ,IAAI;CACJ,iBAAiB;EACf,gBAAgBA,uBAAAA,YAAY;EAC5B,eAAe,mBAAmB;EAClC,uBAAA,YAAY,YAAY;CAC1B,CAAC;CACD,gBAAgB;EACd,aAAa,SAAS;EACtB,uBAAA,YAAY,aAAa;CAC3B,CAAC;AACH;AAEA,SAAgB,iBACd,WACA,UACA,MACM;CACN,MAAM,WAAWA,uBAAAA,YAAY;CAC7B,MAAM,MAAMC,eAAAA,mBAAmB,WAAW,IAAI;CAC9C,SAASC,+BAAAA,iBACP,WACA,KACA,UACA,IAAI,IAAI,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CACnC;AACF;AAEA,SAAgB,eACd,WACA,UACA,IACA,MACG;CACH,OAAO,uBAAuB;EAC5B,iBAAiB,WAAW,UAAU,IAAI;EAC1C,OAAO,GAAG;CACZ,CAAC;AACH;AAsBA,SAAgB,gBACd,WACA,SACiB;CACjB,MAAM,WAAW,IAAI,UAAU;CAC/B,SAASC,+BAAAA,aAAa,EAAE,MAAM,SAAS,KAAK,CAAC;CAE7C,IAAI,SAAS,SAAS,MACpB,WAAW,UAAU,QAAQ,KAAK;CAEpC,IAAI,SAAS;OACN,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,QAAQ,OAAO,GACtD,IAAI,OAAO,SAAS,YAClB,SAAkB,OAAO;CAAA;CAM/B,IAAI,SAAS,QAAQ,MACnB,SAAkBC,+BAAAA,YAAY,oBAAoB,QAAQ,IAAI;CAGhE,OAAO;AACT;AAEA,SAAgB,cACd,WACA,OAGA,MACiB;CACjB,MAAM,WAAWC,eAAAA,OAAO,WAAW,EAAE,KAAK,CAAC;CAC3C,WAAW,UAAU,KAAK;CAC1B,OAAO;AACT;AAGA,SAAS,WAAW,UAAkB,OAAsB;CAC1D,MAAM,SAAS;CACf,IACE,OAAO,OAAO,UAAU,YACxB,OAAO,UAAU,QACjB,OAAO,UAAU,YACjB,UAAU,MAEV,OAAO,MAAM,KAAK;MAElB,OAAO,KAAK,KAAY;AAE5B;AAEA,SAAgB,eACd,WACA,YACA,MACA,MACiB;CACjB,MAAM,WAAWA,eAAAA,OAAO,WAAW,EAAE,KAAK,CAAC;CAC3C,SAAkB,cAAc;CAChC,OAAO;AACT;;;;;;;AAQA,eAAsB,QAAuB;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,KAAK;EACzC,MAAM,SAASC,uBAAAA,oBAAoB;EAGnC,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAIA,uBAAAA,oBAAoB,MAAM,QAAQ;CACxC;CACA,MAAM,IAAI,MACR,kDAAkD,iBAAiB,QACrE;AACF;AAEA,MAAM,mBAAmB"}
@@ -11,10 +11,7 @@ type MethodKeys<T> = {
11
11
  export interface CubitStubOptions<T extends StateContainerConstructor> {
12
12
  state?: ExtractState<T> extends Record<string, any> ? Partial<ExtractState<T>> : ExtractState<T>;
13
13
  methods?: Partial<Record<MethodKeys<InstanceType<T>>, (...args: any[]) => any>>;
14
- /**
15
- * Args to pass to init(). If the bloc's Args type is not void, supplying
16
- * args here causes `initConfig({ args })` to be called so `init()` runs.
17
- */
14
+ /** Args passed to init(). */
18
15
  args?: ExtractArgs<T> extends void ? never : ExtractArgs<T>;
19
16
  /**
20
17
  * Deps slice to pre-wire via the core [APPLY_DEPS] path (synthetic owner
@@ -26,13 +23,10 @@ export declare function createCubitStub<T extends StateContainerConstructor>(Blo
26
23
  export declare function withBlocState<T extends StateContainerConstructor>(BlocClass: T, state: ExtractState<T> extends Record<string, any> ? Partial<ExtractState<T>> : ExtractState<T>, args?: ExtractArgs<T>): InstanceType<T>;
27
24
  export declare function withBlocMethod<T extends StateContainerConstructor>(BlocClass: T, methodName: keyof InstanceType<T>, impl: (...args: any[]) => any, args?: ExtractArgs<T>): InstanceType<T>;
28
25
  /**
29
- * Drain pending microtasks so any channel-flushed effects (channel
30
- * subscribers, `onSystemEvent('stateChanged')` handlers, plugin hooks) run
31
- * before the next assertion.
32
- *
33
- * The default `MicrotaskScheduler` coalesces emits within a tick; tests
34
- * that emit and then assert on subscriber side-effects need `await flush()`
35
- * between the two.
26
+ * Drain pending microtasks so channel-flushed effects (subscribers,
27
+ * `onSystemEvent('stateChanged')` handlers, plugin hooks) run before the
28
+ * next assertion. A subscriber can emit again, so this keeps draining until
29
+ * a round passes with no new state change.
36
30
  */
37
31
  export declare function flush(): Promise<void>;
38
32
  export {};
package/dist/testing.d.ts CHANGED
@@ -11,10 +11,7 @@ type MethodKeys<T> = {
11
11
  export interface CubitStubOptions<T extends StateContainerConstructor> {
12
12
  state?: ExtractState<T> extends Record<string, any> ? Partial<ExtractState<T>> : ExtractState<T>;
13
13
  methods?: Partial<Record<MethodKeys<InstanceType<T>>, (...args: any[]) => any>>;
14
- /**
15
- * Args to pass to init(). If the bloc's Args type is not void, supplying
16
- * args here causes `initConfig({ args })` to be called so `init()` runs.
17
- */
14
+ /** Args passed to init(). */
18
15
  args?: ExtractArgs<T> extends void ? never : ExtractArgs<T>;
19
16
  /**
20
17
  * Deps slice to pre-wire via the core [APPLY_DEPS] path (synthetic owner
@@ -26,13 +23,10 @@ export declare function createCubitStub<T extends StateContainerConstructor>(Blo
26
23
  export declare function withBlocState<T extends StateContainerConstructor>(BlocClass: T, state: ExtractState<T> extends Record<string, any> ? Partial<ExtractState<T>> : ExtractState<T>, args?: ExtractArgs<T>): InstanceType<T>;
27
24
  export declare function withBlocMethod<T extends StateContainerConstructor>(BlocClass: T, methodName: keyof InstanceType<T>, impl: (...args: any[]) => any, args?: ExtractArgs<T>): InstanceType<T>;
28
25
  /**
29
- * Drain pending microtasks so any channel-flushed effects (channel
30
- * subscribers, `onSystemEvent('stateChanged')` handlers, plugin hooks) run
31
- * before the next assertion.
32
- *
33
- * The default `MicrotaskScheduler` coalesces emits within a tick; tests
34
- * that emit and then assert on subscriber side-effects need `await flush()`
35
- * between the two.
26
+ * Drain pending microtasks so channel-flushed effects (subscribers,
27
+ * `onSystemEvent('stateChanged')` handlers, plugin hooks) run before the
28
+ * next assertion. A subscriber can emit again, so this keeps draining until
29
+ * a round passes with no new state change.
36
30
  */
37
31
  export declare function flush(): Promise<void>;
38
32
  export {};