@sladg/apex-state 4.0.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -284,269 +284,6 @@ type BoolLogic<STATE, Depth extends number = DefaultDepth> = {
284
284
  CONTAINS_ALL: PathArrayElementsPair<STATE, Depth>;
285
285
  } | PathValuePair<STATE, Depth>;
286
286
 
287
- type Stage = "input" | "aggregation_write" | "diff" | "sync" | "flip" | "clear_path" | "aggregation_read" | "computation" | "bool_logic" | "value_logic" | "listeners" | "apply";
288
- type ChangeKind = "real" | "redundant" | "breakdown" | "consumed";
289
- type ChangeContext = "Initial" | "Mutation";
290
- type Lineage = "Input" | {
291
- "Derived": {
292
- parent_id: number;
293
- via: Stage;
294
- context: ChangeContext;
295
- };
296
- };
297
- type SkipReason = "wrong_kind" | "guard_failed" | "redundant" | "anchor_missing";
298
- type SkippedChange = {
299
- path: string;
300
- kind: ChangeKind;
301
- reason: SkipReason;
302
- /**
303
- * Human-readable explanation of why this change was skipped.
304
- */
305
- detail: string;
306
- /**
307
- * Which registration owns the resource that was skipped (if applicable).
308
- */
309
- registration_id: string | null;
310
- /**
311
- * Anchor path that caused the skip (if applicable).
312
- */
313
- anchor_path: string | null;
314
- };
315
- type ProducedChange = {
316
- path: string;
317
- /**
318
- * JSON-encoded value.
319
- */
320
- value: string;
321
- /**
322
- * Which registration produced this change (if traceable).
323
- */
324
- registration_id: string | null;
325
- /**
326
- * What input path caused this (e.g. sync source path).
327
- */
328
- source_path: string | null;
329
- };
330
- type ChangeAudit = {
331
- source_path: string | null;
332
- logic_id: number | null;
333
- };
334
- type Change$1 = {
335
- path: string;
336
- value_json: string;
337
- /**
338
- * Opaque JS meta object — carried through the pipeline unchanged.
339
- * WASM never inspects the contents. Returned on listener_changes so JS
340
- * meta survives the boundary without any stringify/parse.
341
- */
342
- meta: unknown | null;
343
- /**
344
- * Coarse classification: Real | Redundant | Breakdown | Consumed.
345
- */
346
- kind: ChangeKind;
347
- /**
348
- * Causal chain. Always set. Carries ChangeContext for stage routing.
349
- */
350
- lineage: Lineage;
351
- /**
352
- * Debug-mode only. None in production.
353
- */
354
- audit: ChangeAudit | null;
355
- };
356
- type StageTrace = {
357
- stage: Stage;
358
- duration_us: number;
359
- /**
360
- * Paths (with JSON-encoded values) that this stage processed.
361
- */
362
- matched: Array<[string, string]>;
363
- skipped: Array<SkippedChange>;
364
- /**
365
- * Changes produced by this stage.
366
- */
367
- produced: Array<ProducedChange>;
368
- followup: Array<StageTrace>;
369
- };
370
- type PipelineTrace = {
371
- total_duration_us: number;
372
- stages: Array<StageTrace>;
373
- /**
374
- * Anchor path → present? Gives display layer full context on anchor-dependent resources.
375
- */
376
- anchor_states: Record<string, boolean>;
377
- };
378
- type ValidatorDispatch = {
379
- validator_id: number;
380
- output_path: string;
381
- dependency_values: Record<string, string>;
382
- };
383
- type GraphSnapshot = {
384
- sync_pairs: Array<[string, string]>;
385
- directed_sync_pairs: Array<[string, string]>;
386
- flip_pairs: Array<[string, string]>;
387
- listeners: Array<{
388
- id: number;
389
- topic: string;
390
- scope: string;
391
- }>;
392
- bool_logics: Array<{
393
- id: number;
394
- output_path: string;
395
- definition: {
396
- "IS_EQUAL": [string, unknown];
397
- } | {
398
- "EXISTS": string;
399
- } | {
400
- "IS_EMPTY": string;
401
- } | {
402
- "AND": Array<BoolLogicNode>;
403
- } | {
404
- "OR": Array<BoolLogicNode>;
405
- } | {
406
- "NOT": BoolLogicNode;
407
- } | {
408
- "GT": [string, number];
409
- } | {
410
- "LT": [string, number];
411
- } | {
412
- "GTE": [string, number];
413
- } | {
414
- "LTE": [string, number];
415
- } | {
416
- "IN": [string, Array<unknown>];
417
- } | {
418
- "CONTAINS_ANY": [string, Array<unknown>];
419
- } | {
420
- "CONTAINS_ALL": [string, Array<unknown>];
421
- };
422
- }>;
423
- value_logics: Array<{
424
- id: number;
425
- output_path: string;
426
- }>;
427
- aggregations: Array<{
428
- target: string;
429
- sources: Array<string>;
430
- }>;
431
- computations: Array<{
432
- target: string;
433
- op: string;
434
- sources: Array<string>;
435
- }>;
436
- };
437
- type SideEffectsResult$1 = {
438
- sync_changes: Array<Change$1>;
439
- aggregation_changes: Array<Change$1>;
440
- computation_changes: Array<Change$1>;
441
- registered_listener_ids: Array<number>;
442
- };
443
- type ListenerRegistration$1 = {
444
- subscriber_id: number;
445
- topic_paths: Array<string>;
446
- scope_path: string;
447
- anchor_path?: string;
448
- };
449
- type SideEffectsRegistration = {
450
- registration_id: string;
451
- sync_pairs: Array<[string, string]>;
452
- /**
453
- * One-way sync pairs: source → target only. Changes to target do NOT propagate back to source.
454
- */
455
- directed_sync_pairs: Array<[string, string]>;
456
- flip_pairs: Array<[string, string]>;
457
- aggregation_pairs: Array<[string, string] | [string, string, string]>;
458
- clear_paths: Array<{
459
- triggers: Array<string>;
460
- targets: Array<string>;
461
- }>;
462
- computation_pairs: Array<[string, string, string] | [string, string, string, string]>;
463
- listeners: Array<ListenerRegistration$1>;
464
- anchor_path?: string;
465
- };
466
- type BoolLogicRegistration = {
467
- output_path: string;
468
- tree_json: string;
469
- };
470
- type ValueLogicRegistration = {
471
- output_path: string;
472
- tree_json: string;
473
- };
474
- type ValidatorRegistration = {
475
- validator_id: number;
476
- output_path: string;
477
- dependency_paths: Array<string>;
478
- };
479
- type ConcernsRegistration = {
480
- /**
481
- * Used by JS for unregister tracking; not consumed by Rust pipeline logic.
482
- */
483
- registration_id: string;
484
- bool_logics: Array<BoolLogicRegistration>;
485
- validators: Array<ValidatorRegistration>;
486
- value_logics: Array<ValueLogicRegistration>;
487
- anchor_path?: string;
488
- };
489
- type FullExecutionPlan = {
490
- /**
491
- * Sequential groups of dispatches (flattened from depth levels + batches).
492
- */
493
- groups: Array<{
494
- dispatches: Array<{
495
- dispatch_id: number;
496
- subscriber_id: number;
497
- scope_path: string;
498
- topic_path: string;
499
- /**
500
- * Indexes into the PrepareResult.listener_changes array.
501
- */
502
- input_change_ids: Array<number>;
503
- }>;
504
- }>;
505
- /**
506
- * propagation_map[dispatch_id] = targets to forward produced changes to.
507
- * Flat array indexed by dispatch_id, empty vec = no propagation.
508
- */
509
- propagation_map: Array<Array<{
510
- target_dispatch_id: number;
511
- /**
512
- * Prefix to prepend to child's relative paths for the target's scope.
513
- */
514
- remap_prefix: string;
515
- }>>;
516
- /**
517
- * cascade_map[dispatch_id] = sibling dispatch_ids (same group, later position)
518
- * that should receive this dispatch's produced changes.
519
- */
520
- cascade_map: Array<Array<number>>;
521
- };
522
- type BoolLogicNode = {
523
- "IS_EQUAL": [string, unknown];
524
- } | {
525
- "EXISTS": string;
526
- } | {
527
- "IS_EMPTY": string;
528
- } | {
529
- "AND": Array<BoolLogicNode>;
530
- } | {
531
- "OR": Array<BoolLogicNode>;
532
- } | {
533
- "NOT": BoolLogicNode;
534
- } | {
535
- "GT": [string, number];
536
- } | {
537
- "LT": [string, number];
538
- } | {
539
- "GTE": [string, number];
540
- } | {
541
- "LTE": [string, number];
542
- } | {
543
- "IN": [string, Array<unknown>];
544
- } | {
545
- "CONTAINS_ANY": [string, Array<unknown>];
546
- } | {
547
- "CONTAINS_ALL": [string, Array<unknown>];
548
- };
549
-
550
287
  /**
551
288
  * GenericMeta interface
552
289
  *
@@ -597,6 +334,16 @@ interface GenericMeta {
597
334
  * Used to track changes triggered by computation logic.
598
335
  */
