opshot 0.5.3 → 0.6.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
@@ -82,7 +82,8 @@ This is how you optimize re-rendering across your component tree: place `scope`
82
82
  ## Creating State
83
83
 
84
84
  ```tsx
85
- import { ignore, unsafeTrack, useMutableState } from "opshot";
85
+ import { ignore, unsafeTrack } from "opshot";
86
+ import { useMutableState } from "opshot/react";
86
87
 
87
88
  interface PlayerState {
88
89
  position: number;
@@ -114,6 +115,10 @@ const Player = () => {
114
115
  };
115
116
  ```
116
117
 
118
+ `createMutableState(properties, options)` creates the same state outside a component, for a store or a process with no React.
119
+
120
+ `ignore(value, false)` and `unsafeTrack(value, false)` undo these effects on the value.
121
+
117
122
  ## Constraints
118
123
 
119
124
  opshot tracks plain data.
@@ -125,16 +130,15 @@ It can't track:
125
130
  - Own function properties on class instances
126
131
  - Non-writable properties that hold an object
127
132
 
128
- By default opshot throws when it meets one of these, naming the value that caused it. Passing `strict: false` turns off those errors but may cause unpredictable behaviour.
129
-
130
- `ignore(value)` stores a value without state inside it being tracked, and `ignore(value, false)` undoes that. `unsafeTrack(value)` does the reverse: it takes a value strict mode would reject, tracking the plain data on it and quietly missing the rest. Either mark only affects states the value enters afterwards.
133
+ `strict: true` throws when opshot meets one of these, naming the value that caused it; `strict: false` stores the value untracked. It defaults to true in development and false in production.
131
134
 
132
135
  ## Tracked collections
133
136
 
134
137
  `TrackedMap`, `TrackedSet`, and `TrackedDate` stand in for the built-ins opshot rejects. They have the exact same API as their counterparts.
135
138
 
136
139
  ```ts
137
- import { TrackedMap, useMutableState } from "opshot";
140
+ import { TrackedMap } from "opshot";
141
+ import { useMutableState } from "opshot/react";
138
142
 
139
143
  const state = useMutableState({ index: new TrackedMap<string, number>() });
140
144
 
@@ -147,15 +151,20 @@ state.index.set("a", 1);
147
151
 
148
152
  ```tsx
149
153
  import { useEffect } from "react";
150
- import { subscribe, useMutableState } from "opshot";
154
+ import { subscribe } from "opshot";
155
+ import { useMutableState } from "opshot/react";
156
+
157
+ interface Meta {
158
+ source: string;
159
+ }
151
160
 
