wenay-react2 1.0.34 → 1.0.35

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.
@@ -1,5 +1,6 @@
1
1
  import { ObserveAll2 } from "wenay-common2";
2
2
  type StoreChange = ObserveAll2.StoreChange;
3
+ type StoreEachCtx = ObserveAll2.StoreEachCtx;
3
4
  type StoreSubOpts = ObserveAll2.StoreSubOpts;
4
5
  type StoreSyncOpts = ObserveAll2.StoreSyncOpts;
5
6
  type StoreDrain = ObserveAll2.StoreDrain;
@@ -55,6 +56,20 @@ export type StoreKeysController<T extends object = any> = {
55
56
  };
56
57
  export declare function useStoreKeys<T extends object>(node: StoreNode<T>, options?: Omit<UseStoreNodeOptions<T>, "mode">): StoreKeysController<T>;
57
58
  export declare function useStoreSelect<T, M extends StoreMask<T>>(selection: StoreSelection<T, M>, options?: UseStoreSelectOptions<StorePick<T, M>>): StoreSelectionController<T, M>;
59
+ export type UseStoreEachOptions = {
60
+ /** false = do not subscribe (and drop the current subscription). Default true. */
61
+ enabled?: boolean;
62
+ };
63
+ /**
64
+ * Per-changed-top-level-key feed of a store (`store.each()` of wenay-common2): cb fires once per
65
+ * CHANGED key per drain window with the current `store.state[key]` (`undefined` = key deleted);
66
+ * a root replace (store.replace / mirror keyframe) EXPANDS into one call per key, so cold start
67
+ * is not a special case for per-key consumers (grid rows, canvas layers, ...). The fold target
68
+ * should live outside React state (ref/store/grid api) — this hook renders nothing by itself.
69
+ * cb goes through a ref — a new identity does not resubscribe.
70
+ * The remote (wire) counterpart is useStoreReplayEach in useReplay.ts.
71
+ */
72
+ export declare function useStoreEach<T extends object>(store: ObserveAll2.Store<T> | null | undefined, cb: (key: string, value: T[keyof T] | undefined, ctx: StoreEachCtx) => void, options?: UseStoreEachOptions): void;
58
73
  export type RemoteStoreLike<T extends object> = {
59
74
  get(mask?: any): T | Promise<T>;
60
75
  changed: ListenLike<readonly []>;
@@ -102,4 +117,4 @@ export declare function useListenValue<T, TArgs extends readonly unknown[] = rea
102
117
  current?: boolean | (() => TArgs | undefined);
103
118
  map?: (...args: TArgs) => T;
104
119
  }): T | undefined;
105
- export type { StoreChange, StoreDrain, StoreMask, StoreNode, StorePick, StoreSelection, StoreSubOpts, StoreSyncOpts };
120
+ export type { StoreChange, StoreDrain, StoreEachCtx, StoreMask, StoreNode, StorePick, StoreSelection, StoreSubOpts, StoreSyncOpts };
@@ -96,6 +96,25 @@ export function useStoreSelect(selection, options = {}) {
96
96
  get: () => selection.get(),
97
97
  }), [selection, value, refresh]);
98
98
  }