599
336
  isComputationChange?: boolean;
337
+ /**
338
+ * Indicates if the change originated from an expression evaluation (BoolLogic/ValueLogic).
339
+ * Used to track changes produced by the expression stage in the pipeline.
340
+ */
341
+ isExpressionChange?: boolean;
342
+ /**
343
+ * Indicates if the change is a concern output (BoolLogic, ValueLogic, or validator result).
344
+ * Stamped by WASM from the registration_meta provided at expression/listener registration.
345
+ */
346
+ isConcern?: boolean;
600
347
  /**
601
348
  * Identifies the originator of the change.
602
349
  * Can be a user ID, component name, listener function name, or any identifier string.
@@ -639,13 +386,6 @@ interface GenericMeta {
639
386
  type ArrayOfChanges<DATA, META extends GenericMeta = GenericMeta> = {
640
387
  [K in DeepKey<DATA>]: [K, DeepValue<DATA, K>, META] | [K, DeepValue<DATA, K>];
641
388
  }[DeepKey<DATA>][];
642
- /** A single state change in internal pipeline form. */
643
- interface Change {
644
- path: string;
645
- value: unknown;
646
- /** Meta object threaded through the pipeline. Lineage is merged in by the bridge on WASM→JS conversion. */
647
- meta: GenericMeta;
648
- }
649
389
 
