@blac/core 2.0.21 → 2.0.22

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 (37) hide show
  1. package/dist/StateContainerRegistry-141uHhtF.cjs.map +1 -1
  2. package/dist/StateContainerRegistry-BH7aOMWV.js +892 -0
  3. package/dist/StateContainerRegistry-BH7aOMWV.js.map +1 -0
  4. package/dist/StateContainerRegistry-BsH_s0pJ.js +895 -0
  5. package/dist/StateContainerRegistry-BsH_s0pJ.js.map +1 -0
  6. package/dist/StateContainerRegistry-CBLeHObO.cjs +1002 -0
  7. package/dist/StateContainerRegistry-CBLeHObO.cjs.map +1 -0
  8. package/dist/StateContainerRegistry-MkT0W4C2.cjs +999 -0
  9. package/dist/StateContainerRegistry-MkT0W4C2.cjs.map +1 -0
  10. package/dist/StateContainerRegistry-vWgsD-RP.js.map +1 -1
  11. package/dist/constants.d.cts +0 -7
  12. package/dist/constants.d.ts +0 -7
  13. package/dist/core/StateContainer.d.ts +16 -7
  14. package/dist/core/StateContainerRegistry.d.ts +9 -4
  15. package/dist/core/meta.d.ts +34 -41
  16. package/dist/ensure-BjhbDW8Z.cjs +761 -0
  17. package/dist/ensure-BjhbDW8Z.cjs.map +1 -0
  18. package/dist/ensure-BnVyd6s4.js +730 -0
  19. package/dist/ensure-BnVyd6s4.js.map +1 -0
  20. package/dist/ensure-CneMkpbI.js +731 -0
  21. package/dist/ensure-CneMkpbI.js.map +1 -0
  22. package/dist/ensure-D15vGnk5.cjs +783 -0
  23. package/dist/ensure-D15vGnk5.cjs.map +1 -0
  24. package/dist/ensure-D3w6YGp9.cjs +784 -0
  25. package/dist/ensure-D3w6YGp9.cjs.map +1 -0
  26. package/dist/ensure-dsqWhIuO.js +708 -0
  27. package/dist/ensure-dsqWhIuO.js.map +1 -0
  28. package/dist/index.cjs +2 -2
  29. package/dist/index.js +2 -2
  30. package/dist/plugin/PluginManager.d.ts +9 -4
  31. package/dist/plugins.cjs +7 -4
  32. package/dist/plugins.cjs.map +1 -1
  33. package/dist/plugins.js +7 -4
  34. package/dist/plugins.js.map +1 -1
  35. package/dist/testing.cjs +2 -2
  36. package/dist/testing.js +2 -2
  37. package/package.json +2 -3