99
+ /**
100
+ * Per-changed-top-level-key feed of a store (`store.each()` of wenay-common2): cb fires once per
101
+ * CHANGED key per drain window with the current `store.state[key]` (`undefined` = key deleted);
102
+ * a root replace (store.replace / mirror keyframe) EXPANDS into one call per key, so cold start
103
+ * is not a special case for per-key consumers (grid rows, canvas layers, ...). The fold target
104
+ * should live outside React state (ref/store/grid api) — this hook renders nothing by itself.
105
+ * cb goes through a ref — a new identity does not resubscribe.
106
+ * The remote (wire) counterpart is useStoreReplayEach in useReplay.ts.
107
+ */
108
+ export function useStoreEach(store, cb, options = {}) {
109
+ const { enabled = true } = options;
110
+ const cbRef = useLatestRef(cb);
111
+ useEffect(() => {
112
+ if (!store || !enabled)
113
+ return;
114
+ // each() is created lazily per subscription: it runs while it has subscribers and closes on off()
115
+ return store.each().on((key, value, ctx) => cbRef.current(key, value, ctx));
116
+ }, [store, enabled, cbRef]);
117
+ }
99
118
  export function useStoreMirror(remote, initial, options) {
100
119
  const { mask, auto = true, current = true, drain, key, partial, onError } = options;
101
120
  const stableMask = useStableStoreMask(mask);
@@ -1,4 +1,6 @@
1
1
  import { ObserveAll2, Replay } from "wenay-common2";
2
+ type StoreDrain = ObserveAll2.StoreDrain;
3
+ type StoreEachCtx = ObserveAll2.StoreEachCtx;
2
4
  type StorePatch = ObserveAll2.StorePatch;
3
5
  type ReplayEvent<Z extends any[]> = Replay.ReplayEvent<Z>;
4
6
  type ReplayRemote<Z extends any[]> = Replay.ReplayRemote<Z>;
@@ -78,6 +80,31 @@ export type StoreReplayMirrorController<T extends object> = StoreReplaySyncContr
78
80
  * so reconnect is a journal tail, not a keyframe. A new `remote` recreates the store.
79
81
  */
80
82
  export declare function useStoreReplayMirror<T extends object>(remote: ReplayRemote<[StorePatch]> | null | undefined, initial: T, options?: UseStoreReplaySyncOptions): StoreReplayMirrorController<T>;
83
+ export type UseStoreReplayEachOptions<T extends object> = UseStoreReplaySyncOptions & {
84
+ /**
85
+ * Seed of the internal mirror store. Creation-time only (a later identity change does nothing).
86
+ * Reconnect after a FULL unmount: pass the saved snapshot together with `since`
87
+ * ({since: prev.seq(), initial: prev.store.snapshot()}) — the tail lands ON TOP of the previous
88
+ * state; a fresh empty mirror would not converge.
89
+ */
90
+ initial?: T;
91
+ /** Drain of the internal mirror store — the coalescing window of the per-key feed. Creation-time only. */
92
+ drain?: StoreDrain;
93
+ };
94
+ /**
95
+ * Per-key fold over a store replay line — React counterpart of `ObserveAll2.syncStoreReplayEach`
96
+ * (internal mirror store + syncStoreReplay + store.each()). cb fires once per CHANGED top-level
97
+ * key per drain window with the current value; the first delivery is the keyframe EXPANDED per
98
+ * key; (key, undefined) = key deleted — cold start / reconnect are not special cases for per-key
99
+ * consumers (grid rows, canvas layers, ...). The fold target should live outside React state.
100
+ *
101
+ * Unlike the library one-call (a fresh store per call), the mirror here lives in a ref: within
102
+ * one mounted component every resubscribe (StrictMode double-effect, restart(), enabled toggling,
103
+ * staleMs/policy change) reconnects by tail ON TOP of the kept state — no snapshot/initial dance.
104
+ * A new `remote` identity recreates the store (fresh keyframe). Direct reads / extra
105
+ * subscriptions: controller.store (useStoreNode/useStoreKeys work on it as usual).
106
+ */
107
+ export declare function useStoreReplayEach<T extends object>(remote: ReplayRemote<[StorePatch]> | null | undefined, cb: (key: string, value: T[keyof T] | undefined, ctx: StoreEachCtx) => void, options?: UseStoreReplayEachOptions<T>): StoreReplayMirrorController<T>;
81
108
  export type UseReplayFrameOptions = {
82
109
  /** Pull period, ms. Default 300. */
83
110
  intervalMs?: number;
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { ObserveAll2, Replay } from "wenay-common2";
3
+ import { useStoreEach } from "./useObserveStore";
3
4
  /**
4
5
  * React bridge over the Replay stack (snapshot + sequenced delta line) of wenay-common2.
5
6
  *
@@ -201,6 +202,32 @@ export function useStoreReplayMirror(remote, initial, options = {}) {
201
202
  const sync = useStoreReplaySync(store, remote, options);
202
203
  return useMemo(() => ({ ...sync, store }), [sync, store]);
203
204
  }
205
+ /**
206
+ * Per-key fold over a store replay line — React counterpart of `ObserveAll2.syncStoreReplayEach`
207
+ * (internal mirror store + syncStoreReplay + store.each()). cb fires once per CHANGED top-level
208
+ * key per drain window with the current value; the first delivery is the keyframe EXPANDED per
209
+ * key; (key, undefined) = key deleted — cold start / reconnect are not special cases for per-key
210
+ * consumers (grid rows, canvas layers, ...). The fold target should live outside React state.
211
+ *
212
+ * Unlike the library one-call (a fresh store per call), the mirror here lives in a ref: within
213
+ * one mounted component every resubscribe (StrictMode double-effect, restart(), enabled toggling,
214
+ * staleMs/policy change) reconnects by tail ON TOP of the kept state — no snapshot/initial dance.
215
+ * A new `remote` identity recreates the store (fresh keyframe). Direct reads / extra
216
+ * subscriptions: controller.store (useStoreNode/useStoreKeys work on it as usual).
217
+ */
218
+ export function useStoreReplayEach(remote, cb, options = {}) {
219
+ const { initial, drain, ...syncOptions } = options;
220
+ const storeRef = useRef(null);
221
+ if (!storeRef.current || storeRef.current.remote !== remote) {
222
+ storeRef.current = { remote, store: ObserveAll2.createStore((initial ?? {}), drain !== undefined ? { drain } : undefined) };
223
+ }
224
+ const store = storeRef.current.store;
225
+ // each BEFORE sync: effects run in hook-call order, so the per-key subscriber already exists
226
+ // when the keyframe applies to the store (the expansion is not missed)
227
+ useStoreEach(store, cb, { enabled: syncOptions.enabled });
228
+ const sync = useStoreReplaySync(store, remote, syncOptions);
229
+ return useMemo(() => ({ ...sync, store }), [sync, store]);
230
+ }
204
231
  /**
205
232
  * Pull a replay line at YOUR pace (the frame model of common2): a timer around
206
233
  * `remote.frame(seq, hint)` — the server condenses the tail via the line's `frame` lambda
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-react2",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Common react",
5
5
  "strict": true,
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "react": "^19.2.0",
24
24
  "react-dom": "^19.2.0",
25
25
  "react-rnd": "^10.5.3",
26
- "wenay-common2": "^1.0.58"
26
+ "wenay-common2": "^1.0.61"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.2.0",