650
390
  /**
651
391
  * Concern type utilities
@@ -1190,25 +930,105 @@ type ValidatedListeners<DATA extends object = object, META extends GenericMeta =
1190
930
  [STORE_DATA]: DATA;
1191
931
  };
1192
932
 
1193
- interface BaseConcernProps<STATE, PATH extends string> {
1194
- state: STATE;
1195
- path: PATH;
1196
- value: unknown;
1197
- }
1198
- interface ConcernType<NAME extends string = string, EXTRA_PROPS = Record<string, unknown>, RETURN_TYPE = unknown> {
1199
- name: NAME;
1200
- description: string;
1201
- /** Evaluated inside effect() - all state accesses are tracked */
1202
- evaluate: (props: BaseConcernProps<Record<string, unknown>, string> & EXTRA_PROPS) => RETURN_TYPE;
1203
- }
1204
- interface ConcernRegistration {
1205
- id: string;
1206
- path: string;
1207
- concernName: string;
1208
- concern: ConcernType;
1209
- config: Record<string, unknown>;
1210
- dispose: () => void;
1211
- }
933
+ type Stage = "input" | "aggregation_write" | "diff" | "sync" | "flip" | "clear_path" | "aggregation_read" | "computation" | "expression" | "listeners" | "apply";
934
+ type ChangeKind = "real" | "redundant" | "breakdown" | "consumed";
935
+ type ChangeContext = "Initial" | "Mutation";
936
+ type Lineage = "Input" | {
937
+ "Derived": {
938
+ parent_id: number;
939
+ via: Stage;
940
+ context: ChangeContext;
941
+ };
942
+ };
943
+ type ChangeAudit = {
944
+ source_path?: string;
945
+ logic_id?: number;
946
+ };
947
+ type TraceEntry = {
948
+ /**
949
+ * Monotonic counter. Parents numbered pipeline-globally, children within parent.
950
+ */
951
+ seq: number;
952
+ /**
953
+ * Stage or operation name (e.g. "input", "sync", "sync_pair", "listener", "input:skip").
954
+ */
955
+ stage: string;
956
+ /**
957
+ * Actual input values as JSON. Per-child: only what that operation matched.
958
+ */
959
+ inputs: Array<unknown>;
960
+ /**
961
+ * Actual output values as JSON. What this operation produced.
962
+ */
963
+ outputs: Array<unknown>;
964
+ /**
965
+ * Microseconds. Parent = total stage time. Child = per-operation time. 0 when timing unavailable.
966
+ */
967
+ duration_us: number;
968
+ /**
969
+ * Extra metadata (registration_id, reason, etc.). Omitted when empty.
970
+ */
971
+ meta: Record<string, unknown>;
972
+ /**
973
+ * Per-operation breakdown. Omitted when empty. Recursive (same struct).
974
+ */
975
+ children: Array<TraceEntry>;
976
+ };
977
+ type PipelineTrace = {
978
+ entries: Array<TraceEntry>;
979
+ };
980
+ type ListenerError = {
981
+ subscriber_id: number;
982
+ name: string;
983
+ error: string;
984
+ };
985
+ type ClearPathInput = {
986
+ triggers: Array<string>;
987
+ targets: Array<string>;
988
+ };
989
+ type SideEffectsRegistration = {
990
+ registration_id: string;
991
+ sync_pairs: Array<[string, string]>;
992
+ directed_sync_pairs: Array<[string, string]>;
993
+ flip_pairs: Array<[string, string]>;
994
+ aggregation_pairs: Array<unknown>;
995
+ clear_paths: Array<ClearPathInput>;
996
+ computation_pairs: Array<unknown>;
997
+ listeners: Array<ListenerRegistration$1>;
998
+ anchor_path: string | null;
999
+ };
1000
+ type ListenerRegistration$1 = {
1001
+ subscriber_id: number;
1002
+ topic_paths: Array<string>;
1003
+ scope_path: string;
1004
+ name?: string;
1005
+ /**
1006
+ * Whether to dispatch this subscriber immediately on registration with current shadow state.
1007
+ * Used by validators to produce initial validation results without requiring a state change.
1008
+ */
1009
+ run_on_registration: boolean | null;
1010
+ };
1011
+ type ConcernsRegistration = {
1012
+ /**
1013
+ * Expression registrations (logic, value, options — WASM auto-detects export type from tree_json).
1014
+ */
1015
+ expressions: Array<ExpressionRegistration>;
1016
+ /**
1017
+ * Subscriber registrations (validators, custom concerns, etc.).
1018
+ * Same format as SideEffectsRegistration.listeners — WASM treats them
1019
+ * identically. TS controls wave_priority at registration time.
1020
+ */
1021
+ listeners: Array<ListenerRegistration$1>;
1022
+ };
1023
+ type ExpressionRegistration = {
1024
+ output_path: string;
1025
+ tree_json: string;
1026
+ /**
1027
+ * Opaque meta stamped on every change produced by this expression.
1028
+ * TS uses this for concern tagging (e.g. `{ isConcern: true }`).
1029
+ */
1030
+ meta: unknown | null;
1031
+ };
1212
1032
 