@@ -0,0 +1,895 @@
1
+ //#region src/config.ts
2
+ /**
3
+ * Default equality: shallow per-key `Object.is` comparison.
4
+ * Falls through to `false` for primitives/null so reference-only state still
5
+ * behaves correctly.
6
+ */
7
+ const shallowEqualState = (prev, next) => {
8
+ if (Object.is(prev, next)) return true;
9
+ if (typeof prev !== "object" || prev === null) return false;
10
+ if (typeof next !== "object" || next === null) return false;
11
+ const p = prev;
12
+ const n = next;
13
+ let prevCount = 0;
14
+ for (const key in p) {
15
+ prevCount++;
16
+ if (!Object.is(p[key], n[key])) return false;
17
+ }
18
+ let nextCount = 0;
19
+ for (const _ in n) nextCount++;
20
+ return prevCount === nextCount;
21
+ };
22
+ const defaultConfig = {
23
+ equality: shallowEqualState,
24
+ maxInstancesPerType: 1e5,
25
+ maxRefsPerInstance: 1e5,
26
+ maxEmitsPerSecond: 1e3
27
+ };
28
+ let globalConfig = { ...defaultConfig };
29
+ /**
30
+ * Configure global defaults for `@blac/core`.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { configureBlac, shallowEqualState } from '@blac/core';
35
+ *
36
+ * // Swap in a deep-equality check for all blocs
37
+ * configureBlac({ equality: myDeepEqual });
38
+ * ```
39
+ */
40
+ function configureBlac(config) {
41
+ globalConfig = {
42
+ ...globalConfig,
43
+ ...config
44
+ };
45
+ }
46
+ /**
47
+ * Get the current global configuration.
48
+ * @internal
49
+ */
50
+ function getBlacConfig() {
51
+ return globalConfig;
52
+ }
53
+ /**
54
+ * Reset configuration to defaults (useful for testing).
55
+ * @internal
56
+ */
57
+ function resetBlacConfig() {
58
+ globalConfig = { ...defaultConfig };
59
+ }
60
+ //#endregion
61
+ //#region src/constants.ts
62
+ /**
63
+ * Default configuration constants for BlaC
64
+ *
65
+ * Centralized location for all magic numbers and default values.
66
+ */
67
+ /**
68
+ * Static property names for StateContainer classes
69
+ * Used for feature flags and configuration on bloc classes
70
+ */
71
+ const BLAC_STATIC_PROPS = {
72
+ /**
73
+ * Mark a bloc to never be auto-disposed (kept alive permanently)
74
+ */
75
+ KEEP_ALIVE: "keepAlive",
76
+ /**
77
+ * Exclude a bloc from DevTools reporting (prevents infinite loops)
78
+ */
79
+ EXCLUDE_FROM_DEVTOOLS: "__excludeFromDevTools",
80
+ /**
81
+ * Per-class equality function used to short-circuit `emit`/`patch` when the
82
+ * new state is structurally equal to the previous state. Falls back to the
83
+ * global `configureBlac({ equality })` value when unset.
84
+ */
85
+ EQUALITY: "__equality",
86
+ /**
87
+ * Per-class key function: `static key = (args) => string`.
88
+ * When set, the registry calls `Type.key(args)` to derive the instance key
89
+ * instead of using a structural hash of `args`. Allows blocs to ignore
90
+ * non-identity fields (e.g., `readonly` flags) while still keying by id.
91
+ */
92
+ KEY: "key",
93
+ /**
94
+ * Explicit, minification-safe identity for a bloc class: `static blacName = '...'`.
95
+ * When unset, identity falls back to `constructor.name`.
96
+ */
97
+ BLAC_NAME: "blacName"
98
+ };
99
+ /**
100
+ * Standard error message prefix
101
+ */
102
+ const BLAC_ERROR_PREFIX = "[BlaC]";
103
+ /**
104
+ * True only when `process.env.NODE_ENV` is defined and not `'production'`.
105
+ *
106
+ * Dev-only checks (emit-rate breaker, deps collision scan, args-mismatch key)
107
+ * cost real work per emit/acquire, so an environment that says nothing about
108
+ * its mode — plain ESM in the browser, Deno, a bundle without a `process`
109
+ * shim — is treated as production. Bundlers that define `process.env.NODE_ENV`
110
+ * still constant-fold the read.
111
+ */
112
+ const nodeEnv = (() => {
113
+ try {
114
+ return process.env.NODE_ENV;
115
+ } catch {
116
+ return;
117
+ }
118
+ })();
119
+ const IS_DEV = nodeEnv !== void 0 && nodeEnv !== "production";
120
+ //#endregion
121
+ //#region src/utils/static-props.ts
122
+ /**
123
+ * Utility functions for accessing static properties on StateContainer classes
124
+ */
125
+ /**
126
+ * Get a static property from a class constructor, walking the prototype
127
+ * chain so subclasses inherit the value unless they define their own.
128
+ * Type-safe helper that avoids (Type as any) casts
129
+ *
130
+ * @param Type - The class constructor
131
+ * @param propName - The property name to access
132
+ * @param defaultValue - Optional default value if property is undefined
133
+ * @returns The property value or default
134
+ */
135
+ function getStaticProp(Type, propName, defaultValue) {
136
+ return Type[propName] ?? defaultValue;
137
+ }
138
+ /**
139
+ * Get a static property declared directly on `Type`, ignoring the prototype
140
+ * chain, so subclasses never inherit a base class's value.
141
+ *
142
+ * @param Type - The class constructor
143
+ * @param propName - The property name to access
144
+ * @returns The own property value or `undefined`
145
+ */
146
+ function getOwnStaticProp(Type, propName) {
147
+ return Object.hasOwn(Type, propName) ? Type[propName] : void 0;
148
+ }
149
+ /**
150
+ * Check if a class is marked as keepAlive.
151
+ * KeepAlive classes are never auto-disposed when ref count reaches 0.
152
+ * Inherited: a subclass keeps a base class's `keepAlive` unless overridden.
153
+ * @param Type - The class constructor to check
154
+ * @returns true if the class has `static keepAlive = true`
155
+ */
156
+ function isKeepAliveClass(Type) {
157
+ return getStaticProp(Type, BLAC_STATIC_PROPS.KEEP_ALIVE) === true;
158
+ }
159
+ /**
160
+ * Check if a class should be excluded from DevTools.
161
+ * Used to prevent infinite loops when DevTools tracks itself.
162
+ * Inherited: a subclass keeps a base class's exclusion unless overridden.
163
+ * @param Type - The class constructor to check
164
+ * @returns true if the class has `static __excludeFromDevTools = true`
165
+ */
166
+ function isExcludedFromDevTools(Type) {
167
+ return getStaticProp(Type, BLAC_STATIC_PROPS.EXCLUDE_FROM_DEVTOOLS) === true;
168
+ }
169
+ /**
170
+ * Get the per-class equality function set via `@blac({ equality })`, if any.
171
+ * Inherited: a subclass keeps a base class's equality fn unless overridden.
172
+ * @param Type - The class constructor to check
173
+ * @returns the equality function or `undefined`
174
+ */
175
+ function getClassEquality(Type) {
176
+ const fn = getStaticProp(Type, BLAC_STATIC_PROPS.EQUALITY);
177
+ return typeof fn === "function" ? fn : void 0;
178
+ }
179
+ /**
180
+ * Get a class's minification-safe identity name.
181
+ * Own only: a subclass never inherits a base class's explicit `blacName`,
182
+ * since that would collapse two distinct blocs onto one identity.
183
+ * Falls back to `Type.name` (the runtime `constructor.name`) when unset.
184
+ * @param Type - The class constructor to check
185
+ * @returns the explicit `static blacName` or `Type.name`
186
+ */
187
+ function getBlacName(Type) {
188
+ return getOwnStaticProp(Type, BLAC_STATIC_PROPS.BLAC_NAME) ?? Type.name;
189
+ }
190
+ /**
191
+ * Get the per-class key function set via `@blac({ key })` or `static key = …`, if any.
192
+ * Used by the registry to derive a stable instance key from `args`.
193
+ * Own only: a subclass never inherits a base class's `key`, since a
194
+ * different `Args` shape would derive the wrong instance key.
195
+ * @param Type - The class constructor to check
196
+ * @returns the key function or `undefined`
197
+ */
198
+ function getClassKey(Type) {
199
+ const fn = getOwnStaticProp(Type, BLAC_STATIC_PROPS.KEY);
200
+ return typeof fn === "function" ? fn : void 0;
201
+ }
202
+ //#endregion
203
+ //#region src/utils/structural-key.ts
204
+ /**
205
+ * Deterministic, order-independent hash of serializable args.
206
+ * Sorts object keys so {a,b} === {b,a}.
207
+ * Throws (dev) if it encounters a function or non-plain object — those belong in `deps`.
208
+ */
209
+ const DEFAULT_STRUCTURAL_KEY = "default";
210
+ /**
211
+ * Produce a stable string key for serializable args.
212
+ * - `undefined` / `void` args → `'default'` sentinel (no-args blocs stay on the default key).
213
+ * - Object keys are sorted for order-independence.
214
+ * - Arrays keep their insertion order.
215
+ * - Functions throw — they must be passed via `deps`, not `args`.
216
+ * - Object args are memoized by identity; callers on hot paths (`resolveKey`,
217
+ * `depend`) re-resolve the same args object on every call.
218
+ */
219
+ const keyCache = /* @__PURE__ */ new WeakMap();
220
+ function structuralKey(args) {
221
+ if (args === void 0 || args === null) return DEFAULT_STRUCTURAL_KEY;
222
+ if (typeof args === "object") {
223
+ const cached = keyCache.get(args);
224
+ if (cached !== void 0) return cached;
225
+ const key = serialize(args);
226
+ keyCache.set(args, key);
227
+ return key;
228
+ }
229
+ return serialize(args);
230
+ }
231
+ function serialize(args) {
232
+ return JSON.stringify(args, function(_k, v) {
233
+ if (typeof v === "function") throw new Error(`[blac] args must be serializable; put refs/callbacks in \`deps\` (found a function at key "${_k || "(root)"}")`);
234
+ if (v && typeof v === "object" && !Array.isArray(v)) return Object.keys(v).sort().reduce((o, k) => {
235
+ o[k] = v[k];
236
+ return o;
237
+ }, {});
238
+ return v;
239
+ });
240
+ }
241
+ //#endregion
242
+ //#region src/core/symbols.ts
243
+ /**
244
+ * @internal Apply one owner's (consumer's) deps slice to an instance.
245
+ * Called by framework adapters (e.g. `@blac/react` useBloc) — not public API.
246
+ */
247
+ const APPLY_DEPS = Symbol("blac.applyDeps");
248
+ /**
249
+ * @internal Withdraw one owner's entire deps slice (consumer unmounted).
250
+ * Called by framework adapters — not public API.
251
+ */
252
+ const REMOVE_DEPS_OWNER = Symbol("blac.removeDepsOwner");
253
+ /**
254
+ * @internal Subscribe to this specific instance's own disposal, bypassing
255
+ * the global registry `'disposed'` event. Called by `watch()` — not public
256
+ * API.
257
+ */
258
+ const ON_DISPOSE = Symbol("blac.onDispose");
259
+ /**
260
+ * @internal Symbol-keyed configuration entry point on `StateContainer`.
261
+ * Writes the instance's identity fields (`_name`/`_debug`/`_instanceId`),
262
+ * resolves per-class equality, emits the registry `created` event, and runs
263
+ * `init()` once.
264
+ *
265
+ * Framework-only — the registry (`StateContainerRegistry.ensure`) and testing
266
+ * helpers are the sole callers, mirroring the `APPLY_DEPS` precedent.
267
+ */
268
+ const INIT_CONFIG = Symbol("blac.initConfig");
269
+ /**
270
+ * @internal Run `fn` with `state` temporarily reporting a render's tracking
271
+ * proxy, so getters record the paths they read. Called by `@blac/react`'s
272
+ * `buildTrackedProxy` — not public API.
273
+ *
274
+ * Replaces the old `this`-Proxy receiver: getters now run with `this` bound to
275
+ * the real instance, so ES `#private` works in user blocs.
276
+ */
277
+ const WITH_TRACKED_STATE = Symbol("blac.withTrackedState");
278
+ /**
279
+ * @internal Drive the activation lifecycle from the registry's 0↔1 ownership
280
+ * transition. Owns the per-instance AbortController so `onActivate`'s signal
281
+ * aborts on the matching deactivation and on dispose.
282
+ *
283
+ * Framework-only — `StateContainerRegistry` is the sole caller.
284
+ */
285
+ const SET_ACTIVE = Symbol("blac.setActive");
286
+ //#endregion
287
+ //#region src/core/StateContainerRegistry.ts
288
+ /** Shared, read-only Map returned by `getInstancesMap` for unregistered types. */
289
+ const EMPTY_INSTANCES_MAP = /* @__PURE__ */ new Map();
290
+ /**
291
+ * Central registry for managing StateContainer instances.
292
+ * Handles instance lifecycle, named ref tracking, and lifecycle event emission.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * const registry = new StateContainerRegistry();
297
+ * const instance = registry.acquire(MyBloc); // ownership, must release
298
+ * const other = registry.ensure(OtherBloc); // no ownership, bloc-to-bloc
299
+ * registry.on('stateChanged', (container, prev, next) => {
300
+ * console.log('State changed:', prev, '->', next);
301
+ * });
302
+ * ```
303
+ *
304
+ * `stateChanged` fires once per emit (microtask-deferred but uncoalesced), so
305
+ * listeners see every intermediate transition. Plugins' `onStateChange` fires
306
+ * once per channel flush with coalesced `prev`/`next` and a `PathSet`. Pick
307
+ * the transition log for devtools/time-travel, the plugin hook for
308
+ * change-driven work.
309
+ */
310
+ var StateContainerRegistry = class {
311
+ /**
312
+ * Derived from the listener Set, never a parallel counter: `on()` with the
313
+ * same function twice adds once, so a counter cannot stay in step.
314
+ */
315
+ get hasStateChangedListeners() {
316
+ return (this.listeners.get("stateChanged")?.size ?? 0) > 0;
317
+ }
318
+ constructor() {
319
+ this.instancesByConstructor = /* @__PURE__ */ new WeakMap();
320
+ this.types = /* @__PURE__ */ new Set();
321
+ this._dependentEdges = /* @__PURE__ */ new WeakMap();
322
+ this._entryById = /* @__PURE__ */ new Map();
323
+ this.listeners = /* @__PURE__ */ new Map();
324
+ this._pendingStateChanges = null;
325
+ this._autoRefIdCounter = 0;
326
+ this.on("disposed", (container) => this._handleDisposed(container));
327
+ }
328
+ /**
329
+ * Registry-internal reaction to any instance's `disposed` lifecycle event.
330
+ * If the disposing container is itself tracked in this registry (i.e. it
331
+ * was `acquire()`/`ensure()`'d, not a bare `new`), prune its Map entry and
332
+ * release its dependent edges on whatever it `depend()`'d on.
333
+ */
334
+ _handleDisposed(container) {
335
+ const Type = container.constructor;
336
+ if (!this._pruneEntry(Type, container)) return;
337
+ const edges = this._dependentEdges.get(container);
338
+ if (!edges) return;
339
+ this._dependentEdges.delete(container);
340
+ for (const [DepType, keys] of edges) for (const depKey of keys) this._releaseDependent(DepType, depKey, container);
341
+ }
342
+ /**
343
+ * An entry may be disposed only when nothing owns it: no public refs, no
344
+ * `depend()` dependents, and not keepAlive. Every dispose decision goes
345
+ * through here so `release()` and `_releaseDependent` cannot disagree.
346
+ */
347
+ _isUnowned(Type, entry) {
348
+ return this._hasNoOwners(entry) && !isKeepAliveClass(Type);
349
+ }
350
+ /**
351
+ * Pure ownership: refs plus `depend()` dependents, with no keepAlive term.
352
+ * `_isUnowned` is "may be disposed"; this is "nothing is using it", which is
353
+ * what the activation lifecycle keys off — a keepAlive instance still
354
+ * deactivates when its last owner goes.
355
+ */
356
+ _hasNoOwners(entry) {
357
+ return entry.refs.size === 0 && (entry.dependents === void 0 || entry.dependents.size === 0);
358
+ }
359
+ /**
360
+ * Single funnel for the 0↔1 ownership transition. `SET_ACTIVE` is idempotent,
361
+ * so every acquire/release path can call this unconditionally rather than
362
+ * each computing the edge itself. Emits `activated`/`deactivated` for
363
+ * plugins to observe the same transition `onActivate`/`onDeactivate` expose
364
+ * to bloc authors — after `SET_ACTIVE` has already run the instance's own
365
+ * hook, so plugins always see a container that has finished reacting.
366
+ */
367
+ _syncActivation(entry) {
368
+ const transition = entry.instance[SET_ACTIVE](!this._hasNoOwners(entry));
369
+ if (transition === "none") return;
370
+ if (transition === "deactivated") this.emit("deactivated", entry.instance);
371
+ else this.emit("activated", entry.instance, transition.signal);
372
+ }
373
+ /**
374
+ * Record that `dependent` resolved `Type` at `key`, so the edge can be
375
+ * released on the owner's disposal. Keyed per resolved key, not per type:
376
+ * one `depend()` handle resolves a distinct key for every `args` it is
377
+ * called with, and all of them must be swept.
378
+ */
379
+ _recordDependentEdge(dependent, Type, key) {
380
+ let byType = this._dependentEdges.get(dependent);
381
+ if (!byType) this._dependentEdges.set(dependent, byType = /* @__PURE__ */ new Map());
382
+ let keys = byType.get(Type);
383
+ if (!keys) byType.set(Type, keys = /* @__PURE__ */ new Set());
384
+ keys.add(key);
385
+ }
386
+ /**
387
+ * Remove the Map entry for `container`, if this registry is tracking it.
388
+ * @returns true if an entry was found and removed
389
+ */
390
+ _pruneEntry(Type, container) {
391
+ const id = container._instanceId;
392
+ if (id === void 0) return false;
393
+ const entry = this._entryById.get(id);
394
+ if (entry === void 0 || entry.instance !== container) return false;
395
+ this.instancesByConstructor.get(Type)?.delete(entry.key);
396
+ this._entryById.delete(id);
397
+ return true;
398
+ }
399
+ /**
400
+ * Drop `dependent`'s edge on the `Type`/`key` entry (a `depend()`-resolved
401
+ * instance). When that leaves the entry with no dependents AND no public
402
+ * refs AND it isn't keepAlive, dispose it — this recurses into the same
403
+ * `disposed` listener, so dep-of-dep chains unwind naturally.
404
+ */
405
+ _releaseDependent(Type, key, dependent) {
406
+ const entry = this.instancesByConstructor.get(Type)?.get(key);
407
+ if (!entry) return;
408
+ entry.dependents?.delete(dependent);
409
+ if (this._isUnowned(Type, entry)) {
410
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
411
+ return;
412
+ }
413
+ this._syncActivation(entry);
414
+ }
415
+ /**
416
+ * Register a type for lifecycle event tracking
417
+ * @param constructor - The StateContainer class constructor
418
+ */
419
+ registerType(constructor) {
420
+ this.types.add(constructor);
421
+ }
422
+ /**
423
+ * Register a StateContainer class with configuration
424
+ * @param constructor - The StateContainer class constructor
425
+ * @throws Error if type is already registered
426
+ */
427
+ register(constructor) {
428
+ if (this.types.has(constructor)) throw new Error(`${BLAC_ERROR_PREFIX} Type "${getBlacName(constructor)}" is already registered`);
429
+ this.registerType(constructor);
430
+ }
431
+ ensureInstancesMap(Type) {
432
+ let instances = this.instancesByConstructor.get(Type);
433
+ if (!instances) {
434
+ instances = /* @__PURE__ */ new Map();
435
+ this.instancesByConstructor.set(Type, instances);
436
+ }
437
+ return instances;
438
+ }
439
+ /**
440
+ * Get the instances Map for a specific class (public API for stats/debugging)
441
+ */
442
+ getInstancesMap(Type) {
443
+ return this.instancesByConstructor.get(Type) || EMPTY_INSTANCES_MAP;
444
+ }
445
+ /**
446
+ * Directly insert an instance entry into the registry without going through
447
+ * acquire/release. Creates the internal WeakMap bucket for the constructor if
448
+ * it does not exist yet. Replaces any existing entry for the same key.
449
+ *
450
+ * The provided instance is NOT configured or disposed here — callers are
451
+ * responsible for any initConfig / dispose calls. Intended for testing
452
+ * helpers that create stubs outside the normal acquisition path.
453
+ *
454
+ * @internal Used by testing helpers only.
455
+ */
456
+ insertInstance(Type, instanceKey, instance, refs = /* @__PURE__ */ new Map()) {
457
+ const instances = this.ensureInstancesMap(Type);
458
+ const existingEntry = instances.get(instanceKey);
459
+ if (existingEntry && existingEntry.instance !== instance && !existingEntry.instance.$blac.disposed) existingEntry.instance.dispose();
460
+ const entry = {
461
+ instance,
462
+ key: instanceKey,
463
+ refs
464
+ };
465
+ instances.set(instanceKey, entry);
466
+ this._entryById.set(instance.$blac.id, entry);
467
+ }
468
+ /**
469
+ * Resolve the storage key for an instance. This is the single source of truth
470
+ * for keying — `acquire` and `release` MUST agree, otherwise a ref taken under
471
+ * an args-derived key is never dropped (instance leaks).
472
+ *
473
+ * Resolution order (explicit beats derived):
474
+ * explicit key \> `static key(args)` \> structural hash of args \> default sentinel.
475
+ *
476
+ * @param Type - The StateContainer class constructor
477
+ * @param instanceKey - Explicit key, or undefined to derive from args
478
+ * @param args - Construction args used for structural/`static key` derivation
479
+ */
480
+ resolveKey(Type, instanceKey, args) {
481
+ if (instanceKey !== void 0) return instanceKey;
482
+ const keyFn = getClassKey(Type);
483
+ if (keyFn) return keyFn(args);
484
+ if (args !== void 0) return structuralKey(args);
485
+ return DEFAULT_STRUCTURAL_KEY;
486
+ }
487
+ /**
488
+ * Circuit breaker for runaway instance creation. Throws when a single
489
+ * constructor would exceed `maxInstancesPerType` live instances — almost
490
+ * always an unstable instance key (e.g. a new object/array passed as `args`
491
+ * every render, or a missing `static key`) leaking an instance per render.
492
+ */
493
+ assertInstanceLimit(Type, currentCount) {
494
+ const limit = getBlacConfig().maxInstancesPerType;
495
+ if (limit > 0 && currentCount >= limit) throw new Error(`${BLAC_ERROR_PREFIX} ${Type.name} exceeded the maximum of ${limit} live instances. This usually means the instance key is unstable — e.g. \`args\` that change identity every render, or a missing \`static key\` — so a new instance is created and never disposed (memory leak). Stabilize the key, add a \`static key(args)\`, or raise \`configureBlac({ maxInstancesPerType })\`.`);
496
+ }
497
+ /**
498
+ * Circuit breaker for runaway reference growth. Throws when one instance
499
+ * accumulates more than `maxRefsPerInstance` distinct live refs — almost
500
+ * always consumer cleanup (e.g. `useBloc`'s unmount `release`) never firing.
501
+ */
502
+ assertRefLimit(Type, resolvedKey, refCount) {
503
+ const limit = getBlacConfig().maxRefsPerInstance;
504
+ if (limit > 0 && refCount > limit) throw new Error(`${BLAC_ERROR_PREFIX} ${Type.name} instance "${resolvedKey}" exceeded the maximum of ${limit} live references. This usually means references are acquired without a matching release (e.g. a consumer that never unmounts/cleans up), leaking refs that keep the instance alive forever. Ensure every acquire is paired with a release, or raise \`configureBlac({ maxRefsPerInstance })\`.`);
505
+ }
506
+ /**
507
+ * Acquire an instance with ref tracking (ownership semantics).
508
+ * Creates a new instance if one doesn't exist, or returns existing and adds a ref.
509
+ * You must call `release()` with the same refId when done.
510
+ *
511
+ * @internal Internal key tier. Public callers use the args-based `acquire`
512
+ * wrapper; `useBloc` and `watch` address a pre-resolved key here.
513
+ * @param Type - The StateContainer class constructor
514
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
515
+ * @param options - Acquisition options: `canCreate` (create when absent,
516
+ * default true), `countRef` (add a reference, default true), `refId`
517
+ * (named reference for debugging, auto-generated when omitted),
518
+ * `dependent` — the `depend()`-owner resolving this instance, recorded so
519
+ * its dependent edge is released on the owner's disposal — and
520
+ * `sweepIfUnowned`, for callers that create speculatively and claim
521
+ * ownership later (see {@link _scheduleSweep}).
522
+ * @returns The state container instance
523
+ */
524
+ acquire(Type, instanceKey, options = {}) {
525
+ const { canCreate = true, countRef = true } = options;
526
+ const args = options.args;
527
+ const resolvedKey = this.resolveKey(Type, instanceKey, args);
528
+ const instances = this.ensureInstancesMap(Type);
529
+ let entry = instances.get(resolvedKey);
530
+ if (entry?.instance.$blac.disposed) {
531
+ instances.delete(resolvedKey);
532
+ entry = void 0;
533
+ }
534
+ if (entry) {
535
+ if (IS_DEV && args !== void 0 && entry.args !== void 0) {
536
+ const incomingKey = structuralKey(args);
537
+ const storedKey = entry.argsKey ??= structuralKey(entry.args);
538
+ if (incomingKey !== storedKey) console.warn(`${BLAC_ERROR_PREFIX} ${Type.name} instance key "${resolvedKey}" was acquired with different args. Existing args: ${storedKey}, new args: ${incomingKey}. The existing instance will be reused. If distinct args should produce distinct instances, either remove the explicit instanceKey or provide a \`static key\` function that reflects the difference.`);
539
+ }
540
+ if (countRef) {
541
+ const refId = options.refId ?? `_auto_${this._autoRefIdCounter++}`;
542
+ entry.refs.set(refId, (entry.refs.get(refId) ?? 0) + 1);
543
+ this.assertRefLimit(Type, resolvedKey, entry.refs.size);
544
+ this.emit("refAcquired", entry.instance, refId);
545
+ }
546
+ if (options.dependent) {
547
+ (entry.dependents ??= /* @__PURE__ */ new Set()).add(options.dependent);
548
+ this._recordDependentEdge(options.dependent, Type, resolvedKey);
549
+ }
550
+ this._syncActivation(entry);
551
+ return entry.instance;
552
+ }
553
+ if (!canCreate) throw new Error(`${BLAC_ERROR_PREFIX} ${Type.name} instance "${resolvedKey}" not found and creation is disabled.`);
554
+ this.assertInstanceLimit(Type, instances.size);
555
+ const config = {
556
+ instanceId: resolvedKey,
557
+ args,
558
+ registry: this
559
+ };
560
+ const instance = new Type();
561
+ instance[INIT_CONFIG](config);
562
+ const initialRefs = /* @__PURE__ */ new Map();
563
+ let initialRefId;
564
+ if (countRef) {
565
+ initialRefId = options.refId ?? `_auto_${this._autoRefIdCounter++}`;
566
+ initialRefs.set(initialRefId, 1);
567
+ }
568
+ const newEntry = {
569
+ instance,
570
+ key: resolvedKey,
571
+ refs: initialRefs,
572
+ args
573
+ };
574
+ if (options.dependent) {
575
+ (newEntry.dependents ??= /* @__PURE__ */ new Set()).add(options.dependent);
576
+ this._recordDependentEdge(options.dependent, Type, resolvedKey);
577
+ }
578
+ instances.set(resolvedKey, newEntry);
579
+ this._entryById.set(instance.$blac.id, newEntry);
580
+ this.registerType(Type);
581
+ if (initialRefId) this.emit("refAcquired", instance, initialRefId);
582
+ this._syncActivation(newEntry);
583
+ if (options.sweepIfUnowned && this._hasNoOwners(newEntry)) this._scheduleSweep(Type, newEntry);
584
+ return instance;
585
+ }
586
+ /**
587
+ * Speculative creates (`useBloc`'s render-time create) leak forever when the
588
+ * render is discarded or never commits — SSR has no commit at all, so nothing
589
+ * ever claims ownership. Sweep at the end of the microtask unless the entry
590
+ * gained an owner or is keepAlive.
591
+ *
592
+ * Opt-in, not automatic: a bare `ensure()` hands the instance to a caller
593
+ * that legitimately holds it without a ref, and sweeping those would make
594
+ * `ensure` unusable across an `await`.
595
+ */
596
+ _scheduleSweep(Type, entry) {
597
+ queueMicrotask(() => {
598
+ const instances = this.instancesByConstructor.get(Type);
599
+ if (instances?.get(entry.key) !== entry) return;
600
+ if (entry.instance.$blac.disposed) return;
601
+ if (!this._isUnowned(Type, entry)) return;
602
+ entry.instance.dispose();
603
+ instances.delete(entry.key);
604
+ });
605
+ }
606
+ /**
607
+ * Borrow an existing instance without adding a ref (borrowing semantics).
608
+ * Tracks cross-bloc dependency for reactive updates.
609
+ *
610
+ * @internal Internal key tier; public callers use the args-based `borrow`.
611
+ * @param Type - The StateContainer class constructor
612
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
613
+ * @returns The state container instance
614
+ * @throws Error if instance doesn't exist
615
+ */
616
+ borrow(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
617
+ return this.acquire(Type, instanceKey, {
618
+ canCreate: false,
619
+ countRef: false
620
+ });
621
+ }
622
+ /**
623
+ * Safely borrow an existing instance (borrowing semantics with error handling).
624
+ * Returns discriminated union for type-safe conditional access.
625
+ *
626
+ * @internal Internal key tier; public callers use the args-based `borrowSafe`.
627
+ * @param Type - The StateContainer class constructor
628
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
629
+ * @returns Discriminated union with either the instance or an error
630
+ */
631
+ borrowSafe(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
632
+ try {
633
+ return {
634
+ error: null,
635
+ instance: this.borrow(Type, instanceKey)
636
+ };
637
+ } catch (error) {
638
+ return {
639
+ error,
640
+ instance: null
641
+ };
642
+ }
643
+ }
644
+ /**
645
+ * Ensure an instance exists without taking ownership (for bloc-to-bloc communication).
646
+ * Gets existing instance OR creates it if it doesn't exist, without adding a ref.
647
+ * Tracks cross-bloc dependency for reactive updates.
648
+ *
649
+ * Use this in bloc-to-bloc communication when you need to ensure an instance exists
650
+ * but don't want to claim ownership (no ref added).
651
+ *
652
+ * @internal Internal key tier; public callers use the args-based `ensure`.
653
+ * @param Type - The StateContainer class constructor
654
+ * @param instanceKey - Pre-resolved instance key, or undefined to derive from `args`
655
+ * (`static key(args)` / structural hash), matching `acquire`.
656
+ * @param args - Construction args; used for keying when no explicit key is given
657
+ * @returns The state container instance
658
+ */
659
+ ensure(Type, instanceKey, args) {
660
+ return this.acquire(Type, instanceKey, {
661
+ canCreate: true,
662
+ countRef: false,
663
+ args
664
+ });
665
+ }
666
+ /**
667
+ * Release a reference to an instance.
668
+ * Removes the ref and disposes when refs is empty (unless keepAlive).
669
+ * Releasing an already-removed refId is a no-op (idempotent).
670
+ *
671
+ * @internal Internal key tier; public callers use the args-based `release`.
672
+ * @param Type - The StateContainer class constructor
673
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
674
+ * @param forceDispose - Force immediate disposal regardless of refs
675
+ * @param refId - The specific ref to remove; removes one arbitrary ref if omitted
676
+ */
677
+ release(Type, instanceKey = DEFAULT_STRUCTURAL_KEY, forceDispose = false, refId) {
678
+ const instances = this.ensureInstancesMap(Type);
679
+ const entry = instances.get(instanceKey);
680
+ if (!entry) return;
681
+ if (forceDispose) {
682
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
683
+ instances.delete(instanceKey);
684
+ return;
685
+ }
686
+ let releasedRefId;
687
+ if (refId !== void 0) {
688
+ const count = entry.refs.get(refId) ?? 0;
689
+ if (count <= 1) entry.refs.delete(refId);
690
+ else entry.refs.set(refId, count - 1);
691
+ releasedRefId = refId;
692
+ } else {
693
+ const firstKey = entry.refs.keys().next().value;
694
+ if (firstKey !== void 0) {
695
+ const count = entry.refs.get(firstKey) ?? 0;
696
+ if (count <= 1) entry.refs.delete(firstKey);
697
+ else entry.refs.set(firstKey, count - 1);
698
+ releasedRefId = firstKey;
699
+ }
700
+ }
701
+ if (releasedRefId) this.emit("refReleased", entry.instance, releasedRefId);
702
+ if (this._isUnowned(Type, entry)) {
703
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
704
+ instances.delete(instanceKey);
705
+ return;
706
+ }
707
+ this._syncActivation(entry);
708
+ }
709
+ /**
710
+ * Get all instances of a specific type.
711
+ * @param Type - The StateContainer class constructor
712
+ * @returns Array of all instances
713
+ */
714
+ getAll(Type) {
715
+ const instances = this.ensureInstancesMap(Type);
716
+ const result = [];
717
+ for (const entry of instances.values()) if (!entry.instance.$blac.disposed) result.push(entry.instance);
718
+ return result;
719
+ }
720
+ /**
721
+ * Safely iterate over all instances of a type.
722
+ * Skips disposed instances and catches callback errors.
723
+ * @param Type - The StateContainer class constructor
724
+ * @param callback - Function to call for each instance
725
+ */
726
+ forEach(Type, callback) {
727
+ const instances = this.ensureInstancesMap(Type);
728
+ for (const entry of instances.values()) {
729
+ const instance = entry.instance;
730
+ if (!instance.$blac.disposed) try {
731
+ callback(instance);
732
+ } catch (error) {
733
+ console.error(`${BLAC_ERROR_PREFIX} forEach callback error for ${Type.name}:`, error);
734
+ }
735
+ }
736
+ }
737
+ /**
738
+ * Clear all instances of a specific type (disposes them).
739
+ * @param Type - The StateContainer class constructor
740
+ */
741
+ clear(Type) {
742
+ const instances = this.ensureInstancesMap(Type);
743
+ for (const entry of instances.values()) if (!entry.instance.$blac.disposed) entry.instance.dispose();
744
+ instances.clear();
745
+ }
746
+ /**
747
+ * Get reference count for an instance (number of active refs).
748
+ *
749
+ * @internal Internal key tier; public callers use the args-based `getRefCount`.
750
+ * @param Type - The StateContainer class constructor
751
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
752
+ * @returns Current ref count (0 if instance doesn't exist)
753
+ */
754
+ getRefCount(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
755
+ return this.ensureInstancesMap(Type).get(instanceKey)?.refs.size ?? 0;
756
+ }
757
+ /**
758
+ * Get all active reference IDs for an instance.
759
+ *
760
+ * @internal Internal key tier; public callers use the args-based `getRefIds`.
761
+ * @param Type - The StateContainer class constructor
762
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
763
+ * @returns Array of ref ID strings (empty if instance doesn't exist)
764
+ */
765
+ getRefIds(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
766
+ const instances = this.instancesByConstructor.get(Type);
767
+ if (!instances) return [];
768
+ const entry = instances.get(instanceKey);
769
+ return entry ? Array.from(entry.refs.keys()) : [];
770
+ }
771
+ /**
772
+ * Get all active reference IDs for an instance by its `$blac.id`, without
773
+ * needing the owning Type. Used by `PluginContext.getRefIds`.
774
+ * @param instanceId - The instance's `$blac.id`
775
+ * @returns Array of ref ID strings (empty if instance doesn't exist)
776
+ */
777
+ getRefIdsById(instanceId) {
778
+ const entry = this._entryById.get(instanceId);
779
+ return entry ? Array.from(entry.refs.keys()) : [];
780
+ }
781
+ /**
782
+ * Check if an instance exists.
783
+ *
784
+ * @internal Internal key tier; public callers use the args-based `hasInstance`.
785
+ * @param Type - The StateContainer class constructor
786
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
787
+ * @returns true if instance exists
788
+ */
789
+ hasInstance(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
790
+ return this.ensureInstancesMap(Type).has(instanceKey);
791
+ }
792
+ /**
793
+ * Clear all instances from all types (for testing)
794
+ *
795
+ * Iterates all registered types and clears their instances.
796
+ * Also clears type tracking to reset the registry state.
797
+ */
798
+ clearAll() {
799
+ for (const Type of this.types) this.clear(Type);
800
+ this.types.clear();
801
+ }
802
+ /**
803
+ * Get registry statistics for debugging.
804
+ * @returns Object with registeredTypes, totalInstances, and typeBreakdown
805
+ */
806
+ getStats() {
807
+ const typeBreakdown = {};
808
+ let totalInstances = 0;
809
+ for (const Type of this.types) {
810
+ const typeName = Type.name;
811
+ const count = this.getInstancesMap(Type).size;
812
+ typeBreakdown[typeName] = count;
813
+ totalInstances += count;
814
+ }
815
+ return {
816
+ registeredTypes: this.types.size,
817
+ totalInstances,
818
+ typeBreakdown
819
+ };
820
+ }
821
+ /**
822
+ * Get all registered types (for plugin system).
823
+ * @returns Array of all registered StateContainer class constructors
824
+ */
825
+ getTypes() {
826
+ return Array.from(this.types);
827
+ }
828
+ /**
829
+ * Subscribe to lifecycle events
830
+ * @param event - The lifecycle event to listen for
831
+ * @param listener - The listener function to call when the event occurs
832
+ * @returns Unsubscribe function
833
+ */
834
+ on(event, listener) {
835
+ let instance = this.listeners.get(event);
836
+ if (!instance) {
837
+ instance = /* @__PURE__ */ new Set();
838
+ this.listeners.set(event, instance);
839
+ }
840
+ instance.add(listener);
841
+ return () => {
842
+ this.listeners.get(event)?.delete(listener);
843
+ };
844
+ }
845
+ emit(event, ...args) {
846
+ const listeners = this.listeners.get(event);
847
+ if (!listeners || listeners.size === 0) return;
848
+ for (const listener of listeners) try {
849
+ listener(...args);
850
+ } catch (error) {
851
+ console.error(`${BLAC_ERROR_PREFIX} Listener error for '${event}':`, error);
852
+ }
853
+ }
854
+ /**
855
+ * Schedule a deferred stateChanged notification via microtask.
856
+ * Skips entirely when no stateChanged listeners are registered.
857
+ * @internal - Called by StateContainer.applyState
858
+ */
859
+ notifyStateChanged(container, previousState, newState) {
860
+ if (!this.hasStateChangedListeners) return;
861
+ if (!this._pendingStateChanges) {
862
+ this._pendingStateChanges = [];
863
+ queueMicrotask(() => this.flushStateChanged());
864
+ }
865
+ this._pendingStateChanges.push([
866
+ container,
867
+ previousState,
868
+ newState
869
+ ]);
870
+ }
871
+ /**
872
+ * Deliver one event per emit, in order — deliberately NOT coalesced.
873
+ *
874
+ * This is a transition log: devtools, time-travel debugging and perf
875
+ * monitoring need every intermediate `prev`/`next`, which coalescing would
876
+ * destroy. The plugin `onStateChange` bridge is the coalesced, `PathSet`-
877
+ * carrying lane (see `PluginManager.setupLifecycleHooks`); the two differ on
878
+ * purpose, so a listener picks the lane that matches what it needs.
879
+ */
880
+ flushStateChanged() {
881
+ const pending = this._pendingStateChanges;
882
+ this._pendingStateChanges = null;
883
+ if (!pending) return;
884
+ for (const [container, prev, next] of pending) this.emit("stateChanged", container, prev, next);
885
+ }
886
+ };
887
+ /**
888
+ * Global default registry instance
889
+ * @public
890
+ */
891
+ const globalRegistry = new StateContainerRegistry();
892
+ //#endregion
893
+ export { resetBlacConfig as _, ON_DISPOSE as a, WITH_TRACKED_STATE as c, isExcludedFromDevTools as d, isKeepAliveClass as f, getBlacConfig as g, configureBlac as h, INIT_CONFIG as i, getBlacName as l, IS_DEV as m, globalRegistry as n, REMOVE_DEPS_OWNER as o, BLAC_STATIC_PROPS as p, APPLY_DEPS as r, SET_ACTIVE as s, StateContainerRegistry as t, getClassEquality as u, shallowEqualState as v };
894
+
895
+ //# sourceMappingURL=StateContainerRegistry-BsH_s0pJ.js.map