opshot 0.4.0 → 0.5.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/README.md +34 -98
- package/dist/index.d.ts +69 -375
- package/dist/index.js +688 -3124
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -95,10 +95,10 @@ const Player = () => {
|
|
|
95
95
|
const player: PlayerState = useMutableState({
|
|
96
96
|
position: 0,
|
|
97
97
|
|
|
98
|
-
// ignore()
|
|
98
|
+
// ignore() stores it as-is; opshot never looks inside.
|
|
99
99
|
element: ignore(new Audio()),
|
|
100
100
|
|
|
101
|
-
// unsafeTrack()
|
|
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:
|
|
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
|
-
|
|
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, (
|
|
158
|
-
//
|
|
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,63 @@ const Counter = () => {
|
|
|
168
164
|
};
|
|
169
165
|
```
|
|
170
166
|
|
|
171
|
-
##
|
|
167
|
+
## Operations
|
|
172
168
|
|
|
173
|
-
An
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
import { useEffect, useRef } from "react";
|
|
196
|
-
import { applyOperations, subscribe, useMutableState, type Operation } from "opshot";
|
|
197
|
-
|
|
198
|
-
const replay = {};
|
|
199
|
-
|
|
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.
|
|
181
|
+
`before` and `after` are absent properties when the key was absent.
|
|
233
182
|
|
|
234
|
-
|
|
183
|
+
## Emission
|
|
235
184
|
|
|
236
|
-
|
|
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.
|
|
237
186
|
|
|
238
|
-
|
|
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,
|
|
243
|
-
|
|
244
|
-
const Editor = () => {
|
|
245
|
-
const group = useGroup();
|
|
191
|
+
import { subscribe, useMutableState } from "opshot";
|
|
246
192
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
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
|
-
|
|
254
|
-
// state is whichever one changed.
|
|
255
|
-
subscribe(group, (state, ops, meta) => {
|
|
199
|
+
subscribe(cursor, (operations) => {
|
|
256
200
|
// ...
|
|
257
201
|
}),
|
|
258
|
-
[
|
|
202
|
+
[cursor],
|
|
259
203
|
);
|
|
260
204
|
|
|
261
205
|
// ...
|
|
262
206
|
};
|
|
263
207
|
```
|
|
264
208
|
|
|
265
|
-
##
|
|
209
|
+
## Batches
|
|
266
210
|
|
|
267
|
-
|
|
211
|
+
`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
212
|
|
|
269
213
|
```tsx
|
|
270
214
|
import { useEffect } from "react";
|
|
271
|
-
import {
|
|
272
|
-
|
|
273
|
-
interface DocumentMeta {
|
|
274
|
-
replay?: boolean;
|
|
275
|
-
source?: string;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const docChannel = createChannel<DocumentMeta>({ source: "editor" }); // set defaults
|
|
215
|
+
import { batch, subscribe, useMutableState } from "opshot";
|
|
279
216
|
|
|
280
217
|
const TitleBar = () => {
|
|
281
218
|
const doc = useMutableState({ title: "Untitled" });
|
|
282
219
|
|
|
283
220
|
useEffect(
|
|
284
221
|
() =>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!context.isTransaction) return;
|
|
288
|
-
|
|
289
|
-
// Own-channel transaction: meta is typed, with defaults merged.
|
|
290
|
-
if (context.meta.replay) return;
|
|
222
|
+
subscribe(doc, (operations) => {
|
|
223
|
+
if (operations[0]?.meta === "replay") return;
|
|
291
224
|
|
|
292
225
|
// ...
|
|
293
226
|
}),
|
|
@@ -295,9 +228,12 @@ const TitleBar = () => {
|
|
|
295
228
|
);
|
|
296
229
|
|
|
297
230
|
const rename = () => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
231
|
+
batch(
|
|
232
|
+
() => {
|
|
233
|
+
doc.title = "Draft";
|
|
234
|
+
},
|
|
235
|
+
{ source: "editor" },
|
|
236
|
+
);
|
|
301
237
|
};
|
|
302
238
|
|
|
303
239
|
// ...
|