opshot 0.4.0 → 0.5.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
@@ -95,10 +95,10 @@ const Player = () => {
95
95
  const player: PlayerState = useMutableState({
96
96
  position: 0,
97
97
 
98
- // ignore() on a value in the factory argument makes the edge at that path untracked.
98
+ // ignore() stores it as-is; opshot never looks inside.
99
99
  element: ignore(new Audio()),
100
100
 
101
- // unsafeTrack() on a value in the factory argument disables strict at and under that path.
101
+ // unsafeTrack() takes it anyway, tracking the plain data on it.
102
102
  queue: unsafeTrack(new Playlist()),
103
103
 
104
104
  seek(position: number) {
@@ -125,9 +125,9 @@ It can't track:
125
125
  - Own function properties on class instances
126
126
  - Non-writable properties that hold an object
127
127
 
128
- `strict: true` throws at a dangerous edge, at the cause.
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
129
 
130
- Use `ignore` on a value in the factory argument to make the edge at that path untracked. Use `unsafeTrack` on a value in the factory argument to disable strict at and under that path.
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.
131
131
 
132
132
  ## Tracked collections
133
133
 
@@ -154,12 +154,8 @@ const Counter = () => {
154
154
 
155
155
  useEffect(
156
156
  () =>
157
- subscribe(counter, (ops, meta) => {
158
- // ops: [{
159
- // do: { verb: "assign", path: ["count"], value: 1 },
160
- // undo: { verb: "assign", path: ["count"], value: 0 },
161
- // }]
162
- // meta: whatever the writer passed, or undefined for bare writes
157
+ subscribe(counter, (operations) => {
158
+ // operations: [{ node, key: "count", before: 0, after: 1, meta: undefined }]
163
159
  }),
164
160
  [counter],
165
161
  );
@@ -168,126 +164,65 @@ const Counter = () => {
168
164
  };
169
165
  ```
170
166
 
171
- ## Ops
167
+ ## Operations
172
168
 
173
- An op is an invertible pair of halves. Every half uses one of three verbs:
169
+ An operation is one key's change on one node:
174
170
 
175
171
  ```ts
176
- type OperationPath = ReadonlyArray<string | number>;
177
-
178
172
  interface Operation {
179
- readonly do:
180
- | {
181
- readonly verb: "assign";
182
- readonly path: OperationPath;
183
- readonly value: unknown;
184
- readonly ids?: ReadonlyArray<number>;
185
- }
186
- | { readonly verb: "delete"; readonly path: OperationPath }
187
- | { readonly verb: "link"; readonly path: OperationPath; readonly ref: number };
188
- readonly undo: Operation["do"];
173
+ readonly node: object;
174
+ readonly key: string;
175
+ readonly before?: unknown;
176
+ readonly after?: unknown;
177
+ readonly meta: unknown;
189
178
  }
190
179
  ```
191
180
 
192
- Ids vend in admission-walk order over the emitted artifact; a departure's undo assign may carry `ids` to rebind that walk, the one naming fact construction cannot re-derive. `applyOperations` puts them back on a state, so a history is a list of ops and an undo is `applyOperations` with `"undo"`.
181
+ `before` and `after` are absent properties when the key was absent.
193
182
 
194
- ```tsx
195
- import { useEffect, useRef } from "react";
196
- import { applyOperations, subscribe, useMutableState, type Operation } from "opshot";
183
+ ## Emission
197
184
 
198
- const replay = {};
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.
199
186
 
200
- const Counter = () => {
201
- const counter = useMutableState({ count: 0 });
202
- const history = useRef<Array<ReadonlyArray<Operation>>>([]);
203
-
204
- useEffect(
205
- () =>
206
- subscribe(counter, (ops, meta) => {
207
- // Skip our own replays, so undo doesn't record itself.
208
- if (meta === replay) return;
209
-
210
- history.current.push(ops);
211
- }),
212
- [counter],
213
- );
214
-
215
- const undo = () => {
216
- const ops = history.current.pop();
217
-
218
- if (!ops) return;
219
-
220
- applyOperations(counter, ops, "undo", replay);
221
- };
222
-
223
- return (
224
- <>
225
- <button onClick={() => counter.count++}>+</button>
226
- <button onClick={undo}>Undo</button>
227
- </>
228
- );
229
- };
230
- ```
231
-
232
- Replay is exact for anything opshot can see: plain data. State behind a constraint is the exception.
233
-
234
- If your state is JSON serializable, **then ops are too**.
235
-
236
- ## Groups
237
-
238
- A group creates states and hears every op from the states it created: one stream for history, sync, persistence, etc.
187
+ `emitOn` sets the window instead. opshot hands you a `flush`, and the state waits until you call it.
239
188
 
240
189
  ```tsx
241
190
  import { useEffect } from "react";
242
- import { subscribe, useGroup, useMutableState } from "opshot";
243
-
244
- const Editor = () => {
245
- const group = useGroup();
191
+ import { subscribe, useMutableState } from "opshot";
246
192
 
247
- // Created through the group, so their ops reach the group's subscribers.
248
- const doc = useMutableState({ items: new Array<string>() }, { group });
249
- const selection = useMutableState({ index: 0 }, { group });
193
+ const Chart = () => {
194
+ // One emission per frame, however many writes land in between.
195
+ const cursor = useMutableState({ x: 0, y: 0 }, { emitOn: (flush) => requestAnimationFrame(flush) });
250
196
 
251
197
  useEffect(
252
198
  () =>
253
- // Fires for doc, selection, and every other state the group created.
254
- // state is whichever one changed.
255
- subscribe(group, (state, ops, meta) => {
199
+ subscribe(cursor, (operations) => {
256
200
  // ...
257
201
  }),
258
- [group],
202
+ [cursor],
259
203
  );
260
204
 
261
205
  // ...
262
206
  };
263
207
  ```
264
208
 
265
- ## Channels
209
+ Separate from that callback, the `flush(state)` export ends the window from outside.
210
+
211
+ ## Batches
266
212
 
267
- A channel binds `transact`, `subscribe`, and `applyOperations` to a typed meta convention, so a listener can tell its own writes from everyone else's.
213
+ `batch` runs a callback and tags every write inside it with your `meta`, so a listener can tell its own writes from everyone else's.
268
214
 
269
215
  ```tsx
270
216
  import { useEffect } from "react";
271
- import { createChannel, useMutableState } from "opshot";
272
-
273
- interface DocumentMeta {
274
- replay?: boolean;
275
- source?: string;
276
- }
277
-
278
- const docChannel = createChannel<DocumentMeta>({ source: "editor" }); // set defaults
217
+ import { batch, subscribe, useMutableState } from "opshot";
279
218
 
280
219
  const TitleBar = () => {
281
220
  const doc = useMutableState({ title: "Untitled" });
282
221
 
283
222
  useEffect(
284
223
  () =>
285
- docChannel.subscribe(doc, (ops, context) => {
286
- // A bare write, or a transact from another channel: meta is unknown.
287
- if (!context.isTransaction) return;
288
-
289
- // Own-channel transaction: meta is typed, with defaults merged.
290
- if (context.meta.replay) return;
224
+ subscribe(doc, (operations) => {
225
+ if (operations[0]?.meta === "replay") return;
291
226
 
292
227
  // ...
293
228
  }),
@@ -295,9 +230,12 @@ const TitleBar = () => {
295
230
  );
296
231
 
297
232
  const rename = () => {
298
- docChannel.transact(doc, () => {
299
- doc.title = "Draft";
300
- });
233
+ batch(
234
+ () => {
235
+ doc.title = "Draft";
236
+ },
237
+ { source: "editor" },
238
+ );
301
239
  };
302
240
 
303
241
  // ...