1213
1033
  /**
1214
1034
  * WASM Bridge — Thin namespace over Rust/WASM exports.
@@ -1224,12 +1044,21 @@ interface ConcernRegistration {
1224
1044
  * @module wasm/bridge
1225
1045
  */
1226
1046
 
1227
- /** Map a Wasm wire result type to JS-facing type: Wasm.Change[] → Change[], rest untouched. */
1228
- type WireToJs<T> = {
1229
- [K in keyof T]: T[K] extends Change$1[] ? Change[] : T[K];
1230
- };
1231
- /** JS-facing side effects result same shape as Wasm wire type but with parsed Change values. */
1232
- type SideEffectsResult = WireToJs<SideEffectsResult$1>;
1047
+ /**
1048
+ * JS-facing change — derived from Wasm.Change with `value_json` replaced by
1049
+ * parsed `value: unknown` and `meta` typed as GenericMeta.
1050
+ *
1051
+ * `kind`, `lineage`, and `audit` are optional: they are populated on changes
1052
+ * returned from WASM (output) but absent on changes constructed in JS (input).
1053
+ */
1054
+ interface Change {
1055
+ path: string;
1056
+ value: unknown;
1057
+ meta: GenericMeta;
1058
+ kind?: ChangeKind;
1059
+ lineage?: Lineage;
1060
+ audit?: ChangeAudit;
1061
+ }
1233
1062
  /**
1234
1063
  * Create a new isolated WASM pipeline instance.
1235
1064
  * Each store should call this once and store the result.
@@ -1243,36 +1072,33 @@ declare const createWasmPipeline: (options?: {
1243
1072
  shadowInit: (state: object) => void;
1244
1073
  shadowDump: () => unknown;
1245
1074
  processChanges: (changes: Change[]) => {
1246
- listener_changes: Change[];
1247
- validators_to_run: ValidatorDispatch[];
1248
- execution_plan: FullExecutionPlan | null;
1249
- has_work: boolean;
1075
+ changes: Change[];
1076
+ listener_errors: ListenerError[];
1077
+ trace: PipelineTrace | undefined;
1250
1078
  };
1251
- pipelineFinalize: (changes: Change[]) => {
1252
- state_changes: Change[];
1253
- trace: PipelineTrace | null;
1079
+ registerSideEffects: (reg: Partial<SideEffectsRegistration>, subscriberFns?: Map<number, (...args: unknown[]) => unknown>) => {
1080
+ sync_changes: Change[];
1081
+ aggregation_changes: Change[];
1082
+ computation_changes: Change[];
1083
+ registered_side_effects: Record<string, ListenerRegistration$1[]>;
1254
1084
  };
1255
- registerSideEffects: (reg: Partial<SideEffectsRegistration>) => {
1085
+ unregisterSideEffects: (registrationId: string) => {
1256
1086
  sync_changes: Change[];
1257
1087
  aggregation_changes: Change[];
1258
1088
  computation_changes: Change[];
1259
- registered_listener_ids: number[];
1089
+ registered_side_effects: Record<string, ListenerRegistration$1[]>;
1260
1090
  };
1261
- unregisterSideEffects: (registrationId: string) => void;
1262
- registerConcerns: (reg: Partial<ConcernsRegistration>) => {
1263
- bool_logic_changes: Change[];
1264
- registered_logic_ids: number[];
1265
- registered_validator_ids: number[];
1266
- value_logic_changes: Change[];
1267
- registered_value_logic_ids: number[];
1091
+ registerConcerns: (reg: Partial<ConcernsRegistration>, subscriberFns?: Map<number, (...args: unknown[]) => unknown>) => {
1092
+ expression_changes: Change[];
1093
+ registered_expression_ids: number[];
1094
+ registered_subscriber_ids: number[];
1095
+ initial_listener_changes: Change[];
1268
1096
  };
1269
1097
  unregisterConcerns: (registrationId: string) => void;
1270
1098
  registerBoolLogic: (outputPath: string, tree: unknown) => number;
1271
1099
  unregisterBoolLogic: (logicId: number) => void;
1272
1100
  pipelineReset: () => void;
1273
1101
  destroy: () => void;
1274
- getGraphSnapshot: () => GraphSnapshot;
1275
- validatorSchemas: Map<number, ValidationSchema<unknown>>;
1276
1102
  };
1277
1103
  type WasmPipeline = ReturnType<typeof createWasmPipeline>;
1278
1104
 
@@ -1280,57 +1106,47 @@ type WasmPipeline = ReturnType<typeof createWasmPipeline>;
1280
1106
  * Apex State Logger — Simplified debug logging with colored console output.
1281
1107
  *
1282
1108
  * Two log functions:
1283
- * 1. logPipeline — called once per processChanges with unified trace
1109
+ * 1. logPipeline — called once per processChanges with WASM pipeline trace
1284
1110
  * 2. logRegistration — called once per register/unregister with graph snapshot
1285
1111
  *
1286
1112
  * Zero runtime cost when log flag is false (returns no-op logger).
1113
+ *
1114
+ * Responsible for:
1115
+ * - Rendering PipelineTrace entries recursively with [NN]/[NN.MM] numbering
1116
+ * - Color-coding entries by registration_id (deterministic palette) or stage
1117
+ * - Formatting timing as ms from duration_us
1118
+ *
1119
+ * Out of scope:
1120
+ * - Trace collection (handled by Rust pipeline_trace.rs)
1121
+ * - DevTools integration (handled by store/devtools.ts)
1287
1122
  */
