opshot 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -2,12 +2,7 @@
2
2
 
3
3
  # opshot
4
4
 
5
- Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation.
6
-
7
- - **Self-contained**: a state carries its data, its methods, and its own subscription; pass it around like any object.
8
- - **Reactive reads**: components read plain properties, and reads are tracked per component: a component re-renders only when a property it read changes.
9
- - **Safe mutation**: write by mutating the actual object directly inside `mutate`.
10
- - **Ops events**: every mutation emits its changes as `{ do, undo }` JSON Patch pairs, ready for history, sync, and persistence.
5
+ Mutable state for React, with re-render for only the components that read what changed. (It's [valtio](https://github.com/pmndrs/valtio), but not a footgun.)
11
6
 
12
7
  ## Install
13
8
 
@@ -15,29 +10,81 @@ Plain-object state for React: mutate it directly, re-render only the components
15
10
  npm install opshot
16
11
  ```
17
12
 
18
- ## Quick start
13
+ ## Mutable state
14
+
15
+ React state is immutable: changing one field means spreading the old object into a new one.
16
+
17
+ ```tsx
18
+ const [user, setUser] = useState({ name: "Ada", age: 36 });
19
+
20
+ setUser((prev) => ({ ...prev, age: 37 }));
21
+ ```
22
+
23
+ opshot state is mutable: you assign the field.
24
+
25
+ ```tsx
26
+ const user = useTrackedState({ name: "Ada", age: 36 });
27
+
28
+ user.mutate((mutable) => (mutable.age = 37));
29
+ ```
30
+
31
+ ## Bounded re-renders
32
+
33
+ React re-renders a component and its children when its state changes.
19
34
 
20
35
  ```tsx
21
- import { useCreateState } from "opshot/react";
36
+ interface User {
37
+ name: string;
38
+ age: number;
39
+ }
22
40
 
23
- const Counter = () => {
24
- const counter = useCreateState({ count: 0 });
41
+ const Parent = () => {
42
+ const [user, setUser] = useState<User>({ name: "Ada", age: 36 });
25
43
 
26
- // Mutate the object directly: assignments, push, delete all work.
27
- const increment = () => counter.op.mutate((proxy) => proxy.count++);
44
+ const birthday = () => setUser((prev) => ({ ...prev, age: prev.age + 1 }));
28
45
 
29
- return <button onClick={increment}>{counter.count}</button>;
46
+ // A click re-renders Parent and Child.
47
+ return (
48
+ <>
49
+ <button onClick={birthday}>+</button>
50
+ <Child user={user} />
51
+ </>
52
+ );
30
53
  };
54
+
55
+ const Child = ({ user }: { user: User }) => <p>{user.age}</p>;
31
56
  ```
32
57
 
33
- ## Creating state
58
+ opshot re-renders only what read the change. Wrap a child in `retrack` and it subscribes to the fields it reads. **Where the mutation happens doesn't matter** — here Parent writes, and only Child re-renders, because renders follow reads, not writes.
59
+
60
+ ```tsx
61
+ const Parent = () => {
62
+ const user = useTrackedState<User>({ name: "Ada", age: 36 });
63
+
64
+ const birthday = () => user.mutate((mutable) => mutable.age++);
65
+
66
+ // A click re-renders only Child.
67
+ return (
68
+ <>
69
+ <button onClick={birthday}>+</button>
70
+ <Child user={user} />
71
+ </>
72
+ );
73
+ };
74
+
75
+ const Child = retrack<{ user: State<User> }>(({ user }) => <p>{user.age}</p>);
76
+ ```
77
+
78
+ This is how you optimize re-rendering across your component tree: place `retrack` boundaries where you want re-renders contained, and each boundary re-renders only when a field it read changes. `useTrackedState` is a boundary itself.
79
+
80
+ ## Creating State
34
81
 
35
82
  ```tsx
36
83
  import { ref } from "opshot";
37
- import { useCreateState } from "opshot/react";
84
+ import { useTrackedState } from "opshot/react";
38
85
 
39
86
  const Player = () => {
40
- const player = useCreateState((mutate, get) => ({
87
+ const player = useTrackedState((mutate, get) => ({
41
88
  position: 0,
42
89
 
43
90
  // ref() keeps a value out of reactivity and ops.
@@ -47,7 +94,7 @@ const Player = () => {
47
94
  seek: (position: number) => {
48
95
  get().element.currentTime = position;
49
96
 
50
- mutate((proxy) => (proxy.position = position));
97
+ mutate((mutable) => (mutable.position = position));
51
98
  },
52
99
  }));
53
100
 
@@ -55,16 +102,16 @@ const Player = () => {
55
102
  };
56
103
  ```
57
104
 
58
- ## state.op
105
+ ## Tracked State
59
106
 
60
- Everything opshot attaches lives under one reserved key, `op`.
107
+ Everything opshot attaches lives under two reserved keys, `mutate` and `op`.
61
108
 
62
109
  ```ts
63
110
  // The write path. An optional second argument is passed to every subscriber.
64
- counter.op.mutate((proxy) => {}, { transactionKey: "drag" });
111
+ counter.mutate((mutable) => mutable.count++, { transactionKey: "drag" });
65
112
 
66
113
  // Hears every op this state emits; returns an unsubscribe.
67
- const unsubscribe = counter.op.subscribe((state, ops, options) => {
114
+ const unsubscribe = counter.op.subscribe((state, ops, meta) => {
68
115
  // ...
69
116
  });
70
117
 
@@ -78,13 +125,13 @@ counter.op.isMutating;
78
125
  counter.op.unwrap();
79
126
 
80
127
  // The underlying valtio proxy, typed object: an escape hatch.
81
- counter.op.proxy;
128
+ counter.op.unsafeMutable;
82
129
  ```
83
130
 
84
131
  ## Ops
85
132
 
86
133
  ```ts
87
- const unsubscribe = counter.op.subscribe((state, ops, options) => {
134
+ const unsubscribe = counter.op.subscribe((state, ops, meta) => {
88
135
  // state: the snapshot these ops produced
89
136
  // ops: [{
90
137
  // do: { op: "replace", path: "/count", value: 1 },
@@ -99,75 +146,71 @@ A subscriber must not write to the state it subscribes to; writing to a differen
99
146
 
100
147
  Ops cost nothing until someone listens: a state with no subscribers, on itself or its group, skips computing them entirely.
101
148
 
102
- ## Groups
149
+ ## Meta
103
150
 
104
- A group creates states and hears every op from the states it created: one stream for history, sync, and persistence.
151
+ `mutate`'s optional second argument is delivered to every subscriber alongside the ops.
152
+
153
+ To type it, declare a meta token once and pass it in.
105
154
 
106
155
  ```tsx
107
156
  import { useEffect } from "react";
108
- import { useCreateGroup, useCreateState } from "opshot/react";
157
+ import { createMeta } from "opshot";
158
+ import { useTrackedState } from "opshot/react";
159
+
160
+ interface DocumentMeta {
161
+ replay?: boolean;
162
+ }
163
+
164
+ // Declared once, at module scope.
165
+ const documentMeta = createMeta<DocumentMeta>();
109
166
 
110
167
  const Editor = () => {
111
- // A lifetime-stable group.
112
- const group = useCreateGroup();
168
+ const doc = useTrackedState({ title: "Untitled" }, documentMeta);
113
169
 
114
- // Created through the group, so its ops reach the group's subscribers.
115
- const doc = useCreateState({ items: new Array<string>() }, group);
170
+ // A history replaying an undone op marks the write, so recorders can tell it apart.
171
+ // The meta argument is typed DocumentMeta.
172
+ const undo = () => doc.mutate((mutable) => (mutable.title = "Untitled"), { replay: true });
116
173
 
117
174
  useEffect(
118
175
  () =>
119
- // Fires for doc and every other state the group created.
120
- group.subscribe((state, ops, options) => {
176
+ // The subscriber's meta parameter is typed DocumentMeta.
177
+ doc.op.subscribe((state, ops, meta) => {
178
+ // A recorder skips its own replays.
179
+ if (meta.replay) return;
180
+
121
181
  // ...
122
182
  }),
123
- [group],
183
+ [doc.op],
124
184
  );
125
185
 
126
186
  // ...
127
187
  };
128
188
  ```
129
189
 
130
- ## resnapshot
131
-
132
- A component re-renders when a property it read changes. Reads belong to the nearest subscribed component above them: `useCreateState` subscribes the component that created the state, and `resnapshot` subscribes the component it wraps.
190
+ ## Groups
133
191
 
134
- This is a lever for bounding re-renders. Here `CounterButton` is plain, so its `count` read belongs to `App`, and every click re-renders `App` and everything under it:
192
+ A group creates states and hears every op from the states it created: one stream for history, sync, and persistence.
135
193
 
136
194
  ```tsx
137
- import type { State } from "opshot";
138
- import { useCreateState } from "opshot/react";
195
+ import { useEffect } from "react";
196
+ import { useGroup, useTrackedState } from "opshot/react";
139
197
 
140
- interface Counter {
141
- title: string;
142
- count: number;
143
- }
198
+ const Editor = () => {
199
+ // A lifetime-stable group.
200
+ const group = useGroup();
144
201
 
145
- // Every click re-renders App and its whole subtree.
146
- const App = () => {
147
- const counter = useCreateState<Counter>({ title: "Hits", count: 0 });
202
+ // Created through the group, so its ops reach the group's subscribers.
203
+ const doc = useTrackedState({ items: new Array<string>() }, group);
148
204
 
149
- return (
150
- <>
151
- <h1>{counter.title}</h1>
152
- <CounterButton counter={counter} />
153
- </>
205
+ useEffect(
206
+ () =>
207
+ // Fires for doc and every other state the group created.
208
+ group.subscribe((state, ops, meta) => {
209
+ // ...
210
+ }),
211
+ [group],
154
212
  );
155
- };
156
213
 
157
- const CounterButton = ({ counter }: { counter: State<Counter> }) => (
158
- <button onClick={() => counter.op.mutate((proxy) => proxy.count++)}>{counter.count}</button>
159
- );
160
- ```
161
-
162
- Wrapping `CounterButton` in `resnapshot` subscribes it, so its `count` read becomes its own. A click now re-renders `CounterButton` alone:
163
-
164
- ```tsx
165
- import { resnapshot } from "opshot/react";
166
-
167
- // A click re-renders only CounterButton. A title change would still re-render App.
168
- const CounterButton = resnapshot<{ counter: State<Counter> }>(({ counter }) => (
169
- <button onClick={() => counter.op.mutate((proxy) => proxy.count++)}>{counter.count}</button>
170
- ));
214
+ // ...
215
+ };
171
216
  ```
172
-
173
- Subscribed components re-render independently: had `App` also read `count`, both would re-render.
package/dist/index.d.ts CHANGED
@@ -19,29 +19,38 @@ interface Op {
19
19
  }
20
20
  declare function diffSnapshots(before: unknown, after: unknown): Array<Op>;
21
21
 
22
- type MutateOptions = Record<string, unknown>;
23
- type Mutate<T extends object> = (callback: (proxy: T) => void, options?: MutateOptions) => void;
24
- type StateListener<T extends object> = (state: State<T>, ops: Array<Op>, options: MutateOptions) => void;
25
- interface OpshotHandle<T extends object> {
26
- readonly proxy: object;
22
+ type MetaRecord = Record<string, unknown>;
23
+ declare const metaIn: unique symbol;
24
+ interface Meta<In extends object = MetaRecord, Out extends object = MetaRecord> {
25
+ readonly defaults?: Out;
26
+ readonly [metaIn]?: (value: In) => void;
27
+ }
28
+ type Mutate<T extends object, In extends object = MetaRecord> = (callback: (mutable: T) => void, ...meta: {} extends In ? [meta?: In] : [meta: In]) => void;
29
+ type StateListener<T extends object, In extends object = MetaRecord, Out extends object = MetaRecord> = (state: State<T, In, Out>, ops: Array<Op>, meta: Out) => void;
30
+ interface OpshotHandle<T extends object, In extends object = MetaRecord, Out extends object = MetaRecord> {
31
+ readonly unsafeMutable: object;
27
32
  readonly isMutating: boolean;
28
- readonly mutate: Mutate<T>;
29
- readonly subscribe: (listener: StateListener<T>) => () => void;
33
+ readonly subscribe: (listener: StateListener<T, In, Out>) => () => void;
30
34
  readonly isSameState: (other: unknown) => boolean;
31
35
  readonly unwrap: () => Snapshot<T>;
32
36
  }
33
- type State<T extends object> = Snapshot<T> & {
34
- readonly op: OpshotHandle<T>;
37
+ type State<T extends object, In extends object = MetaRecord, Out extends object = MetaRecord> = Snapshot<T> & {
38
+ readonly mutate: Mutate<T, In>;
39
+ readonly op: OpshotHandle<T, In, Out>;
35
40
  };
36
- type DefineCallback<T extends object> = (mutate: Mutate<T>, get: () => State<T>) => T;
37
- type Define<T extends object> = DefineCallback<T> | T;
41
+ type DefineCallback<T extends object, In extends object = MetaRecord, Out extends object = MetaRecord> = (mutate: Mutate<T, In>, get: () => State<T, In, Out>) => T;
42
+ type Define<T extends object, In extends object = MetaRecord, Out extends object = MetaRecord> = DefineCallback<T, In, Out> | T;
43
+ declare function createMeta<M extends object>(): Meta<M, M>;
44
+ declare function createMeta<M extends object>(defaults: M): Meta<Partial<M>, M>;
38
45
  declare function createState<T extends object>(define: Define<T>): State<T>;
46
+ declare function createState<T extends object, In extends object, Out extends object>(define: Define<T, In, Out>, meta: Meta<In, Out>): State<T, In, Out>;
39
47
  declare function isState(value: unknown): value is State<object>;
40
48
 
41
- interface Group {
42
- createState<T extends object>(define: Define<T>): State<T>;
43
- subscribe(listener: StateListener<object>): () => void;
49
+ interface Group<In extends object = MetaRecord, Out extends object = MetaRecord> {
50
+ createState<T extends object>(define: Define<T, In, Out>): State<T, In, Out>;
51
+ subscribe(listener: StateListener<object, In, Out>): () => void;
44
52
  }
45
53
  declare function createGroup(): Group;
54
+ declare function createGroup<In extends object, Out extends object>(meta: Meta<In, Out>): Group<In, Out>;
46
55
 
47
- export { type Define, type DefineCallback, type Group, type Mutate, type MutateOptions, type Op, type OpshotHandle, type PatchOperation, type State, type StateListener, createGroup, createState, diffSnapshots, isState };
56
+ export { type Define, type DefineCallback, type Group, type Meta, type MetaRecord, type Mutate, type Op, type OpshotHandle, type PatchOperation, type State, type StateListener, createGroup, createMeta, createState, diffSnapshots, isState };
package/dist/index.js CHANGED
@@ -66,11 +66,16 @@ function diffSnapshots(before, after) {
66
66
 
67
67
  // src/createState.ts
68
68
  var stateBrand = /* @__PURE__ */ Symbol.for("opshot.state");
69
+ var metaBrand = /* @__PURE__ */ Symbol.for("opshot.meta");
69
70
  var hasOwn = (value, key) => Object.hasOwn(value, key);
70
- function createState(define) {
71
- return createGroupState(define);
71
+ function createMeta(defaults) {
72
+ const token = defaults === void 0 ? { [metaBrand]: true } : { defaults, [metaBrand]: true };
73
+ return token;
72
74
  }
73
- function createGroupState(define, groupListeners) {
75
+ function createState(define, meta) {
76
+ return createGroupState(define, void 0, meta);
77
+ }
78
+ function createGroupState(define, groupListeners, metaToken) {
74
79
  const callback = typeof define === "function" ? define : () => define;
75
80
  const listeners = /* @__PURE__ */ new Set();
76
81
  const created = {};
@@ -80,7 +85,8 @@ function createGroupState(define, groupListeners) {
80
85
  return proxied;
81
86
  };
82
87
  const get = () => snapshot(requireProxy());
83
- const mutate = (callback2, options = {}) => {
88
+ const mutate = (callback2, ...metaArgs) => {
89
+ const [meta] = metaArgs;
84
90
  const proxied = requireProxy();
85
91
  if (handle.isMutating) throw new Error("opshot: nested mutate on the same state");
86
92
  handle.isMutating = true;
@@ -95,8 +101,9 @@ function createGroupState(define, groupListeners) {
95
101
  if (listeners.size === 0 && (groupListeners?.size ?? 0) === 0) return;
96
102
  const ops = diffSnapshots(before, after);
97
103
  if (ops.length === 0) return;
98
- for (const listener of [...groupListeners ?? []]) listener(after, ops, options);
99
- for (const listener of [...listeners]) listener(after, ops, options);
104
+ const emittedMeta = metaToken?.defaults !== void 0 ? { ...metaToken.defaults, ...meta } : meta ?? {};
105
+ for (const listener of [...groupListeners ?? []]) listener(after, ops, emittedMeta);
106
+ for (const listener of [...listeners]) listener(after, ops, emittedMeta);
100
107
  };
101
108
  const subscribe = (listener) => {
102
109
  listeners.add(listener);
@@ -106,17 +113,20 @@ function createGroupState(define, groupListeners) {
106
113
  };
107
114
  const isSameState = (other) => isState(other) && other.op === handle;
108
115
  const unwrap = () => {
109
- const { op, ...rest } = get();
116
+ const { op, mutate: mutate2, ...rest } = get();
110
117
  return rest;
111
118
  };
112
119
  const literal = callback(mutate, get);
113
- if (Object.hasOwn(literal, "op")) throw new Error('opshot: "op" is a reserved key on a state');
120
+ for (const key of ["op", "mutate"]) {
121
+ if (Object.hasOwn(literal, key)) throw new Error(`opshot: "${key}" is a reserved key on a state`);
122
+ }
114
123
  const base = Object.create(Reflect.getPrototypeOf(literal));
115
124
  Object.defineProperties(base, Object.getOwnPropertyDescriptors(literal));
116
- const handle = { proxy: base, isMutating: false, mutate, subscribe, isSameState, unwrap, [stateBrand]: true };
125
+ const handle = { unsafeMutable: base, isMutating: false, subscribe, isSameState, unwrap, [stateBrand]: true };
117
126
  Object.defineProperty(base, "op", { value: ref(handle), enumerable: true, writable: false, configurable: false });
127
+ Object.defineProperty(base, "mutate", { value: mutate, enumerable: true, writable: false, configurable: false });
118
128
  created.proxied = proxy(base);
119
- handle.proxy = created.proxied;
129
+ handle.unsafeMutable = created.proxied;
120
130
  return get();
121
131
  }
122
132
  function isState(value) {
@@ -127,11 +137,11 @@ function isState(value) {
127
137
  }
128
138
 
129
139
  // src/createGroup.ts
130
- function createGroup() {
140
+ function createGroup(meta) {
131
141
  const listeners = /* @__PURE__ */ new Set();
132
142
  return {
133
143
  createState(define) {
134
- return createGroupState(define, listeners);
144
+ return createGroupState(define, listeners, meta);
135
145
  },
136
146
  subscribe(listener) {
137
147
  listeners.add(listener);
@@ -142,4 +152,4 @@ function createGroup() {
142
152
  };
143
153
  }
144
154
 
145
- export { createGroup, createState, diffSnapshots, isState };
155
+ export { createGroup, createMeta, createState, diffSnapshots, isState };
package/dist/react.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { FC } from 'react';
2
- import { Group, Define, State } from './index.js';
2
+ import { Group, Meta, Define, State } from './index.js';
3
3
  import 'valtio/vanilla';
4
4
 
5
- declare function resnapshot<P extends object>(component: FC<P>): FC<P>;
6
- declare const useCreateGroup: () => Group;
7
- declare const useCreateState: <T extends object>(define: Define<T>, group?: Group) => State<T>;
5
+ declare function retrack<P extends object>(component: FC<P>): FC<P>;
6
+ declare function useGroup(): Group;
7
+ declare function useGroup<In extends object, Out extends object>(meta: Meta<In, Out>): Group<In, Out>;
8
+ declare function useTrackedState<T extends object>(define: Define<T>): State<T>;
9
+ declare function useTrackedState<T extends object, In extends object, Out extends object>(define: Define<T, In, Out>, groupOrMeta: Group<In, Out> | Meta<In, Out>): State<T, In, Out>;
8
10
 
9
- export { resnapshot, useCreateGroup, useCreateState };
11
+ export { retrack, useGroup, useTrackedState };
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isChanged, createProxy } from 'proxy-compare';
2
2
  import { memo, useState, useMemo, useRef, useCallback, useSyncExternalStore, useLayoutEffect } from 'react';
3
- import { unstable_getInternalStates, snapshot, subscribe, ref, proxy } from 'valtio/vanilla';
3
+ import { unstable_getInternalStates, ref, proxy, snapshot, subscribe } from 'valtio/vanilla';
4
4
 
5
5
  // src/react.tsx
6
6
  var { refSet } = unstable_getInternalStates();
@@ -67,11 +67,13 @@ function diffSnapshots(before, after) {
67
67
 
68
68
  // src/createState.ts
69
69
  var stateBrand = /* @__PURE__ */ Symbol.for("opshot.state");
70
+ var metaBrand = /* @__PURE__ */ Symbol.for("opshot.meta");
70
71
  var hasOwn = (value, key) => Object.hasOwn(value, key);
71
- function createState(define) {
72
- return createGroupState(define);
72
+ function isMeta(value) {
73
+ if (typeof value !== "object" || value === null || !hasOwn(value, metaBrand)) return false;
74
+ return value[metaBrand] === true;
73
75
  }
74
- function createGroupState(define, groupListeners) {
76
+ function createGroupState(define, groupListeners, metaToken) {
75
77
  const callback = typeof define === "function" ? define : () => define;
76
78
  const listeners = /* @__PURE__ */ new Set();
77
79
  const created = {};
@@ -81,7 +83,8 @@ function createGroupState(define, groupListeners) {
81
83
  return proxied;
82
84
  };
83
85
  const get = () => snapshot(requireProxy());
84
- const mutate = (callback2, options = {}) => {
86
+ const mutate = (callback2, ...metaArgs) => {
87
+ const [meta] = metaArgs;
85
88
  const proxied = requireProxy();
86
89
  if (handle.isMutating) throw new Error("opshot: nested mutate on the same state");
87
90
  handle.isMutating = true;
@@ -96,8 +99,9 @@ function createGroupState(define, groupListeners) {
96
99
  if (listeners.size === 0 && (groupListeners?.size ?? 0) === 0) return;
97
100
  const ops = diffSnapshots(before, after);
98
101
  if (ops.length === 0) return;
99
- for (const listener of [...groupListeners ?? []]) listener(after, ops, options);
100
- for (const listener of [...listeners]) listener(after, ops, options);
102
+ const emittedMeta = metaToken?.defaults !== void 0 ? { ...metaToken.defaults, ...meta } : meta ?? {};
103
+ for (const listener of [...groupListeners ?? []]) listener(after, ops, emittedMeta);
104
+ for (const listener of [...listeners]) listener(after, ops, emittedMeta);
101
105
  };
102
106
  const subscribe = (listener) => {
103
107
  listeners.add(listener);
@@ -107,17 +111,20 @@ function createGroupState(define, groupListeners) {
107
111
  };
108
112
  const isSameState = (other) => isState(other) && other.op === handle;
109
113
  const unwrap = () => {
110
- const { op, ...rest } = get();
114
+ const { op, mutate: mutate2, ...rest } = get();
111
115
  return rest;
112
116
  };
113
117
  const literal = callback(mutate, get);
114
- if (Object.hasOwn(literal, "op")) throw new Error('opshot: "op" is a reserved key on a state');
118
+ for (const key of ["op", "mutate"]) {
119
+ if (Object.hasOwn(literal, key)) throw new Error(`opshot: "${key}" is a reserved key on a state`);
120
+ }
115
121
  const base = Object.create(Reflect.getPrototypeOf(literal));
116
122
  Object.defineProperties(base, Object.getOwnPropertyDescriptors(literal));
117
- const handle = { proxy: base, isMutating: false, mutate, subscribe, isSameState, unwrap, [stateBrand]: true };
123
+ const handle = { unsafeMutable: base, isMutating: false, subscribe, isSameState, unwrap, [stateBrand]: true };
118
124
  Object.defineProperty(base, "op", { value: ref(handle), enumerable: true, writable: false, configurable: false });
125
+ Object.defineProperty(base, "mutate", { value: mutate, enumerable: true, writable: false, configurable: false });
119
126
  created.proxied = proxy(base);
120
- handle.proxy = created.proxied;
127
+ handle.unsafeMutable = created.proxied;
121
128
  return get();
122
129
  }
123
130
  function isState(value) {
@@ -128,11 +135,11 @@ function isState(value) {
128
135
  }
129
136
 
130
137
  // src/createGroup.ts
131
- function createGroup() {
138
+ function createGroup(meta) {
132
139
  const listeners = /* @__PURE__ */ new Set();
133
140
  return {
134
141
  createState(define) {
135
- return createGroupState(define, listeners);
142
+ return createGroupState(define, listeners, meta);
136
143
  },
137
144
  subscribe(listener) {
138
145
  listeners.add(listener);
@@ -150,7 +157,7 @@ function shouldTraverse(value) {
150
157
  const prototype = Object.getPrototypeOf(value);
151
158
  return prototype === Object.prototype || prototype === null;
152
159
  }
153
- function findSnapshotPaths(value, path = [], paths = []) {
160
+ function findStatePaths(value, path = [], paths = []) {
154
161
  if (isState(value)) {
155
162
  paths.push(path);
156
163
  return paths;
@@ -158,12 +165,12 @@ function findSnapshotPaths(value, path = [], paths = []) {
158
165
  if (!shouldTraverse(value)) return paths;
159
166
  if (Array.isArray(value)) {
160
167
  value.forEach((item, index) => {
161
- findSnapshotPaths(item, [...path, index], paths);
168
+ findStatePaths(item, [...path, index], paths);
162
169
  });
163
170
  } else {
164
171
  for (const [key, propertyValue] of Object.entries(value)) {
165
172
  if (key === "children") continue;
166
- findSnapshotPaths(propertyValue, [...path, key], paths);
173
+ findStatePaths(propertyValue, [...path, key], paths);
167
174
  }
168
175
  }
169
176
  return paths;
@@ -191,10 +198,10 @@ function setAtPath(object, path, value) {
191
198
  return { ...object, [head]: updated };
192
199
  }
193
200
  var targetCache = /* @__PURE__ */ new WeakMap();
194
- function useResnapshotAll(snapshots) {
201
+ function useRetrackAll(states) {
195
202
  const lastRendered = useRef([]);
196
203
  const lastReturned = useRef([]);
197
- const nextProxies = snapshots.map((snap) => snap.op.proxy);
204
+ const nextProxies = states.map((state) => state.op.unsafeMutable);
198
205
  const [proxies, setProxies] = useState(nextProxies);
199
206
  const isStale = proxies.length !== nextProxies.length || proxies.some((proxied, index) => proxied !== nextProxies[index]);
200
207
  if (isStale) setProxies(nextProxies);
@@ -228,40 +235,45 @@ function useResnapshotAll(snapshots) {
228
235
  },
229
236
  [proxies, trackings]
230
237
  );
231
- const freshSnapshots = useSyncExternalStore(subscribe$1, getSnapshot);
238
+ const freshStates = useSyncExternalStore(subscribe$1, getSnapshot);
232
239
  useLayoutEffect(() => {
233
- lastRendered.current = freshSnapshots;
240
+ lastRendered.current = freshStates;
234
241
  });
235
242
  const trackedSnapshots = useMemo(
236
- () => freshSnapshots.map((snap, index) => {
243
+ () => freshStates.map((snap, index) => {
237
244
  const tracking = trackings[index];
238
245
  if (!tracking) return snap;
239
246
  return createProxy(snap, tracking.affected, tracking.proxyCache, targetCache);
240
247
  }),
241
- [freshSnapshots, trackings]
248
+ [freshStates, trackings]
242
249
  );
243
- return isStale ? snapshots : trackedSnapshots;
250
+ return isStale ? states : trackedSnapshots;
244
251
  }
245
- function resnapshot(component) {
252
+ function retrack(component) {
246
253
  const componentName = component.displayName ?? component.name;
247
- const Resnapshotted = (props) => {
248
- const snapshotPaths = useMemo(() => findSnapshotPaths(props), [props]);
249
- const staleSnapshots = useMemo(() => snapshotPaths.map((path) => getAtPath(props, path)).filter(isState), [props, snapshotPaths]);
250
- const freshSnapshots = useResnapshotAll(staleSnapshots);
254
+ const Retracked = (props) => {
255
+ const snapshotPaths = useMemo(() => findStatePaths(props), [props]);
256
+ const staleStates = useMemo(() => snapshotPaths.map((path) => getAtPath(props, path)).filter(isState), [props, snapshotPaths]);
257
+ const freshStates = useRetrackAll(staleStates);
251
258
  const freshProps = useMemo(() => {
252
- if (freshSnapshots === staleSnapshots) return props;
253
- return snapshotPaths.reduce((acc, path, index) => setAtPath(acc, path, freshSnapshots[index]), props);
254
- }, [props, snapshotPaths, staleSnapshots, freshSnapshots]);
259
+ if (freshStates === staleStates) return props;
260
+ return snapshotPaths.reduce((acc, path, index) => setAtPath(acc, path, freshStates[index]), props);
261
+ }, [props, snapshotPaths, staleStates, freshStates]);
255
262
  return component(freshProps);
256
263
  };
257
- Resnapshotted.displayName = `resnapshot(${componentName === "" ? "Anonymous" : componentName})`;
258
- return memo(Resnapshotted);
264
+ Retracked.displayName = `retrack(${componentName === "" ? "Anonymous" : componentName})`;
265
+ return memo(Retracked);
266
+ }
267
+ function useGroup(meta) {
268
+ return useState(() => meta === void 0 ? createGroup() : createGroup(meta))[0];
259
269
  }
260
- var useCreateGroup = () => useState(() => createGroup())[0];
261
- var useCreateState = (define, group) => {
262
- const created = useState(() => group ? group.createState(define) : createState(define))[0];
263
- const [fresh] = useResnapshotAll([created]);
270
+ function useTrackedState(define, groupOrMeta) {
271
+ const created = useState(() => {
272
+ if (groupOrMeta !== void 0 && !isMeta(groupOrMeta)) return groupOrMeta.createState(define);
273
+ return createGroupState(define, void 0, groupOrMeta);
274
+ })[0];
275
+ const [fresh] = useRetrackAll([created]);
264
276
  return fresh;
265
- };
277
+ }
266
278
 
267
- export { resnapshot, useCreateGroup, useCreateState };
279
+ export { retrack, useGroup, useTrackedState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opshot",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation",
5
5
  "type": "module",
6
6
  "exports": {
@@ -68,5 +68,9 @@
68
68
  "typescript": "^5.9.3",
69
69
  "typescript-eslint": "^8.64.0",
70
70
  "vitest": "^4.1.10"
71
+ },
72
+ "allowScripts": {
73
+ "esbuild@0.27.7": true,
74
+ "unrs-resolver@1.12.2": true
71
75
  }
72
76
  }