@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
@@ -0,0 +1,866 @@
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
+ unownedSweepDelayMs: 5e3
28
+ };
29
+ let globalConfig = { ...defaultConfig };
30
+ /**
31
+ * Configure global defaults for `@blac/core`.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { configureBlac, shallowEqualState } from '@blac/core';
36
+ *
37
+ * // Swap in a deep-equality check for all blocs
38
+ * configureBlac({ equality: myDeepEqual });
39
+ * ```
40
+ */
41
+ function configureBlac(config) {
42
+ globalConfig = {
43
+ ...globalConfig,
44
+ ...config
45
+ };
46
+ }
47
+ /**
48
+ * Get the current global configuration.
49
+ * @internal
50
+ */
51
+ function getBlacConfig() {
52
+ return globalConfig;
53
+ }
54
+ /**
55
+ * Reset configuration to defaults (useful for testing).
56
+ * @internal
57
+ */
58
+ function resetBlacConfig() {
59
+ globalConfig = { ...defaultConfig };
60
+ }
61
+ //#endregion
62
+ //#region src/constants.ts
63
+ /** Static property names used for feature flags and configuration on bloc classes. */
64
+ const BLAC_STATIC_PROPS = {
65
+ /** Mark a bloc to never be auto-disposed (kept alive permanently). */
66
+ KEEP_ALIVE: "keepAlive",
67
+ /** Exclude a bloc from DevTools reporting (prevents infinite loops). */
68
+ EXCLUDE_FROM_DEVTOOLS: "__excludeFromDevTools",
69
+ /**
70
+ * Per-class equality function used to short-circuit `emit`/`patch` when the
71
+ * new state is structurally equal to the previous state. Falls back to the
72
+ * global `configureBlac({ equality })` value when unset.
73
+ */
74
+ EQUALITY: "__equality",
75
+ /**
76
+ * Per-class key function: `static key = (args) => string`.
77
+ * When set, the registry calls `Type.key(args)` to derive the instance key
78
+ * instead of using a structural hash of `args`. Allows blocs to ignore
79
+ * non-identity fields (e.g., `readonly` flags) while still keying by id.
80
+ */
81
+ KEY: "key",
82
+ /**
83
+ * Explicit, minification-safe identity for a bloc class: `static blacName = '...'`.
84
+ * When unset, identity falls back to `constructor.name`.
85
+ */
86
+ BLAC_NAME: "blacName"
87
+ };
88
+ const BLAC_ERROR_PREFIX = "[BlaC]";
89
+ function readNodeEnv() {
90
+ try {
91
+ return process.env.NODE_ENV;
92
+ } catch {
93
+ return;
94
+ }
95
+ }
96
+ const nodeEnv = readNodeEnv();
97
+ /**
98
+ * True only when `process.env.NODE_ENV` is defined and not `'production'`.
99
+ *
100
+ * Dev-only checks (emit-rate breaker, deps collision scan, args-mismatch key)
101
+ * cost real work per emit/acquire, so an environment that says nothing about
102
+ * its mode — plain ESM in the browser, Deno, a bundle without a `process`
103
+ * shim — is treated as production. Bundlers that define `process.env.NODE_ENV`
104
+ * still constant-fold the read.
105
+ */
106
+ const IS_DEV = nodeEnv !== void 0 && nodeEnv !== "production";
107
+ //#endregion
108
+ //#region src/utils/static-props.ts
109
+ /**
110
+ * Get a static property from a class constructor, walking the prototype
111
+ * chain so subclasses inherit the value unless they define their own.
112
+ */
113
+ function getStaticProp(Type, propName) {
114
+ return Type[propName];
115
+ }
116
+ /**
117
+ * Get a static property declared directly on `Type`, ignoring the prototype
118
+ * chain, so subclasses never inherit a base class's value.
119
+ */
120
+ function getOwnStaticProp(Type, propName) {
121
+ return Object.hasOwn(Type, propName) ? Type[propName] : void 0;
122
+ }
123
+ /**
124
+ * Whether a class is marked `keepAlive` (never auto-disposed at ref count 0).
125
+ * Inherited: a subclass keeps a base class's `keepAlive` unless overridden.
126
+ */
127
+ function isKeepAliveClass(Type) {
128
+ return getStaticProp(Type, BLAC_STATIC_PROPS.KEEP_ALIVE) === true;
129
+ }
130
+ /**
131
+ * Whether a class is excluded from DevTools (avoids DevTools tracking itself
132
+ * into an infinite loop). Inherited unless overridden by a subclass.
133
+ */
134
+ function isExcludedFromDevTools(Type) {
135
+ return getStaticProp(Type, BLAC_STATIC_PROPS.EXCLUDE_FROM_DEVTOOLS) === true;
136
+ }
137
+ /**
138
+ * Get the per-class equality function set via `@blac({ equality })`, if any.
139
+ * Inherited: a subclass keeps a base class's equality fn unless overridden.
140
+ */
141
+ function getClassEquality(Type) {
142
+ const fn = getStaticProp(Type, BLAC_STATIC_PROPS.EQUALITY);
143
+ return typeof fn === "function" ? fn : void 0;
144
+ }
145
+ /**
146
+ * Get a class's minification-safe identity name (`static blacName`), falling
147
+ * back to `Type.name`. Own only: a subclass never inherits a base class's
148
+ * explicit `blacName`, since that would collapse two distinct blocs onto one
149
+ * identity.
150
+ */
151
+ function getBlacName(Type) {
152
+ return getOwnStaticProp(Type, BLAC_STATIC_PROPS.BLAC_NAME) ?? Type.name;
153
+ }
154
+ /**
155
+ * Get the per-class key function set via `@blac({ key })` or `static key = …`.
156
+ * Own only: a subclass never inherits a base class's `key`, since a
157
+ * different `Args` shape would derive the wrong instance key.
158
+ */
159
+ function getClassKey(Type) {
160
+ const fn = getOwnStaticProp(Type, BLAC_STATIC_PROPS.KEY);
161
+ return typeof fn === "function" ? fn : void 0;
162
+ }
163
+ //#endregion
164
+ //#region src/utils/structural-key.ts
165
+ const DEFAULT_STRUCTURAL_KEY = "default";
166
+ /**
167
+ * Produce a stable string key for serializable args.
168
+ * - `undefined` / `void` args → `'default'` sentinel (no-args blocs stay on the default key).
169
+ * - Object keys are sorted for order-independence.
170
+ * - Arrays keep their insertion order.
171
+ * - Functions throw — they must be passed via `deps`, not `args`.
172
+ * - JSON semantics otherwise: `{ a: undefined }` equals `{}`, and `NaN` and
173
+ * `Infinity` serialize as `null`.
174
+ * - Object args are memoized by identity; callers on hot paths (`resolveKey`,
175
+ * `depend`) re-resolve the same args object on every call. Mutating an args
176
+ * object after first use therefore keeps its old key.
177
+ */
178
+ const keyCache = /* @__PURE__ */ new WeakMap();
179
+ function structuralKey(args) {
180
+ if (args === void 0 || args === null) return DEFAULT_STRUCTURAL_KEY;
181
+ if (typeof args === "object") {
182
+ const cached = keyCache.get(args);
183
+ if (cached !== void 0) return cached;
184
+ const key = serialize(args);
185
+ keyCache.set(args, key);
186
+ return key;
187
+ }
188
+ return serialize(args);
189
+ }
190
+ function serialize(args) {
191
+ return JSON.stringify(args, function(_k, v) {
192
+ if (typeof v === "function") throw new Error(`[blac] args must be serializable; put refs/callbacks in \`deps\` (found a function at key "${_k || "(root)"}")`);
193
+ if (v && typeof v === "object" && !Array.isArray(v)) {
194
+ if (IS_DEV && !isPlainObject(v)) throw new Error(`[blac] args must be plain data; put refs/instances in \`deps\` (found a ${v.constructor?.name ?? "non-plain object"} at key "${_k || "(root)"}")`);
195
+ return Object.keys(v).sort().reduce((o, k) => {
196
+ o[k] = v[k];
197
+ return o;
198
+ }, {});
199
+ }
200
+ return v;
201
+ });
202
+ }
203
+ function isPlainObject(value) {
204
+ const proto = Object.getPrototypeOf(value);
205
+ return proto === Object.prototype || proto === null;
206
+ }
207
+ //#endregion
208
+ //#region src/core/symbols.ts
209
+ /**
210
+ * @internal Apply one owner's (consumer's) deps slice to an instance.
211
+ * Called by framework adapters (e.g. `@blac/react` useBloc) — not public API.
212
+ */
213
+ const APPLY_DEPS = Symbol("blac.applyDeps");
214
+ /**
215
+ * @internal Withdraw one owner's entire deps slice (consumer unmounted).
216
+ * Called by framework adapters — not public API.
217
+ */
218
+ const REMOVE_DEPS_OWNER = Symbol("blac.removeDepsOwner");
219
+ /**
220
+ * @internal Subscribe to this specific instance's own disposal, bypassing
221
+ * the global registry `'disposed'` event. Called by `watch()` — not public
222
+ * API.
223
+ */
224
+ const ON_DISPOSE = Symbol("blac.onDispose");
225
+ /**
226
+ * @internal Symbol-keyed configuration entry point on `StateContainer`.
227
+ * Writes the instance's identity fields (`_name`/`_debug`/`_instanceId`),
228
+ * resolves per-class equality, emits the registry `created` event, and runs
229
+ * `init()` once.
230
+ *
231
+ * Framework-only — the registry (`StateContainerRegistry.ensure`) and testing
232
+ * helpers are the sole callers, mirroring the `APPLY_DEPS` precedent.
233
+ */
234
+ const INIT_CONFIG = Symbol("blac.initConfig");
235
+ /**
236
+ * @internal Run `fn` with `state` temporarily reporting a render's tracking
237
+ * proxy, so getters record the paths they read. Called by `@blac/react`'s
238
+ * `buildTrackedProxy` — not public API. Getters run with `this` bound to the
239
+ * real instance, so ES `#private` works in user blocs.
240
+ */
241
+ const WITH_TRACKED_STATE = Symbol("blac.withTrackedState");
242
+ /**
243
+ * @internal Drive the activation lifecycle from the registry's 0↔1 ownership
244
+ * transition. Owns the per-instance AbortController so `onActivate`'s signal
245
+ * aborts on the matching deactivation and on dispose.
246
+ *
247
+ * Framework-only — `StateContainerRegistry` is the sole caller.
248
+ */
249
+ const SET_ACTIVE = Symbol("blac.setActive");
250
+ /**
251
+ * @internal Insert an instance entry directly, bypassing acquire/release.
252
+ * Called by the testing helpers — not public API.
253
+ */
254
+ const INSERT_INSTANCE = Symbol("blac.insertInstance");
255
+ //#endregion
256
+ //#region src/core/StateContainerRegistry.ts
257
+ const EMPTY_INSTANCES_MAP = /* @__PURE__ */ new Map();
258
+ /**
259
+ * Owns StateContainer instances: creation, ref tracking, disposal and
260
+ * lifecycle events.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const registry = new StateContainerRegistry();
265
+ * const instance = registry.acquire(MyBloc); // ownership, must release
266
+ * const other = registry.ensure(OtherBloc); // no ownership, bloc-to-bloc
267
+ * registry.on('stateChanged', (container, prev, next) => {
268
+ * console.log('State changed:', prev, '->', next);
269
+ * });
270
+ * ```
271
+ *
272
+ * `stateChanged` fires once per emit (microtask-deferred, not coalesced).
273
+ * Plugins' `onStateChange` fires once per channel flush with coalesced
274
+ * `prev`/`next` and a `PathSet`.
275
+ */
276
+ var StateContainerRegistry = class {
277
+ get hasStateChangedListeners() {
278
+ return (this.listeners.get("stateChanged")?.size ?? 0) > 0;
279
+ }
280
+ constructor() {
281
+ this.instancesByConstructor = /* @__PURE__ */ new WeakMap();
282
+ this.types = /* @__PURE__ */ new Set();
283
+ this._dependentEdges = /* @__PURE__ */ new WeakMap();
284
+ this._entryByInstance = /* @__PURE__ */ new WeakMap();
285
+ this._entryById = /* @__PURE__ */ new Map();
286
+ this.listeners = /* @__PURE__ */ new Map();
287
+ this._pendingStateChanges = null;
288
+ this._autoRefIdCounter = 0;
289
+ this.on("disposed", (container) => this._handleDisposed(container));
290
+ }
291
+ /** Prune a tracked instance's entry and release its `depend()` edges. */
292
+ _handleDisposed(container) {
293
+ const Type = container.constructor;
294
+ if (!this._pruneEntry(Type, container)) return;
295
+ const edges = this._dependentEdges.get(container);
296
+ if (!edges) return;
297
+ this._dependentEdges.delete(container);
298
+ for (const [DepType, keys] of edges) for (const depKey of keys) this._releaseDependent(DepType, depKey, container);
299
+ }
300
+ /** The single dispose decision: no refs, no dependents, not keepAlive. */
301
+ _isUnowned(Type, entry) {
302
+ return this._hasNoOwners(entry) && !isKeepAliveClass(Type);
303
+ }
304
+ /** Ignores keepAlive: a keepAlive instance still deactivates. */
305
+ _hasNoOwners(entry) {
306
+ return entry.refs.size === 0 && (entry.dependents === void 0 || entry.dependents.size === 0);
307
+ }
308
+ /**
309
+ * Idempotent, so every acquire/release path calls it. The events fire after
310
+ * the instance's own `onActivate`/`onDeactivate`.
311
+ */
312
+ _syncActivation(entry) {
313
+ const transition = entry.instance[SET_ACTIVE](!this._hasNoOwners(entry));
314
+ if (transition === "none") return;
315
+ if (transition === "deactivated") this.emit("deactivated", entry.instance);
316
+ else this.emit("activated", entry.instance, transition.signal);
317
+ }
318
+ _recordDependentEdge(dependent, Type, key) {
319
+ let byType = this._dependentEdges.get(dependent);
320
+ if (!byType) this._dependentEdges.set(dependent, byType = /* @__PURE__ */ new Map());
321
+ let keys = byType.get(Type);
322
+ if (!keys) byType.set(Type, keys = /* @__PURE__ */ new Set());
323
+ keys.add(key);
324
+ }
325
+ /** @returns true if this registry tracked `container`. */
326
+ _pruneEntry(Type, container) {
327
+ const entry = this._entryByInstance.get(container);
328
+ if (entry === void 0) return false;
329
+ this._entryByInstance.delete(container);
330
+ const instances = this.instancesByConstructor.get(Type);
331
+ if (instances?.get(entry.key) === entry) instances.delete(entry.key);
332
+ const id = container.$blac.id;
333
+ if (this._entryById.get(id) === entry) this._entryById.delete(id);
334
+ return true;
335
+ }
336
+ _indexEntry(entry) {
337
+ this._entryByInstance.set(entry.instance, entry);
338
+ this._entryById.set(entry.instance.$blac.id, entry);
339
+ }
340
+ /** Disposing here recurses through `disposed`, unwinding dep chains. */
341
+ _releaseDependent(Type, key, dependent) {
342
+ const entry = this.instancesByConstructor.get(Type)?.get(key);
343
+ if (!entry) return;
344
+ entry.dependents?.delete(dependent);
345
+ if (this._isUnowned(Type, entry)) {
346
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
347
+ return;
348
+ }
349
+ this._syncActivation(entry);
350
+ }
351
+ /**
352
+ * Register a StateContainer class with configuration
353
+ * @param constructor - The StateContainer class constructor
354
+ * @throws Error if type is already registered
355
+ */
356
+ register(constructor) {
357
+ if (this.types.has(constructor)) throw new Error(`${BLAC_ERROR_PREFIX} Type "${getBlacName(constructor)}" is already registered`);
358
+ this.types.add(constructor);
359
+ }
360
+ ensureInstancesMap(Type) {
361
+ let instances = this.instancesByConstructor.get(Type);
362
+ if (!instances) {
363
+ instances = /* @__PURE__ */ new Map();
364
+ this.instancesByConstructor.set(Type, instances);
365
+ }
366
+ return instances;
367
+ }
368
+ /**
369
+ * Get the instances Map for a specific class (public API for stats/debugging)
370
+ */
371
+ getInstancesMap(Type) {
372
+ return this.instancesByConstructor.get(Type) || EMPTY_INSTANCES_MAP;
373
+ }
374
+ /**
375
+ * Insert an entry, replacing the one at `instanceKey`. The instance is not
376
+ * configured here.
377
+ *
378
+ * @internal Used by testing helpers only.
379
+ */
380
+ [INSERT_INSTANCE](Type, instanceKey, instance, refs = /* @__PURE__ */ new Map()) {
381
+ const instances = this.ensureInstancesMap(Type);
382
+ const existingEntry = instances.get(instanceKey);
383
+ if (existingEntry && existingEntry.instance !== instance && !existingEntry.instance.$blac.disposed) existingEntry.instance.dispose();
384
+ const entry = {
385
+ instance,
386
+ key: instanceKey,
387
+ refs
388
+ };
389
+ instances.set(instanceKey, entry);
390
+ this._indexEntry(entry);
391
+ this.types.add(Type);
392
+ }
393
+ /**
394
+ * The single source of keying, so `acquire` and `release` agree: explicit
395
+ * key \> `static key(args)` \> structural hash of args \> default key.
396
+ *
397
+ * @param Type - The StateContainer class constructor
398
+ * @param instanceKey - Explicit key, or undefined to derive from args
399
+ * @param args - Construction args used for structural/`static key` derivation
400
+ */
401
+ resolveKey(Type, instanceKey, args) {
402
+ if (instanceKey !== void 0) return instanceKey;
403
+ const keyFn = getClassKey(Type);
404
+ if (keyFn) return keyFn(args);
405
+ if (args !== void 0) return structuralKey(args);
406
+ return DEFAULT_STRUCTURAL_KEY;
407
+ }
408
+ assertInstanceLimit(Type, currentCount) {
409
+ const limit = getBlacConfig().maxInstancesPerType;
410
+ if (limit > 0 && currentCount >= limit) throw new Error(`${BLAC_ERROR_PREFIX} ${getBlacName(Type)} 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 })\`.`);
411
+ }
412
+ assertRefLimit(Type, resolvedKey, refCount) {
413
+ const limit = getBlacConfig().maxRefsPerInstance;
414
+ if (limit > 0 && refCount > limit) throw new Error(`${BLAC_ERROR_PREFIX} ${getBlacName(Type)} 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 })\`.`);
415
+ }
416
+ /**
417
+ * Get or create an instance and add a ref; `release()` it with the same
418
+ * refId.
419
+ *
420
+ * @internal Internal key tier. Public callers use the args-based `acquire`
421
+ * wrapper; `useBloc` and `watch` address a pre-resolved key here.
422
+ * @param Type - The StateContainer class constructor
423
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
424
+ * @param options - `canCreate` and `countRef` default to true; `refId` is
425
+ * auto-generated when omitted; `dependent` is the `depend()`-owner, whose
426
+ * edge is released on its disposal; `sweepIfUnowned` is for speculative
427
+ * creates (see {@link _scheduleSweep}).
428
+ */
429
+ acquire(Type, instanceKey, options = {}) {
430
+ const { canCreate = true, countRef = true } = options;
431
+ const args = options.args;
432
+ const resolvedKey = this.resolveKey(Type, instanceKey, args);
433
+ const instances = this.ensureInstancesMap(Type);
434
+ let entry = instances.get(resolvedKey);
435
+ if (entry?.instance.$blac.disposed) {
436
+ instances.delete(resolvedKey);
437
+ entry = void 0;
438
+ }
439
+ if (entry) {
440
+ if (IS_DEV && args !== void 0 && entry.args !== void 0) {
441
+ const incomingKey = structuralKey(args);
442
+ const storedKey = entry.argsKey ??= structuralKey(entry.args);
443
+ if (incomingKey !== storedKey) console.warn(`${BLAC_ERROR_PREFIX} ${getBlacName(Type)} 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.`);
444
+ }
445
+ if (countRef) {
446
+ const refId = options.refId ?? `_auto_${this._autoRefIdCounter++}`;
447
+ const count = entry.refs.get(refId) ?? 0;
448
+ if (count === 0) this.assertRefLimit(Type, resolvedKey, entry.refs.size + 1);
449
+ entry.refs.set(refId, count + 1);
450
+ this.emit("refAcquired", entry.instance, refId);
451
+ }
452
+ if (options.dependent) {
453
+ (entry.dependents ??= /* @__PURE__ */ new Set()).add(options.dependent);
454
+ this._recordDependentEdge(options.dependent, Type, resolvedKey);
455
+ }
456
+ this._syncActivation(entry);
457
+ if (options.sweepIfUnowned && entry.sweepTimer !== void 0) this._scheduleSweep(Type, entry);
458
+ return entry.instance;
459
+ }
460
+ if (!canCreate) throw new Error(`${BLAC_ERROR_PREFIX} ${getBlacName(Type)} instance "${resolvedKey}" not found and creation is disabled.`);
461
+ this.assertInstanceLimit(Type, instances.size);
462
+ const config = {
463
+ instanceId: resolvedKey,
464
+ args,
465
+ registry: this
466
+ };
467
+ const instance = new Type();
468
+ instance[INIT_CONFIG](config);
469
+ const initialRefs = /* @__PURE__ */ new Map();
470
+ let initialRefId;
471
+ if (countRef) {
472
+ initialRefId = options.refId ?? `_auto_${this._autoRefIdCounter++}`;
473
+ initialRefs.set(initialRefId, 1);
474
+ }
475
+ const newEntry = {
476
+ instance,
477
+ key: resolvedKey,
478
+ refs: initialRefs,
479
+ args
480
+ };
481
+ if (options.dependent) {
482
+ (newEntry.dependents ??= /* @__PURE__ */ new Set()).add(options.dependent);
483
+ this._recordDependentEdge(options.dependent, Type, resolvedKey);
484
+ }
485
+ instances.set(resolvedKey, newEntry);
486
+ this._indexEntry(newEntry);
487
+ this.types.add(Type);
488
+ if (initialRefId) this.emit("refAcquired", instance, initialRefId);
489
+ this._syncActivation(newEntry);
490
+ if (options.sweepIfUnowned && this._hasNoOwners(newEntry)) this._scheduleSweep(Type, newEntry);
491
+ return instance;
492
+ }
493
+ /**
494
+ * Dispose a speculative create (a render that never commits, SSR) that is
495
+ * still unowned after `unownedSweepDelayMs`. The delay must outlast a
496
+ * time-sliced render; a repeat speculative acquire restarts it. Opt-in, so a
497
+ * bare `ensure()` stays usable across an `await`.
498
+ */
499
+ _scheduleSweep(Type, entry) {
500
+ clearTimeout(entry.sweepTimer);
501
+ entry.sweepTimer = setTimeout(() => {
502
+ entry.sweepTimer = void 0;
503
+ const instances = this.instancesByConstructor.get(Type);
504
+ if (instances?.get(entry.key) !== entry) return;
505
+ if (entry.instance.$blac.disposed) return;
506
+ if (!this._isUnowned(Type, entry)) return;
507
+ entry.instance.dispose();
508
+ instances.delete(entry.key);
509
+ }, getBlacConfig().unownedSweepDelayMs);
510
+ }
511
+ /**
512
+ * Get an existing instance without adding a ref.
513
+ *
514
+ * @internal Internal key tier; public callers use the args-based `borrow`.
515
+ * @param Type - The StateContainer class constructor
516
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
517
+ * @returns The state container instance
518
+ * @throws Error if instance doesn't exist
519
+ */
520
+ borrow(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
521
+ return this.acquire(Type, instanceKey, {
522
+ canCreate: false,
523
+ countRef: false
524
+ });
525
+ }
526
+ /**
527
+ * Like `borrow`, but returns the error instead of throwing.
528
+ *
529
+ * @internal Internal key tier; public callers use the args-based `borrowSafe`.
530
+ * @param Type - The StateContainer class constructor
531
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
532
+ * @returns Discriminated union with either the instance or an error
533
+ */
534
+ borrowSafe(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
535
+ try {
536
+ return {
537
+ error: null,
538
+ instance: this.borrow(Type, instanceKey)
539
+ };
540
+ } catch (error) {
541
+ return {
542
+ error,
543
+ instance: null
544
+ };
545
+ }
546
+ }
547
+ /**
548
+ * Get or create an instance without adding a ref.
549
+ *
550
+ * @internal Internal key tier; public callers use the args-based `ensure`.
551
+ * @param Type - The StateContainer class constructor
552
+ * @param instanceKey - Pre-resolved instance key, or undefined to derive from `args`
553
+ * (`static key(args)` / structural hash), matching `acquire`.
554
+ * @param args - Construction args; used for keying when no explicit key is given
555
+ * @returns The state container instance
556
+ */
557
+ ensure(Type, instanceKey, args) {
558
+ return this.acquire(Type, instanceKey, {
559
+ canCreate: true,
560
+ countRef: false,
561
+ args
562
+ });
563
+ }
564
+ /**
565
+ * Remove a ref and dispose the instance once nothing owns it. Releasing a
566
+ * ref that isn't held is a no-op.
567
+ *
568
+ * @internal Internal key tier; public callers use the args-based `release`.
569
+ * @param Type - The StateContainer class constructor
570
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
571
+ * @param forceDispose - Force immediate disposal regardless of refs
572
+ * @param refId - The specific ref to remove; removes one arbitrary ref if omitted
573
+ */
574
+ release(Type, instanceKey = DEFAULT_STRUCTURAL_KEY, forceDispose = false, refId) {
575
+ const instances = this.instancesByConstructor.get(Type);
576
+ const entry = instances?.get(instanceKey);
577
+ if (!instances || !entry) return;
578
+ if (forceDispose) {
579
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
580
+ instances.delete(instanceKey);
581
+ return;
582
+ }
583
+ const releasedRefId = refId ?? entry.refs.keys().next().value;
584
+ if (releasedRefId === void 0) return;
585
+ const count = entry.refs.get(releasedRefId) ?? 0;
586
+ if (count === 0) return;
587
+ if (count === 1) entry.refs.delete(releasedRefId);
588
+ else entry.refs.set(releasedRefId, count - 1);
589
+ this.emit("refReleased", entry.instance, releasedRefId);
590
+ if (this._isUnowned(Type, entry)) {
591
+ if (!entry.instance.$blac.disposed) entry.instance.dispose();
592
+ instances.delete(instanceKey);
593
+ return;
594
+ }
595
+ this._syncActivation(entry);
596
+ }
597
+ /** Live instances of `Type`. */
598
+ getAll(Type) {
599
+ const instances = this.getInstancesMap(Type);
600
+ const result = [];
601
+ for (const entry of instances.values()) if (!entry.instance.$blac.disposed) result.push(entry.instance);
602
+ return result;
603
+ }
604
+ /** Iterate live instances of `Type`; callback errors are logged. */
605
+ forEach(Type, callback) {
606
+ const instances = this.getInstancesMap(Type);
607
+ for (const entry of instances.values()) {
608
+ const instance = entry.instance;
609
+ if (!instance.$blac.disposed) try {
610
+ callback(instance);
611
+ } catch (error) {
612
+ console.error(`${BLAC_ERROR_PREFIX} forEach callback error for ${getBlacName(Type)}:`, error);
613
+ }
614
+ }
615
+ }
616
+ /** Dispose all instances of `Type`. */
617
+ clear(Type) {
618
+ const instances = this.instancesByConstructor.get(Type);
619
+ if (!instances) return;
620
+ for (const entry of instances.values()) if (!entry.instance.$blac.disposed) entry.instance.dispose();
621
+ instances.clear();
622
+ }
623
+ /**
624
+ * Get reference count for an instance (number of active refs).
625
+ *
626
+ * @internal Internal key tier; public callers use the args-based `getRefCount`.
627
+ * @param Type - The StateContainer class constructor
628
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
629
+ * @returns Current ref count (0 if instance doesn't exist)
630
+ */
631
+ getRefCount(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
632
+ return this.getInstancesMap(Type).get(instanceKey)?.refs.size ?? 0;
633
+ }
634
+ /**
635
+ * Get all active reference IDs for an instance.
636
+ *
637
+ * @internal Internal key tier; public callers use the args-based `getRefIds`.
638
+ * @param Type - The StateContainer class constructor
639
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
640
+ * @returns Array of ref ID strings (empty if instance doesn't exist)
641
+ */
642
+ getRefIds(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
643
+ const entry = this.getInstancesMap(Type).get(instanceKey);
644
+ return entry ? Array.from(entry.refs.keys()) : [];
645
+ }
646
+ /** Ref ids by `$blac.id`, for `PluginContext.getRefIds`. */
647
+ getRefIdsById(instanceId) {
648
+ const entry = this._entryById.get(instanceId);
649
+ return entry ? Array.from(entry.refs.keys()) : [];
650
+ }
651
+ /**
652
+ * Check if an instance exists.
653
+ *
654
+ * @internal Internal key tier; public callers use the args-based `hasInstance`.
655
+ * @param Type - The StateContainer class constructor
656
+ * @param instanceKey - Pre-resolved instance key (defaults to 'default')
657
+ * @returns true if a live (not disposed) instance exists
658
+ */
659
+ hasInstance(Type, instanceKey = DEFAULT_STRUCTURAL_KEY) {
660
+ const entry = this.getInstancesMap(Type).get(instanceKey);
661
+ return entry !== void 0 && !entry.instance.$blac.disposed;
662
+ }
663
+ /** Dispose every instance and forget all types (for testing). */
664
+ clearAll() {
665
+ for (const Type of this.types) this.clear(Type);
666
+ this.types.clear();
667
+ }
668
+ /** Instance counts, for debugging. */
669
+ getStats() {
670
+ const typeBreakdown = {};
671
+ let totalInstances = 0;
672
+ for (const Type of this.types) {
673
+ const typeName = Type.name;
674
+ const count = this.getInstancesMap(Type).size;
675
+ typeBreakdown[typeName] = count;
676
+ totalInstances += count;
677
+ }
678
+ return {
679
+ registeredTypes: this.types.size,
680
+ totalInstances,
681
+ typeBreakdown
682
+ };
683
+ }
684
+ /** All registered types (for plugins). */
685
+ getTypes() {
686
+ return Array.from(this.types);
687
+ }
688
+ /**
689
+ * Subscribe to a lifecycle event.
690
+ * @returns Unsubscribe function
691
+ */
692
+ on(event, listener) {
693
+ let instance = this.listeners.get(event);
694
+ if (!instance) {
695
+ instance = /* @__PURE__ */ new Set();
696
+ this.listeners.set(event, instance);
697
+ }
698
+ instance.add(listener);
699
+ return () => {
700
+ this.listeners.get(event)?.delete(listener);
701
+ };
702
+ }
703
+ emit(event, ...args) {
704
+ const listeners = this.listeners.get(event);
705
+ if (!listeners || listeners.size === 0) return;
706
+ for (const listener of listeners) try {
707
+ listener(...args);
708
+ } catch (error) {
709
+ console.error(`${BLAC_ERROR_PREFIX} Listener error for '${event}':`, error);
710
+ }
711
+ }
712
+ /**
713
+ * Queue a microtask-deferred `stateChanged`.
714
+ * @internal - Called by StateContainer.applyState
715
+ */
716
+ notifyStateChanged(container, previousState, newState) {
717
+ if (!this.hasStateChangedListeners) return;
718
+ if (!this._pendingStateChanges) {
719
+ this._pendingStateChanges = [];
720
+ queueMicrotask(() => this.flushStateChanged());
721
+ }
722
+ this._pendingStateChanges.push([
723
+ container,
724
+ previousState,
725
+ newState
726
+ ]);
727
+ }
728
+ /**
729
+ * One event per emit, deliberately not coalesced: devtools and time-travel
730
+ * need every intermediate transition. Plugins get the coalesced lane.
731
+ */
732
+ flushStateChanged() {
733
+ const pending = this._pendingStateChanges;
734
+ this._pendingStateChanges = null;
735
+ if (!pending) return;
736
+ for (const [container, prev, next] of pending) this.emit("stateChanged", container, prev, next);
737
+ }
738
+ };
739
+ /**
740
+ * The global default registry.
741
+ * @public
742
+ */
743
+ const globalRegistry = new StateContainerRegistry();
744
+ //#endregion
745
+ Object.defineProperty(exports, "APPLY_DEPS", {
746
+ enumerable: true,
747
+ get: function() {
748
+ return APPLY_DEPS;
749
+ }
750
+ });
751
+ Object.defineProperty(exports, "BLAC_STATIC_PROPS", {
752
+ enumerable: true,
753
+ get: function() {
754
+ return BLAC_STATIC_PROPS;
755
+ }
756
+ });
757
+ Object.defineProperty(exports, "INIT_CONFIG", {
758
+ enumerable: true,
759
+ get: function() {
760
+ return INIT_CONFIG;
761
+ }
762
+ });
763
+ Object.defineProperty(exports, "INSERT_INSTANCE", {
764
+ enumerable: true,
765
+ get: function() {
766
+ return INSERT_INSTANCE;
767
+ }
768
+ });
769
+ Object.defineProperty(exports, "IS_DEV", {
770
+ enumerable: true,
771
+ get: function() {
772
+ return IS_DEV;
773
+ }
774
+ });
775
+ Object.defineProperty(exports, "ON_DISPOSE", {
776
+ enumerable: true,
777
+ get: function() {
778
+ return ON_DISPOSE;
779
+ }
780
+ });
781
+ Object.defineProperty(exports, "REMOVE_DEPS_OWNER", {
782
+ enumerable: true,
783
+ get: function() {
784
+ return REMOVE_DEPS_OWNER;
785
+ }
786
+ });
787
+ Object.defineProperty(exports, "SET_ACTIVE", {
788
+ enumerable: true,
789
+ get: function() {
790
+ return SET_ACTIVE;
791
+ }
792
+ });
793
+ Object.defineProperty(exports, "StateContainerRegistry", {
794
+ enumerable: true,
795
+ get: function() {
796
+ return StateContainerRegistry;
797
+ }
798
+ });
799
+ Object.defineProperty(exports, "WITH_TRACKED_STATE", {
800
+ enumerable: true,
801
+ get: function() {
802
+ return WITH_TRACKED_STATE;
803
+ }
804
+ });
805
+ Object.defineProperty(exports, "configureBlac", {
806
+ enumerable: true,
807
+ get: function() {
808
+ return configureBlac;
809
+ }
810
+ });
811
+ Object.defineProperty(exports, "getBlacConfig", {
812
+ enumerable: true,
813
+ get: function() {
814
+ return getBlacConfig;
815
+ }
816
+ });
817
+ Object.defineProperty(exports, "getBlacName", {
818
+ enumerable: true,
819
+ get: function() {
820
+ return getBlacName;
821
+ }
822
+ });
823
+ Object.defineProperty(exports, "getClassEquality", {
824
+ enumerable: true,
825
+ get: function() {
826
+ return getClassEquality;
827
+ }
828
+ });
829
+ Object.defineProperty(exports, "globalRegistry", {
830
+ enumerable: true,
831
+ get: function() {
832
+ return globalRegistry;
833
+ }
834
+ });
835
+ Object.defineProperty(exports, "isExcludedFromDevTools", {
836
+ enumerable: true,
837
+ get: function() {
838
+ return isExcludedFromDevTools;
839
+ }
840
+ });
841
+ Object.defineProperty(exports, "isKeepAliveClass", {
842
+ enumerable: true,
843
+ get: function() {
844
+ return isKeepAliveClass;
845
+ }
846
+ });
847
+ Object.defineProperty(exports, "readNodeEnv", {
848
+ enumerable: true,
849
+ get: function() {
850
+ return readNodeEnv;
851
+ }
852
+ });
853
+ Object.defineProperty(exports, "resetBlacConfig", {
854
+ enumerable: true,
855
+ get: function() {
856
+ return resetBlacConfig;
857
+ }
858
+ });
859
+ Object.defineProperty(exports, "shallowEqualState", {
860
+ enumerable: true,
861
+ get: function() {
862
+ return shallowEqualState;
863
+ }
864
+ });
865
+
866
+ //# sourceMappingURL=StateContainerRegistry-BvSRfhKv.cjs.map