1288
1123
 
1289
- /** Per-listener dispatch trace (JS-measured). */
1290
- interface ListenerDispatchTrace {
1291
- dispatchId: number;
1292
- subscriberId: number;
1293
- fnName: string;
1294
- scope: string;
1295
- topic: string;
1296
- registrationId: string;
1297
- input: [string, unknown, unknown][];
1298
- output: Change[];
1299
- currentState: unknown;
1300
- durationMs: number;
1301
- slow: boolean;
1302
- }
1303
- /** Universal trace — single object for console, devtools, any observability tool. */
1304
- interface UnifiedPipelineTrace {
1305
- wasm: PipelineTrace;
1306
- listeners: ListenerDispatchTrace[];
1307
- totalDurationMs: number;
1308
- wasmDurationMs: number;
1309
- listenerDurationMs: number;
1310
- /** True when listener timing was actually measured (debug.timing: true). False → show N/A. */
1311
- listenerTimingEnabled: boolean;
1312
- }
1124
+ type GraphSnapshot$1 = Record<string, unknown>;
1125
+ type SideEffectsResult = ReturnType<WasmPipeline['registerSideEffects']>;
1313
1126
  interface PipelineLogData {
1314
1127
  initialChanges: Change[];
1315
- trace: UnifiedPipelineTrace | null;
1316
- /** Pre-finalize changes: listener output + validator output before WASM merge/dedup. */
1317
- preFinalizableChanges?: Change[];
1318
- /** Final applied changes: after WASM finalization, partitioned to state + concerns. */
1319
- appliedChanges?: Change[];
1320
- /** Frozen valtio snapshot of state after all changes applied. */
1321
- stateSnapshot?: unknown;
1128
+ /** WASM pipeline trace (undefined if not traced). */
1129
+ trace: PipelineTrace | undefined;
1130
+ /** Final applied changes: always present (can be empty). */
1131
+ appliedChanges: Change[];
1132
+ /** State snapshot after changes (undefined if not captured). */
1133
+ stateSnapshot: unknown | undefined;
1322
1134
  }