152
161
  const Counter = () => {
153
162
  const counter = useMutableState({ count: 0 });
154
163
 
155
164
  useEffect(
156
165
  () =>
157
- subscribe(counter, (operations) => {
158
- // operations: [{ node, key: "count", before: 0, after: 1, meta: undefined }]
166
+ subscribe<Meta | undefined>(counter, (operations) => {
167
+ // operations: [{ kind: "change", node, key: "count", before: 0, after: 1, meta: undefined }]
159
168
  }),
160
169
  [counter],
161
170
  );
@@ -169,26 +178,58 @@ const Counter = () => {
169
178
  An operation is one key's change on one node:
170
179
 
171
180
  ```ts
172
- interface Operation {
173
- readonly node: object;
181
+ interface AddOperation<Meta = unknown> {
182
+ readonly kind: "add";
183
+ readonly node: Record<string, unknown>; // The live state being applied to
184
+ readonly key: string;
185
+ readonly after: unknown;
186
+ readonly meta: Meta;
187
+ }
188
+
189
+ interface ChangeOperation<Meta = unknown> {
190
+ readonly kind: "change";
191
+ readonly node: Record<string, unknown>;
192
+ readonly key: string;
193
+ readonly before: unknown;
194
+ readonly after: unknown;
195
+ readonly meta: Meta;
196
+ }
197
+
198
+ interface DeleteOperation<Meta = unknown> {
199
+ readonly kind: "delete";
200
+ readonly node: Record<string, unknown>;
174
201
  readonly key: string;
175
- readonly before?: unknown;
176
- readonly after?: unknown;
177
- readonly meta: unknown;
202
+ readonly before: unknown;
203
+ readonly meta: Meta;
178
204
  }
205
+
206
+ type Operation<Meta = unknown> = AddOperation<Meta> | ChangeOperation<Meta> | DeleteOperation<Meta>;
207
+ ```
208
+
209
+ ```ts
210
+ const revert = (operation: Operation) => {
211
+ if (operation.kind === "add") delete operation.node[operation.key];
212
+ else operation.node[operation.key] = operation.before;
213
+ };
214
+
215
+ const apply = (operation: Operation) => {
216
+ if (operation.kind === "delete") delete operation.node[operation.key];
217
+ else operation.node[operation.key] = operation.after;
218
+ };
179
219
  ```
180
220
 
181
- `before` and `after` are absent properties when the key was absent.
221
+ Undo and redo are a switch on `kind`. Revert an emission in reverse and apply it forward.
182
222
 
183
223
  ## Emission
184
224
 
185
- A state gathers its writes and delivers them together. The window is a microtask by default, so everything you change in one go arrives as one emission carrying the net change — a listener hears where a field ended up, not every step it took there.
225
+ A state gathers its writes and delivers them together, in order. The window is a microtask by default, so everything you change in one go arrives as one emission carrying the net change — a listener hears where a field ended up, not every step it took there.
186
226
 
187
227
  `emitOn` sets the window instead. opshot hands you a `flush`, and the state waits until you call it.
188
228
 
189
229
  ```tsx
190
230
  import { useEffect } from "react";
191
- import { subscribe, useMutableState } from "opshot";
231
+ import { subscribe } from "opshot";
232
+ import { useMutableState } from "opshot/react";
192
233
 
193
234
  const Chart = () => {
194
235
  // One emission per frame, however many writes land in between.
@@ -206,7 +247,7 @@ const Chart = () => {
206
247
  };
207
248
  ```
208
249
 
209
- Separate from that callback, the `flush(state)` export ends the window from outside.
250
+ Separate from that callback, the `flush(state, ...states)` export ends each state's window from outside.
210
251
 
211
252
  ## Batches
212
253
 
@@ -214,7 +255,8 @@ Separate from that callback, the `flush(state)` export ends the window from outs
214
255
 
215
256
  ```tsx
216
257
  import { useEffect } from "react";
217
- import { batch, subscribe, useMutableState } from "opshot";
258
+ import { batch, subscribe } from "opshot";
259
+ import { useMutableState } from "opshot/react";
218
260
 
219
261
  const TitleBar = () => {
220
262
  const doc = useMutableState({ title: "Untitled" });
@@ -222,7 +264,7 @@ const TitleBar = () => {
222
264
  useEffect(
223
265
  () =>
224
266
  subscribe(doc, (operations) => {
225
- if (operations[0]?.meta === "replay") return;
267
+ const edits = operations.filter((operation) => operation.meta !== "replay");
226
268
 
227
269
  // ...
228
270
  }),
@@ -230,18 +272,35 @@ const TitleBar = () => {
230
272
  );
231
273
 
232
274
  const rename = () => {
233
- batch(
234
- () => {
235
- doc.title = "Draft";
236
- },
237
- { source: "editor" },
238
- );
275
+ batch(() => {
276
+ doc.title = "Draft";
277
+ }, "editor");
239
278
  };
240
279
 
241
280
  // ...
242
281
  };
243
282
  ```
244
283
 
284
+ ## Identity
285
+
286
+ ```tsx
287
+ const object = { name: "Ada", age: 36 };
288
+ const state = useMutableState(object);
289
+
290
+ object === state; // false
291
+ isSameIdentity(object, state); // true
292
+
293
+ isState(object); // false
294
+ isState(state); // true
295
+ ```
296
+
297
+ ```tsx
298
+ // The state's identity changes when the state or anything nested inside it changes.
299
+ useEffect(() => {
300
+ // ...
301
+ }, [state]);
302
+ ```
303
+
245
304
  ## License
246
305
 
247
306
  [MIT](LICENSE)