@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.
- package/dist/StateContainerRegistry-141uHhtF.cjs.map +1 -1
- package/dist/StateContainerRegistry-BH7aOMWV.js +892 -0
- package/dist/StateContainerRegistry-BH7aOMWV.js.map +1 -0
- package/dist/StateContainerRegistry-BsH_s0pJ.js +895 -0
- package/dist/StateContainerRegistry-BsH_s0pJ.js.map +1 -0
- package/dist/StateContainerRegistry-CBLeHObO.cjs +1002 -0
- package/dist/StateContainerRegistry-CBLeHObO.cjs.map +1 -0
- package/dist/StateContainerRegistry-MkT0W4C2.cjs +999 -0
- package/dist/StateContainerRegistry-MkT0W4C2.cjs.map +1 -0
- package/dist/StateContainerRegistry-vWgsD-RP.js.map +1 -1
- package/dist/constants.d.cts +0 -7
- package/dist/constants.d.ts +0 -7
- package/dist/core/StateContainer.d.ts +16 -7
- package/dist/core/StateContainerRegistry.d.ts +9 -4
- package/dist/core/meta.d.ts +34 -41
- package/dist/ensure-BjhbDW8Z.cjs +761 -0
- package/dist/ensure-BjhbDW8Z.cjs.map +1 -0
- package/dist/ensure-BnVyd6s4.js +730 -0
- package/dist/ensure-BnVyd6s4.js.map +1 -0
- package/dist/ensure-CneMkpbI.js +731 -0
- package/dist/ensure-CneMkpbI.js.map +1 -0
- package/dist/ensure-D15vGnk5.cjs +783 -0
- package/dist/ensure-D15vGnk5.cjs.map +1 -0
- package/dist/ensure-D3w6YGp9.cjs +784 -0
- package/dist/ensure-D3w6YGp9.cjs.map +1 -0
- package/dist/ensure-dsqWhIuO.js +708 -0
- package/dist/ensure-dsqWhIuO.js.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/PluginManager.d.ts +9 -4
- package/dist/plugins.cjs +7 -4
- package/dist/plugins.cjs.map +1 -1
- package/dist/plugins.js +7 -4
- package/dist/plugins.js.map +1 -1
- package/dist/testing.cjs +2 -2
- package/dist/testing.js +2 -2
- package/package.json +2 -3
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { a as ON_DISPOSE, c as WITH_TRACKED_STATE, g as getBlacConfig, i as INIT_CONFIG, l as getBlacName, m as IS_DEV, n as globalRegistry, o as REMOVE_DEPS_OWNER, r as APPLY_DEPS, s as SET_ACTIVE, u as getClassEquality } from "./StateContainerRegistry-BH7aOMWV.js";
|
|
2
|
+
import { ALL_PATHS, StructuralContainer } from "@dirtytalk/structural";
|
|
3
|
+
//#region src/utils/idGenerator.ts
|
|
4
|
+
/**
|
|
5
|
+
* Centralized ID Generation
|
|
6
|
+
*
|
|
7
|
+
* Provides consistent, collision-resistant ID generation for all BlaC subsystems.
|
|
8
|
+
* Uses timestamp + counter + random suffix for uniqueness.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generate simple ID with timestamp and random (no counter tracking)
|
|
12
|
+
*
|
|
13
|
+
* Format: `${prefix}:${timestamp}_${random}`
|
|
14
|
+
*
|
|
15
|
+
* @param prefix - Prefix for the ID
|
|
16
|
+
* @returns Branded ID string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const id = generateSimpleId('CounterBloc');
|
|
21
|
+
* // Returns: "CounterBloc:1698765432100_a3k9d7f2q"
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function generateSimpleId(prefix, affix) {
|
|
25
|
+
if (affix) return `${prefix}:${affix}`;
|
|
26
|
+
return `${prefix}:${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/registry/config.ts
|
|
30
|
+
let _registry = globalRegistry;
|
|
31
|
+
/**
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
function getRegistry() {
|
|
35
|
+
return _registry;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
function setRegistry(registry) {
|
|
41
|
+
_registry = registry;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
function getStats() {
|
|
47
|
+
return _registry.getStats();
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/core/meta.ts
|
|
51
|
+
/**
|
|
52
|
+
* `$blac` meta namespace for `StateContainer`.
|
|
53
|
+
*
|
|
54
|
+
* `BlacMeta` and `BlacHydration` are classes whose accessors live on the
|
|
55
|
+
* prototype and read the container through one `#private` back-reference, so
|
|
56
|
+
* a container allocates a single small object here instead of a literal with
|
|
57
|
+
* seven closures. Members are therefore not own-enumerable: `Object.keys` and
|
|
58
|
+
* `JSON.stringify` see an empty object. Read the fields by name.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Non-enumerable brand carried on every `$blac` object. Used by the clobber
|
|
62
|
+
* guard in `StateContainer` to detect a subclass class-field shadowing the
|
|
63
|
+
* base's own `$blac` property, and by the meta tests. Intentionally NOT
|
|
64
|
+
* re-exported from the package barrel.
|
|
65
|
+
*/
|
|
66
|
+
const META_BRAND = Symbol("blac.meta");
|
|
67
|
+
const EMPTY_DEPS = /* @__PURE__ */ new Map();
|
|
68
|
+
/**
|
|
69
|
+
* Hydration sub-surface of {@link BlacMeta}. Exposes the container's
|
|
70
|
+
* hydration state machine — the `begin`/`apply`/`finish`/`fail`/`wait`
|
|
71
|
+
* methods and the `status`/`error`/`isHydrated`/`changedWhileHydrating`
|
|
72
|
+
* getters — namespaced under `$blac`.
|
|
73
|
+
*/
|
|
74
|
+
var BlacHydration = class {
|
|
75
|
+
#c;
|
|
76
|
+
/** @internal */
|
|
77
|
+
constructor(container) {
|
|
78
|
+
this.#c = container;
|
|
79
|
+
Object.freeze(this);
|
|
80
|
+
}
|
|
81
|
+
get status() {
|
|
82
|
+
return this.#c._hydrationStatus;
|
|
83
|
+
}
|
|
84
|
+
get error() {
|
|
85
|
+
return this.#c._hydrationError;
|
|
86
|
+
}
|
|
87
|
+
get isHydrated() {
|
|
88
|
+
return this.#c._hydrationStatus === "hydrated";
|
|
89
|
+
}
|
|
90
|
+
get changedWhileHydrating() {
|
|
91
|
+
return this.#c._changedWhileHydrating;
|
|
92
|
+
}
|
|
93
|
+
begin() {
|
|
94
|
+
this.#c._beginHydration();
|
|
95
|
+
}
|
|
96
|
+
apply(next) {
|
|
97
|
+
return this.#c._applyHydratedState(next);
|
|
98
|
+
}
|
|
99
|
+
finish() {
|
|
100
|
+
this.#c._finishHydration();
|
|
101
|
+
}
|
|
102
|
+
fail(error) {
|
|
103
|
+
this.#c._failHydration(error);
|
|
104
|
+
}
|
|
105
|
+
wait() {
|
|
106
|
+
return this.#c._waitForHydration();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* The reserved `$blac` instance member on every `StateContainer`. Gathers the
|
|
111
|
+
* container's identity/lifecycle/hydration surface (`name`, `id`, `debug`,
|
|
112
|
+
* `createdAt`, `disposed`, `dependencies`, and the hydration methods) under one
|
|
113
|
+
* namespace, keeping the generic names free for userland blocs.
|
|
114
|
+
*
|
|
115
|
+
* Allocated once per container and frozen. Every accessor reads the
|
|
116
|
+
* container's current `_`-private fields live — values are never snapshotted.
|
|
117
|
+
*/
|
|
118
|
+
var BlacMeta = class {
|
|
119
|
+
#c;
|
|
120
|
+
#hydration;
|
|
121
|
+
/** @internal Called from the `StateContainer` field initializer. */
|
|
122
|
+
constructor(container) {
|
|
123
|
+
this.#c = container;
|
|
124
|
+
Object.freeze(this);
|
|
125
|
+
}
|
|
126
|
+
get name() {
|
|
127
|
+
return this.#c._name;
|
|
128
|
+
}
|
|
129
|
+
get id() {
|
|
130
|
+
const c = this.#c;
|
|
131
|
+
return c._instanceId ??= generateSimpleId(getBlacName(c.constructor), "main");
|
|
132
|
+
}
|
|
133
|
+
get debug() {
|
|
134
|
+
return this.#c._debug;
|
|
135
|
+
}
|
|
136
|
+
get createdAt() {
|
|
137
|
+
return this.#c._createdAt;
|
|
138
|
+
}
|
|
139
|
+
get disposed() {
|
|
140
|
+
return this.#c._disposed;
|
|
141
|
+
}
|
|
142
|
+
get dependencies() {
|
|
143
|
+
return this.#c._dependencies ?? EMPTY_DEPS;
|
|
144
|
+
}
|
|
145
|
+
/** Built on first access; identity-stable afterwards. */
|
|
146
|
+
get hydration() {
|
|
147
|
+
return this.#hydration ??= new BlacHydration(this.#c);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
Object.defineProperty(BlacMeta.prototype, META_BRAND, {
|
|
151
|
+
value: true,
|
|
152
|
+
enumerable: false,
|
|
153
|
+
writable: false,
|
|
154
|
+
configurable: false
|
|
155
|
+
});
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/core/StateContainer.ts
|
|
158
|
+
/**
|
|
159
|
+
* Brand symbol carried (non-enumerable) on every handle returned by
|
|
160
|
+
* `depend()`. Framework adapters (`@blac/react`) use this to detect handles
|
|
161
|
+
* inside a tracked-proxy and swap in a session-bound wrapper.
|
|
162
|
+
*
|
|
163
|
+
* @internal — exported for `@blac/react`; not part of the public API.
|
|
164
|
+
*/
|
|
165
|
+
const DEP_BRAND = Symbol("blac.depHandle");
|
|
166
|
+
const EMPTY_RECORD = Object.freeze({});
|
|
167
|
+
let deactivatedReason;
|
|
168
|
+
/**
|
|
169
|
+
* Shallow per-key `Object.is` comparison of two plain records. Keys are
|
|
170
|
+
* considered: a key present in one but not the other (regardless of value)
|
|
171
|
+
* makes the records unequal. Used to detect real deps changes.
|
|
172
|
+
*/
|
|
173
|
+
function shallowEqualRecord(a, b) {
|
|
174
|
+
if (a === b) return true;
|
|
175
|
+
const aKeys = Object.keys(a);
|
|
176
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
177
|
+
for (const key of aKeys) {
|
|
178
|
+
if (!(key in b)) return false;
|
|
179
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* BlaC's lifecycle/identity/dependency layer on top of `StructuralContainer`.
|
|
185
|
+
*
|
|
186
|
+
* StructuralContainer provides:
|
|
187
|
+
* - `state` getter
|
|
188
|
+
* - `emit` / `patch` / `update` (path-tracked, microtask-flushed)
|
|
189
|
+
* - `channel` (the underlying `DirtyChannel`)
|
|
190
|
+
* - `registerConsumerPaths` / `unregisterConsumer` (for fine-grained consumers)
|
|
191
|
+
* - per-class `PathInterner`
|
|
192
|
+
*
|
|
193
|
+
* StateContainer layers on:
|
|
194
|
+
* - identity / lifecycle / hydration (exposed via `$blac: BlacMeta`)
|
|
195
|
+
* - cross-bloc deps (`depend()`, `$blac.dependencies`)
|
|
196
|
+
* - per-consumer deps slices (`APPLY_DEPS` / `REMOVE_DEPS_OWNER` / `onDepsChanged`)
|
|
197
|
+
* - registry integration (config-driven equality, emit-rate circuit breaker)
|
|
198
|
+
*
|
|
199
|
+
* Subscribers can attach via:
|
|
200
|
+
* - `onSystemEvent('stateChanged' | 'dispose' | 'hydrationChanged', cb)` —
|
|
201
|
+
* coarse lifecycle events.
|
|
202
|
+
* - `this.channel.subscribe(interest, cb)` — path-scoped.
|
|
203
|
+
*/
|
|
204
|
+
var StateContainer = class extends StructuralContainer {
|
|
205
|
+
static {
|
|
206
|
+
this.__excludeFromDevTools = false;
|
|
207
|
+
}
|
|
208
|
+
get deps() {
|
|
209
|
+
return this._deps;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* @internal Apply one owner's (consumer's) deps slice. Shallow-merges the
|
|
213
|
+
* slice, reconciles keys the owner dropped since its last apply (other
|
|
214
|
+
* owners' keys untouched), dev-warns on cross-owner key collisions, then
|
|
215
|
+
* recomputes the merged view and fires onDepsChanged if it changed.
|
|
216
|
+
*
|
|
217
|
+
* Idempotent: re-applying an identical slice for the same owner is a no-op.
|
|
218
|
+
*/
|
|
219
|
+
[APPLY_DEPS](ownerId, slice) {
|
|
220
|
+
if (this._disposed) return;
|
|
221
|
+
const owners = this._depsByOwner ??= /* @__PURE__ */ new Map();
|
|
222
|
+
const prevSlice = owners.get(ownerId);
|
|
223
|
+
if (prevSlice && shallowEqualRecord(prevSlice, slice)) return;
|
|
224
|
+
if (IS_DEV) for (const key of Object.keys(slice)) for (const [otherOwner, otherSlice] of owners) {
|
|
225
|
+
if (otherOwner === ownerId) continue;
|
|
226
|
+
if (key in otherSlice && otherSlice[key] !== slice[key]) console.warn(`[${this._name}] multiple owners writing dep \`${String(key)}\`; last write wins`);
|
|
227
|
+
}
|
|
228
|
+
owners.set(ownerId, { ...slice });
|
|
229
|
+
this.reconcileDeps();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* @internal Withdraw an owner's entire deps slice (consumer unmounted).
|
|
233
|
+
*/
|
|
234
|
+
[REMOVE_DEPS_OWNER](ownerId) {
|
|
235
|
+
if (this._disposed) return;
|
|
236
|
+
if (!this._depsByOwner?.delete(ownerId)) return;
|
|
237
|
+
this.reconcileDeps();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @internal Subscribe to this instance's own disposal. Delegates to the
|
|
241
|
+
* per-instance `'dispose'` system event — used by `watch()` in place of
|
|
242
|
+
* filtering the global registry `'disposed'` event.
|
|
243
|
+
*/
|
|
244
|
+
[ON_DISPOSE](handler) {
|
|
245
|
+
return this.onSystemEvent("dispose", handler);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* @internal Drive the 0↔1 ownership transition. Idempotent: repeated calls
|
|
249
|
+
* with the same value are no-ops, so the registry can call it from every
|
|
250
|
+
* acquire/release without tracking edges itself.
|
|
251
|
+
*
|
|
252
|
+
* Returns which real transition (if any) just happened, so the registry can
|
|
253
|
+
* emit the matching `activated`/`deactivated` plugin event without exposing
|
|
254
|
+
* `_activation` itself — `'none'` for an idempotent no-op call.
|
|
255
|
+
*/
|
|
256
|
+
[SET_ACTIVE](active) {
|
|
257
|
+
if (this._disposed) return "none";
|
|
258
|
+
if (active === (this._activation !== null)) return "none";
|
|
259
|
+
if (active) {
|
|
260
|
+
this._activation = new AbortController();
|
|
261
|
+
this.onActivate(this._activation.signal);
|
|
262
|
+
return {
|
|
263
|
+
kind: "activated",
|
|
264
|
+
signal: this._activation.signal
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
this._abortActivation();
|
|
268
|
+
this.onDeactivate();
|
|
269
|
+
return "deactivated";
|
|
270
|
+
}
|
|
271
|
+
_abortActivation() {
|
|
272
|
+
this._activation?.abort(deactivatedReason ??= new DOMException("The bloc was deactivated", "AbortError"));
|
|
273
|
+
this._activation = null;
|
|
274
|
+
}
|
|
275
|
+
reconcileDeps() {
|
|
276
|
+
const prev = this._deps;
|
|
277
|
+
const next = {};
|
|
278
|
+
if (this._depsByOwner) for (const slice of this._depsByOwner.values()) Object.assign(next, slice);
|
|
279
|
+
for (const key of Object.keys(prev)) if (!(key in next)) next[key] = void 0;
|
|
280
|
+
if (shallowEqualRecord(prev, next)) return;
|
|
281
|
+
this._deps = next;
|
|
282
|
+
this.onDepsChanged(next, prev);
|
|
283
|
+
this._registry.emit("depsChanged", this, prev, next);
|
|
284
|
+
}
|
|
285
|
+
onDepsChanged(_next, _prev) {}
|
|
286
|
+
get state() {
|
|
287
|
+
return this._stateOverride ?? super.state;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* @internal Run `fn` (with `this` = this instance, so a prototype getter can
|
|
291
|
+
* be passed directly) while `state` reports `tracked` and dep handles read
|
|
292
|
+
* off `this` route through `onDepHandle`. Restored in `finally`, and
|
|
293
|
+
* nestable — getters reading other getters stay in tracked context.
|
|
294
|
+
*
|
|
295
|
+
* The dep hook lives here rather than in the adapter's proxy because getters
|
|
296
|
+
* now run with `this` = the real instance (so ES `#private` works), which
|
|
297
|
+
* means a `this.someDep` read never passes through the proxy's trap.
|
|
298
|
+
*/
|
|
299
|
+
[WITH_TRACKED_STATE](tracked, fn, onDepHandle) {
|
|
300
|
+
const prevState = this._stateOverride;
|
|
301
|
+
const prevHook = this._depHandleHook;
|
|
302
|
+
this._stateOverride = tracked;
|
|
303
|
+
this._depHandleHook = onDepHandle;
|
|
304
|
+
try {
|
|
305
|
+
return fn.call(this);
|
|
306
|
+
} finally {
|
|
307
|
+
this._stateOverride = prevState;
|
|
308
|
+
this._depHandleHook = prevHook;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
get args() {
|
|
312
|
+
return this._config.args;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Declare a cross-bloc dependency. Returns a branded handle with two
|
|
316
|
+
* accessors — the dep instance is resolved against the registry on each call,
|
|
317
|
+
* which keeps the surface immune to dep-instance churn:
|
|
318
|
+
*
|
|
319
|
+
* - `handle.track(options?)` — reactive read. Returns `[state, instance]`.
|
|
320
|
+
* Inside a getter reached through the React proxy this subscribes the
|
|
321
|
+
* reading component to the dep's changes (base impl: live, no subscription).
|
|
322
|
+
* - `handle.untracked(options?)` — returns the live instance with no tracking,
|
|
323
|
+
* for imperative method calls and one-off reads.
|
|
324
|
+
*
|
|
325
|
+
* `defaultArgs` resolves the dep instance when an accessor is called without
|
|
326
|
+
* its own `args`; per-call `options.args` overrides it and can derive from
|
|
327
|
+
* current state. This does NOT auto-resubscribe outside the React proxy;
|
|
328
|
+
* non-React consumers needing updates should subscribe explicitly.
|
|
329
|
+
*/
|
|
330
|
+
depend(Type, defaultArgs) {
|
|
331
|
+
if (!this._dependencies) this._dependencies = /* @__PURE__ */ new Map();
|
|
332
|
+
this._dependencies.set(Type, this._registry.resolveKey(Type, void 0, defaultArgs));
|
|
333
|
+
const resolve = (args) => {
|
|
334
|
+
const effectiveArgs = args ?? defaultArgs;
|
|
335
|
+
const key = this._registry.resolveKey(Type, void 0, effectiveArgs);
|
|
336
|
+
return this._registry.acquire(Type, key, {
|
|
337
|
+
canCreate: true,
|
|
338
|
+
countRef: false,
|
|
339
|
+
args: effectiveArgs,
|
|
340
|
+
dependent: this
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
const handle = {
|
|
344
|
+
track: (options) => {
|
|
345
|
+
const wrapped = this._depHandleHook?.(handle);
|
|
346
|
+
if (wrapped !== void 0 && wrapped !== handle) return wrapped.track(options);
|
|
347
|
+
const instance = resolve(options?.args);
|
|
348
|
+
return [instance.state, instance];
|
|
349
|
+
},
|
|
350
|
+
untracked: (options) => resolve(options?.args)
|
|
351
|
+
};
|
|
352
|
+
Object.defineProperty(handle, DEP_BRAND, {
|
|
353
|
+
value: {
|
|
354
|
+
Type,
|
|
355
|
+
defaultArgs
|
|
356
|
+
},
|
|
357
|
+
enumerable: false,
|
|
358
|
+
writable: false,
|
|
359
|
+
configurable: false
|
|
360
|
+
});
|
|
361
|
+
return handle;
|
|
362
|
+
}
|
|
363
|
+
constructor(initialState, options) {
|
|
364
|
+
super(initialState, options);
|
|
365
|
+
this._depsByOwner = null;
|
|
366
|
+
this._deps = EMPTY_RECORD;
|
|
367
|
+
this._disposed = false;
|
|
368
|
+
this._hydrationStatus = "idle";
|
|
369
|
+
this._changedWhileHydrating = false;
|
|
370
|
+
this._hydrationPromise = null;
|
|
371
|
+
this._hydrationPromiseSettled = false;
|
|
372
|
+
this._config = EMPTY_RECORD;
|
|
373
|
+
this._initCalled = false;
|
|
374
|
+
this._activation = null;
|
|
375
|
+
this._emitWindowStart = 0;
|
|
376
|
+
this._emitCount = 0;
|
|
377
|
+
this._emitRateWarned = false;
|
|
378
|
+
this._systemEventHandlers = null;
|
|
379
|
+
this._dependencies = null;
|
|
380
|
+
this._pendingChange = null;
|
|
381
|
+
this._bridgeUnsub = null;
|
|
382
|
+
this._registry = getRegistry();
|
|
383
|
+
this._equalityFn = getBlacConfig().equality;
|
|
384
|
+
this._name = getBlacName(this.constructor);
|
|
385
|
+
this._debug = false;
|
|
386
|
+
this._createdAt = Date.now();
|
|
387
|
+
this.$blac = new BlacMeta(this);
|
|
388
|
+
this.onSystemEvent = (event, handler) => {
|
|
389
|
+
const all = this._systemEventHandlers ??= /* @__PURE__ */ new Map();
|
|
390
|
+
let handlers = all.get(event);
|
|
391
|
+
if (!handlers) {
|
|
392
|
+
handlers = /* @__PURE__ */ new Set();
|
|
393
|
+
all.set(event, handlers);
|
|
394
|
+
}
|
|
395
|
+
handlers.add(handler);
|
|
396
|
+
if (event === "stateChanged" && this._bridgeUnsub === null) this._bridgeUnsub = this.channel.subscribe(() => ALL_PATHS, () => this._drainPending());
|
|
397
|
+
return () => {
|
|
398
|
+
handlers?.delete(handler);
|
|
399
|
+
if (event === "stateChanged" && handlers?.size === 0) {
|
|
400
|
+
this._bridgeUnsub?.();
|
|
401
|
+
this._bridgeUnsub = null;
|
|
402
|
+
this._pendingChange = null;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Called once after construction with the args passed at acquire time, before the first
|
|
409
|
+
* state snapshot is read by any consumer. Override to seed args-derived state (via
|
|
410
|
+
* this.emit(...)) or kick off loads.
|
|
411
|
+
*/
|
|
412
|
+
init(_args) {}
|
|
413
|
+
/**
|
|
414
|
+
* Called when this instance goes from unowned to owned (first ref or first
|
|
415
|
+
* `depend()` dependent). Override to start work that should only run while
|
|
416
|
+
* something is actually using the instance — fetches, timers, listeners.
|
|
417
|
+
*
|
|
418
|
+
* `signal` aborts on the matching {@link onDeactivate} and on dispose, so
|
|
419
|
+
* async work started here can be cancelled without extra bookkeeping.
|
|
420
|
+
*
|
|
421
|
+
* Runs before the first `useBlocDeps` slice is applied, so `this.deps` is
|
|
422
|
+
* empty on first activation — use `onDepsChanged` for deps-driven work.
|
|
423
|
+
*/
|
|
424
|
+
onActivate(_signal) {}
|
|
425
|
+
/**
|
|
426
|
+
* Called when this instance goes from owned back to unowned while surviving
|
|
427
|
+
* in the registry (i.e. `keepAlive`). A disposed instance does NOT get this
|
|
428
|
+
* callback — `dispose()` is the teardown path for that case.
|
|
429
|
+
*/
|
|
430
|
+
onDeactivate() {}
|
|
431
|
+
/**
|
|
432
|
+
* @internal Framework-only configuration entry point (registry + testing
|
|
433
|
+
* helpers). Writes the `_`-private identity fields directly, resolves
|
|
434
|
+
* per-class equality, emits the registry `created` event, and runs `init()`
|
|
435
|
+
* once. See {@link INIT_CONFIG}.
|
|
436
|
+
*/
|
|
437
|
+
[INIT_CONFIG](config) {
|
|
438
|
+
this._config = { ...config };
|
|
439
|
+
const className = getBlacName(this.constructor);
|
|
440
|
+
this._name = this._config.name || className;
|
|
441
|
+
this._debug = this._config.debug ?? false;
|
|
442
|
+
this._instanceId = generateSimpleId(className, this._config.instanceId);
|
|
443
|
+
const perClass = getClassEquality(this.constructor);
|
|
444
|
+
this._equalityFn = perClass ?? getBlacConfig().equality;
|
|
445
|
+
if (!this._initCalled) {
|
|
446
|
+
this._initCalled = true;
|
|
447
|
+
this.init(this._config.args);
|
|
448
|
+
}
|
|
449
|
+
this._registry.emit("created", this);
|
|
450
|
+
if (IS_DEV) {
|
|
451
|
+
if (!this.$blac?.[META_BRAND]) console.warn(`[blac] ${this.constructor.name} appears to declare its own \`$blac\` member, which shadows the reserved meta namespace. \`$blac\` is reserved by StateContainer — remove the subclass field.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
dispose() {
|
|
455
|
+
if (this._disposed) return;
|
|
456
|
+
if (this._debug) console.log(`[${this._name}] Disposing...`);
|
|
457
|
+
if (this._depsByOwner) {
|
|
458
|
+
this._depsByOwner.clear();
|
|
459
|
+
this.reconcileDeps();
|
|
460
|
+
}
|
|
461
|
+
this._disposed = true;
|
|
462
|
+
if (this._hydrationStatus === "hydrating") this._failHydration(/* @__PURE__ */ new Error(`Hydration cancelled because ${this._name} was disposed`));
|
|
463
|
+
this.emitSystemEvent("dispose", void 0);
|
|
464
|
+
this._bridgeUnsub?.();
|
|
465
|
+
this._bridgeUnsub = null;
|
|
466
|
+
this._abortActivation();
|
|
467
|
+
this._systemEventHandlers = null;
|
|
468
|
+
this._pendingChange = null;
|
|
469
|
+
this._registry.emit("disposed", this);
|
|
470
|
+
super.dispose();
|
|
471
|
+
if (this._debug) console.log(`[${this._name}] Disposed successfully`);
|
|
472
|
+
}
|
|
473
|
+
emit(next) {
|
|
474
|
+
this.applyState(next, "default");
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Override of `StructuralContainer.patch` that routes through the
|
|
478
|
+
* StateContainer concerns: disposed guard, dev-only emit-rate check,
|
|
479
|
+
* `_changedWhileHydrating` flag, pending-change capture (so `stateChanged`
|
|
480
|
+
* system events see the merged prev/next), and the registry-level
|
|
481
|
+
* `stateChanged` notification. We still call
|
|
482
|
+
* `super.patch` so path-marking semantics (the whole point of patch) are
|
|
483
|
+
* preserved.
|
|
484
|
+
*/
|
|
485
|
+
patch(partial) {
|
|
486
|
+
if (this._disposed) {
|
|
487
|
+
this._warnDisposedMutation("patch");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const prev = super.state;
|
|
491
|
+
super.patch(partial);
|
|
492
|
+
const next = super.state;
|
|
493
|
+
if (Object.is(prev, next)) return;
|
|
494
|
+
if (IS_DEV && !this._emitRateWarned) this._checkEmitRate();
|
|
495
|
+
if (this._hydrationStatus === "hydrating") this._changedWhileHydrating = true;
|
|
496
|
+
if (this._bridgeUnsub !== null) if (this._pendingChange) this._pendingChange.next = next;
|
|
497
|
+
else this._pendingChange = {
|
|
498
|
+
prev,
|
|
499
|
+
next
|
|
500
|
+
};
|
|
501
|
+
if (this._registry.hasStateChangedListeners) this._registry.notifyStateChanged(this, prev, next);
|
|
502
|
+
}
|
|
503
|
+
applyState(next, source) {
|
|
504
|
+
if (this._disposed) {
|
|
505
|
+
this._warnDisposedMutation("emit");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const prev = super.state;
|
|
509
|
+
if (prev === next) return;
|
|
510
|
+
if (this._equalityFn(prev, next)) return;
|
|
511
|
+
if (IS_DEV && !this._emitRateWarned) this._checkEmitRate();
|
|
512
|
+
if (this._hydrationStatus === "hydrating" && source !== "hydration") this._changedWhileHydrating = true;
|
|
513
|
+
if (this._bridgeUnsub !== null) if (this._pendingChange) this._pendingChange.next = next;
|
|
514
|
+
else this._pendingChange = {
|
|
515
|
+
prev,
|
|
516
|
+
next
|
|
517
|
+
};
|
|
518
|
+
super.emit(next);
|
|
519
|
+
if (this._registry.hasStateChangedListeners) this._registry.notifyStateChanged(this, prev, next);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Called by the channel-bridge subscriber on each flush. Drains the
|
|
523
|
+
* pending change (if any) into the 'stateChanged' system event.
|
|
524
|
+
* No pending change == flush from a no-op patch == skip.
|
|
525
|
+
*/
|
|
526
|
+
_drainPending() {
|
|
527
|
+
const pending = this._pendingChange;
|
|
528
|
+
if (!pending) return;
|
|
529
|
+
this._pendingChange = null;
|
|
530
|
+
const handlers = this._systemEventHandlers?.get("stateChanged");
|
|
531
|
+
if (handlers && handlers.size > 0) {
|
|
532
|
+
const payload = {
|
|
533
|
+
state: pending.next,
|
|
534
|
+
previousState: pending.prev
|
|
535
|
+
};
|
|
536
|
+
let count = 0;
|
|
537
|
+
const size = handlers.size;
|
|
538
|
+
for (const handler of handlers) {
|
|
539
|
+
if (++count > size) break;
|
|
540
|
+
try {
|
|
541
|
+
handler(payload);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error(`[${this._name}] Error in system event handler:`, error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
_beginHydration() {
|
|
549
|
+
if (this._disposed) throw new Error(`Cannot begin hydration for disposed container ${this._name}`);
|
|
550
|
+
this._changedWhileHydrating = false;
|
|
551
|
+
this._hydrationError = void 0;
|
|
552
|
+
this._hydrationPromise = null;
|
|
553
|
+
this._resolveHydrationPromise = void 0;
|
|
554
|
+
this._rejectHydrationPromise = void 0;
|
|
555
|
+
this._hydrationPromiseSettled = false;
|
|
556
|
+
this.ensureHydrationPromise();
|
|
557
|
+
this.setHydrationStatus("hydrating");
|
|
558
|
+
}
|
|
559
|
+
_warnDisposedMutation(op) {
|
|
560
|
+
if (IS_DEV) console.warn(`[blac] ${this._name}: ${op}() after dispose ignored. This usually means an async method resolved after the last consumer unmounted. Guard with \`if (this.$blac.disposed) return\` after each \`await\`, or abort the work from \`onSystemEvent('dispose', ...)\`.`);
|
|
561
|
+
}
|
|
562
|
+
_applyHydratedState(next) {
|
|
563
|
+
if (this._disposed) return false;
|
|
564
|
+
if (this._hydrationStatus !== "hydrating" || this._changedWhileHydrating) {
|
|
565
|
+
if (IS_DEV && this._changedWhileHydrating) console.warn(`[blac] ${this._name}: persisted state discarded because the container emitted while hydrating.`);
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
this.applyState(next, "hydration");
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
_finishHydration() {
|
|
572
|
+
if (this._hydrationStatus !== "hydrating") {
|
|
573
|
+
if (this._hydrationStatus === "hydrated") return;
|
|
574
|
+
if (this._hydrationStatus === "error") this.ensureHydrationPromise();
|
|
575
|
+
}
|
|
576
|
+
this.setHydrationStatus("hydrated");
|
|
577
|
+
this.resolveHydration();
|
|
578
|
+
}
|
|
579
|
+
_failHydration(error) {
|
|
580
|
+
const err = error instanceof Error ? error : /* @__PURE__ */ new Error(`Hydration failed: ${String(error)}`);
|
|
581
|
+
this._hydrationError = err;
|
|
582
|
+
this.setHydrationStatus("error", err);
|
|
583
|
+
this.rejectHydration(err);
|
|
584
|
+
}
|
|
585
|
+
_waitForHydration() {
|
|
586
|
+
if (this._hydrationStatus === "idle" || this._hydrationStatus === "hydrated") return Promise.resolve();
|
|
587
|
+
if (this._hydrationStatus === "error") return Promise.reject(this._hydrationError ?? /* @__PURE__ */ new Error(`Hydration failed for container ${this._name}`));
|
|
588
|
+
return this.ensureHydrationPromise();
|
|
589
|
+
}
|
|
590
|
+
setHydrationStatus(status, error) {
|
|
591
|
+
const previousStatus = this._hydrationStatus;
|
|
592
|
+
this._hydrationStatus = status;
|
|
593
|
+
this._hydrationError = error;
|
|
594
|
+
this.emitSystemEvent("hydrationChanged", {
|
|
595
|
+
status,
|
|
596
|
+
previousStatus,
|
|
597
|
+
error,
|
|
598
|
+
changedWhileHydrating: this._changedWhileHydrating
|
|
599
|
+
});
|
|
600
|
+
this._registry.emit("hydrationChanged", this, status, previousStatus);
|
|
601
|
+
}
|
|
602
|
+
ensureHydrationPromise() {
|
|
603
|
+
if (!this._hydrationPromise || this._hydrationPromiseSettled) {
|
|
604
|
+
this._hydrationPromiseSettled = false;
|
|
605
|
+
this._hydrationPromise = new Promise((resolve, reject) => {
|
|
606
|
+
this._resolveHydrationPromise = () => {
|
|
607
|
+
if (this._hydrationPromiseSettled) return;
|
|
608
|
+
this._hydrationPromiseSettled = true;
|
|
609
|
+
resolve();
|
|
610
|
+
};
|
|
611
|
+
this._rejectHydrationPromise = (error) => {
|
|
612
|
+
if (this._hydrationPromiseSettled) return;
|
|
613
|
+
this._hydrationPromiseSettled = true;
|
|
614
|
+
reject(error);
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
this._hydrationPromise.catch(() => {});
|
|
618
|
+
}
|
|
619
|
+
return this._hydrationPromise;
|
|
620
|
+
}
|
|
621
|
+
resolveHydration() {
|
|
622
|
+
this._resolveHydrationPromise?.();
|
|
623
|
+
}
|
|
624
|
+
rejectHydration(error) {
|
|
625
|
+
this.ensureHydrationPromise();
|
|
626
|
+
this._rejectHydrationPromise?.(error);
|
|
627
|
+
}
|
|
628
|
+
emitSystemEvent(event, payload) {
|
|
629
|
+
const handlers = this._systemEventHandlers?.get(event);
|
|
630
|
+
if (!handlers) return;
|
|
631
|
+
for (const handler of handlers) try {
|
|
632
|
+
handler(payload);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error(`[${this._name}] Error in system event handler:`, error);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Dev-only soft circuit breaker. Counts real state changes in a rolling 1s
|
|
639
|
+
* window and warns once if the rate exceeds `maxEmitsPerSecond` — the
|
|
640
|
+
* signature of a runaway loop (RAF/animation or emit-on-every-commit) pushing
|
|
641
|
+
* high-frequency data through state, which freezes subscribers/plugins.
|
|
642
|
+
*/
|
|
643
|
+
_checkEmitRate() {
|
|
644
|
+
const limit = getBlacConfig().maxEmitsPerSecond;
|
|
645
|
+
if (!(limit > 0) || !Number.isFinite(limit)) return;
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
if (now - this._emitWindowStart >= 1e3) {
|
|
648
|
+
this._emitWindowStart = now;
|
|
649
|
+
this._emitCount = 1;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this._emitCount++;
|
|
653
|
+
if (this._emitCount > limit) {
|
|
654
|
+
this._emitRateWarned = true;
|
|
655
|
+
console.warn(`[${this._name}] emitted more than ${limit} state changes in under a second. This is usually a runaway loop pushing high-frequency data through state (e.g. \`emit\`/\`patch\` inside a requestAnimationFrame loop, or an effect that emits on every render). It can freeze the app by saturating subscribers and plugins (logging/devtools). Keep high-frequency work imperative and emit only coarse/throttled state, or raise \`configureBlac({ maxEmitsPerSecond })\`. (This warning fires once.)`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/core/Cubit.ts
|
|
661
|
+
/**
|
|
662
|
+
* `Cubit<S>` is a `StateContainer<S>` that exposes mutation publicly.
|
|
663
|
+
*
|
|
664
|
+
* That is the whole difference, and it is a real one: on `StateContainer`
|
|
665
|
+
* `emit` / `patch` / `update` are `protected`, so a container mutates itself
|
|
666
|
+
* from its own methods and callers go through the API it chooses to publish.
|
|
667
|
+
* `Cubit` re-declares the three as `public` for the cases where a caller
|
|
668
|
+
* legitimately drives state from outside — test helpers, devtools
|
|
669
|
+
* time-travel, benchmarks.
|
|
670
|
+
*
|
|
671
|
+
* Reach for `StateContainer` when business logic should live in the class,
|
|
672
|
+
* and `Cubit` when the caller owns the transitions.
|
|
673
|
+
*/
|
|
674
|
+
var Cubit = class extends StateContainer {
|
|
675
|
+
emit(next) {
|
|
676
|
+
super.emit(next);
|
|
677
|
+
}
|
|
678
|
+
patch(partial) {
|
|
679
|
+
super.patch(partial);
|
|
680
|
+
}
|
|
681
|
+
update(fn) {
|
|
682
|
+
super.update(fn);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/registry/acquire.ts
|
|
687
|
+
/**
|
|
688
|
+
* Acquire an instance with ref tracking (ownership semantics). Instance
|
|
689
|
+
* identity is derived purely from `args` (via the class's `static key(args)`,
|
|
690
|
+
* a structural hash of `args`, or the default sentinel when there are none).
|
|
691
|
+
*
|
|
692
|
+
* @param BlocClass - The StateContainer class constructor
|
|
693
|
+
* @param opts - `args` derives the instance key; `refId` names the reference
|
|
694
|
+
* for debugging and is auto-generated when omitted.
|
|
695
|
+
*/
|
|
696
|
+
function acquire(BlocClass, opts) {
|
|
697
|
+
const registry = getRegistry();
|
|
698
|
+
const key = registry.resolveKey(BlocClass, void 0, opts?.args);
|
|
699
|
+
return registry.acquire(BlocClass, key, {
|
|
700
|
+
canCreate: true,
|
|
701
|
+
countRef: true,
|
|
702
|
+
refId: opts?.refId,
|
|
703
|
+
args: opts?.args
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Resolve the storage key an instance would be acquired under, given its
|
|
708
|
+
* construction `args`. This is the canonical key-computation path. Callers that
|
|
709
|
+
* both acquire and release against the `@internal` registry tier (e.g.
|
|
710
|
+
* `@blac/react` `useBloc`) must use this once and pass the same resolved key to
|
|
711
|
+
* both, so the ref is dropped under the key it was taken.
|
|
712
|
+
*/
|
|
713
|
+
function resolveInstanceKey(BlocClass, args) {
|
|
714
|
+
return getRegistry().resolveKey(BlocClass, void 0, args);
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/registry/ensure.ts
|
|
718
|
+
/**
|
|
719
|
+
* Ensure an instance exists without taking ownership (no ref added). Instance
|
|
720
|
+
* identity is derived purely from `args`, matching `acquire`.
|
|
721
|
+
*/
|
|
722
|
+
function ensure(BlocClass, opts) {
|
|
723
|
+
const registry = getRegistry();
|
|
724
|
+
const key = registry.resolveKey(BlocClass, void 0, opts?.args);
|
|
725
|
+
return registry.ensure(BlocClass, key, opts?.args);
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
export { DEP_BRAND as a, getStats as c, Cubit as i, setRegistry as l, acquire as n, StateContainer as o, resolveInstanceKey as r, getRegistry as s, ensure as t };
|
|
729
|
+
|
|
730
|
+
//# sourceMappingURL=ensure-BnVyd6s4.js.map
|