1323
1135
  interface RegistrationLogData {
1324
- result?: SideEffectsResult;
1325
- appliedChanges?: Change[];
1326
- stateSnapshot?: unknown;
1327
- durationMs?: number;
1328
- /** subscriber_id → function name, for enriching listener entries in the log. */
1329
- listenerNames?: Map<number, string>;
1136
+ result: SideEffectsResult | undefined;
1137
+ appliedChanges: Change[];
1138
+ stateSnapshot: unknown | undefined;
1139
+ durationMs: number;
1140
+ }
1141
+ interface RegistrationLogParams {
1142
+ type: 'register' | 'unregister';
1143
+ id: string;
1144
+ snapshot: GraphSnapshot$1;
1145
+ data?: RegistrationLogData;
1330
1146
  }
1331
1147
  interface ApexLogger {
1332
1148
  logPipeline: (data: PipelineLogData) => void;
1333
- logRegistration: (type: 'register' | 'unregister', id: string, snapshot: GraphSnapshot, data?: RegistrationLogData) => void;
1149
+ logRegistration: (params: RegistrationLogParams) => void;
1334
1150
  destroy: () => void;
1335
1151
  }
1336
1152
 
@@ -1345,6 +1161,7 @@ interface ApexLogger {
1345
1161
  * Singleton connections per proxy — survives StrictMode remounts and HMR reloads.
1346
1162
  */
1347
1163
 
1164
+ type GraphSnapshot = Record<string, unknown>;
1348
1165
  interface DevToolsNotifier {
1349
1166
  notifyPipeline: (data: PipelineLogData) => void;
1350
1167
  notifyRegistration: (type: 'register' | 'unregister', id: string, snapshot: GraphSnapshot) => void;
@@ -1518,18 +1335,9 @@ interface MultiPathListener<DATA extends object = object, META extends GenericMe
1518
1335
  }
1519
1336
  /** Listener registration — single-path or multi-path. */
1520
1337
  type ListenerRegistration<DATA extends object = object, META extends GenericMeta = GenericMeta, Depth extends number = DefaultDepth> = SinglePathListener<DATA, META, Depth> | MultiPathListener<DATA, META, Depth>;
1521
- interface ListenerHandlerRef {
1522
- scope: string | null;
1523
- fn: (...args: any[]) => unknown;
1524
- name: string;
1525
- registrationId: string;
1526
- }
1527
1338
  interface Registrations {
1528
- concerns: Map<string, ConcernType[]>;
1529
- effectCleanups: Set<() => void>;
1530
- sideEffectCleanups: Map<string, () => void>;
1531
- /** O(1) lookup: subscriber_id -> handler ref. Populated by registerListener. */
1532
- listenerHandlers: Map<number, ListenerHandlerRef>;
1339
+ /** Cleanup functions keyed by registration ID. Multiple cleanups may exist per registration. */
1340
+ cleanups: Record<string, (() => void)[]>;
1533
1341
  }
1534
1342
  /** Internal store state (NOT tracked - wrapped in ref()) */
1535
1343
  interface InternalState {
@@ -1551,6 +1359,26 @@ interface StoreInstance<DATA extends object> {
1551
1359
  _debug: DebugTrack | null;
1552
1360
  }
1553
1361
 
1362
+ interface BaseConcernProps<STATE, PATH extends string> {
1363
+ state: STATE;
1364
+ path: PATH;
1365
+ value: unknown;
1366
+ }
1367
+ interface ConcernType<NAME extends string = string, EXTRA_PROPS = Record<string, unknown>, RETURN_TYPE = unknown> {
1368
+ name: NAME;
1369
+ description: string;
1370
+ /** Evaluated inside effect() - all state accesses are tracked */
1371
+ evaluate: (props: BaseConcernProps<Record<string, unknown>, string> & EXTRA_PROPS) => RETURN_TYPE;
1372
+ }
1373
+ interface ConcernRegistration {
1374
+ id: string;
1375
+ path: string;
1376
+ concernName: string;
1377
+ concern: ConcernType;
1378
+ config: Record<string, unknown>;
1379
+ dispose: () => void;
1380
+ }
1381
+
1554
1382
  interface ValidationError {
1555
1383
  field?: string;
1556
1384
  message: string;