opshot 0.3.4 → 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 +43 -113
- package/dist/index.d.ts +156 -69
- package/dist/index.js +1105 -1789
- package/package.json +19 -12
package/README.md
CHANGED
|
@@ -28,12 +28,6 @@ const user = useMutableState({ name: "Ada", age: 36 });
|
|
|
28
28
|
user.age = 37;
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The state is created once, on the first render. Pass a function to build the properties once as well, exactly as `useState` does:
|
|
32
|
-
|
|
33
|
-
```tsx
|
|
34
|
-
const navigation = useMutableState(createNavigation);
|
|
35
|
-
```
|
|
36
|
-
|
|
37
31
|
## Bounded re-renders
|
|
38
32
|
|
|
39
33
|
React re-renders a component and its children when its state changes.
|
|
@@ -88,23 +82,23 @@ This is how you optimize re-rendering across your component tree: place `scope`
|
|
|
88
82
|
## Creating State
|
|
89
83
|
|
|
90
84
|
```tsx
|
|
91
|
-
import { ignore, unsafeTrack, useMutableState
|
|
85
|
+
import { ignore, unsafeTrack, useMutableState } from "opshot";
|
|
92
86
|
|
|
93
87
|
interface PlayerState {
|
|
94
88
|
position: number;
|
|
95
|
-
element:
|
|
96
|
-
queue:
|
|
89
|
+
element: HTMLAudioElement;
|
|
90
|
+
queue: Playlist;
|
|
97
91
|
seek: (position: number) => void;
|
|
98
92
|
}
|
|
99
93
|
|
|
100
94
|
const Player = () => {
|
|
101
|
-
const player = useMutableState
|
|
95
|
+
const player: PlayerState = useMutableState({
|
|
102
96
|
position: 0,
|
|
103
97
|
|
|
104
|
-
// ignore()
|
|
98
|
+
// ignore() stores it as-is; opshot never looks inside.
|
|
105
99
|
element: ignore(new Audio()),
|
|
106
100
|
|
|
107
|
-
// unsafeTrack()
|
|
101
|
+
// unsafeTrack() takes it anyway, tracking the plain data on it.
|
|
108
102
|
queue: unsafeTrack(new Playlist()),
|
|
109
103
|
|
|
110
104
|
seek(position: number) {
|
|
@@ -126,13 +120,14 @@ opshot tracks plain data.
|
|
|
126
120
|
|
|
127
121
|
It can't track:
|
|
128
122
|
|
|
129
|
-
-
|
|
130
|
-
- #private fields
|
|
131
|
-
-
|
|
123
|
+
- Hidden stores (language-level features like in Map)
|
|
124
|
+
- #private fields
|
|
125
|
+
- Own function properties on class instances
|
|
126
|
+
- Non-writable properties that hold an object
|
|
132
127
|
|
|
133
|
-
|
|
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.
|
|
134
129
|
|
|
135
|
-
|
|
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.
|
|
136
131
|
|
|
137
132
|
## Tracked collections
|
|
138
133
|
|
|
@@ -159,12 +154,8 @@ const Counter = () => {
|
|
|
159
154
|
|
|
160
155
|
useEffect(
|
|
161
156
|
() =>
|
|
162
|
-
subscribe(counter, (
|
|
163
|
-
//
|
|
164
|
-
// do: { op: "replace", path: ["count"], value: 1 },
|
|
165
|
-
// undo: { op: "replace", path: ["count"], value: 0 },
|
|
166
|
-
// }]
|
|
167
|
-
// 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 }]
|
|
168
159
|
}),
|
|
169
160
|
[counter],
|
|
170
161
|
);
|
|
@@ -173,127 +164,63 @@ const Counter = () => {
|
|
|
173
164
|
};
|
|
174
165
|
```
|
|
175
166
|
|
|
176
|
-
##
|
|
167
|
+
## Operations
|
|
177
168
|
|
|
178
|
-
An
|
|
169
|
+
An operation is one key's change on one node:
|
|
179
170
|
|
|
180
171
|
```ts
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
interface Op {
|
|
189
|
-
readonly do: Operation;
|
|
190
|
-
readonly undo: Operation;
|
|
172
|
+
interface Operation {
|
|
173
|
+
readonly node: object;
|
|
174
|
+
readonly key: string;
|
|
175
|
+
readonly before?: unknown;
|
|
176
|
+
readonly after?: unknown;
|
|
177
|
+
readonly meta: unknown;
|
|
191
178
|
}
|
|
192
179
|
```
|
|
193
180
|
|
|
194
|
-
`
|
|
195
|
-
|
|
196
|
-
```tsx
|
|
197
|
-
import { useEffect, useRef } from "react";
|
|
198
|
-
import { applyOps, subscribe, useMutableState, type Op } from "opshot";
|
|
199
|
-
|
|
200
|
-
const replay = {};
|
|
201
|
-
|
|
202
|
-
const Counter = () => {
|
|
203
|
-
const counter = useMutableState({ count: 0 });
|
|
204
|
-
const history = useRef<Array<ReadonlyArray<Op>>>([]);
|
|
205
|
-
|
|
206
|
-
useEffect(
|
|
207
|
-
() =>
|
|
208
|
-
subscribe(counter, (ops, meta) => {
|
|
209
|
-
// Skip our own replays, so undo doesn't record itself.
|
|
210
|
-
if (meta === replay) return;
|
|
181
|
+
`before` and `after` are absent properties when the key was absent.
|
|
211
182
|
|
|
212
|
-
|
|
213
|
-
}),
|
|
214
|
-
[counter],
|
|
215
|
-
);
|
|
183
|
+
## Emission
|
|
216
184
|
|
|
217
|
-
|
|
218
|
-
const ops = history.current.pop();
|
|
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.
|
|
219
186
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
applyOps(
|
|
223
|
-
counter,
|
|
224
|
-
[...ops].reverse().map((op) => op.undo),
|
|
225
|
-
replay,
|
|
226
|
-
);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<>
|
|
231
|
-
<button onClick={() => counter.count++}>+</button>
|
|
232
|
-
<button onClick={undo}>Undo</button>
|
|
233
|
-
</>
|
|
234
|
-
);
|
|
235
|
-
};
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Replay is exact for anything opshot can see: plain data. State behind a constraint is the exception.
|
|
239
|
-
|
|
240
|
-
If your state is JSON serializable, **then ops are too**.
|
|
241
|
-
|
|
242
|
-
## Groups
|
|
243
|
-
|
|
244
|
-
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.
|
|
245
188
|
|
|
246
189
|
```tsx
|
|
247
190
|
import { useEffect } from "react";
|
|
248
|
-
import { subscribe,
|
|
249
|
-
|
|
250
|
-
const Editor = () => {
|
|
251
|
-
const group = useGroup();
|
|
191
|
+
import { subscribe, useMutableState } from "opshot";
|
|
252
192
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
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) });
|
|
256
196
|
|
|
257
197
|
useEffect(
|
|
258
198
|
() =>
|
|
259
|
-
|
|
260
|
-
// state is whichever one changed.
|
|
261
|
-
subscribe(group, (state, ops, meta) => {
|
|
199
|
+
subscribe(cursor, (operations) => {
|
|
262
200
|
// ...
|
|
263
201
|
}),
|
|
264
|
-
[
|
|
202
|
+
[cursor],
|
|
265
203
|
);
|
|
266
204
|
|
|
267
205
|
// ...
|
|
268
206
|
};
|
|
269
207
|
```
|
|
270
208
|
|
|
271
|
-
##
|
|
209
|
+
## Batches
|
|
272
210
|
|
|
273
|
-
|
|
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.
|
|
274
212
|
|
|
275
213
|
```tsx
|
|
276
214
|
import { useEffect } from "react";
|
|
277
|
-
import {
|
|
278
|
-
|
|
279
|
-
interface DocumentMeta {
|
|
280
|
-
replay?: boolean;
|
|
281
|
-
source?: string;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const docChannel = createChannel<DocumentMeta>({ source: "editor" }); // set defaults
|
|
215
|
+
import { batch, subscribe, useMutableState } from "opshot";
|
|
285
216
|
|
|
286
217
|
const TitleBar = () => {
|
|
287
218
|
const doc = useMutableState({ title: "Untitled" });
|
|
288
219
|
|
|
289
220
|
useEffect(
|
|
290
221
|
() =>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (!context.isTransaction) return;
|
|
294
|
-
|
|
295
|
-
// Own-channel transaction: meta is typed, with defaults merged.
|
|
296
|
-
if (context.meta.replay) return;
|
|
222
|
+
subscribe(doc, (operations) => {
|
|
223
|
+
if (operations[0]?.meta === "replay") return;
|
|
297
224
|
|
|
298
225
|
// ...
|
|
299
226
|
}),
|
|
@@ -301,9 +228,12 @@ const TitleBar = () => {
|
|
|
301
228
|
);
|
|
302
229
|
|
|
303
230
|
const rename = () => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
231
|
+
batch(
|
|
232
|
+
() => {
|
|
233
|
+
doc.title = "Draft";
|
|
234
|
+
},
|
|
235
|
+
{ source: "editor" },
|
|
236
|
+
);
|
|
307
237
|
};
|
|
308
238
|
|
|
309
239
|
// ...
|
package/dist/index.d.ts
CHANGED
|
@@ -1,75 +1,112 @@
|
|
|
1
1
|
import { ComponentType, FC } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type StateListener = (ops: ReadonlyArray<Op>, meta: unknown) => void;
|
|
26
|
-
type GroupListener = (state: object, ops: ReadonlyArray<Op>, meta: unknown) => void;
|
|
27
|
-
|
|
28
|
-
interface Group {
|
|
29
|
-
createMutableState<T extends object>(properties: T): T;
|
|
3
|
+
/**
|
|
4
|
+
* Sets when a state's window flushes. Call `flush` once.
|
|
5
|
+
*
|
|
6
|
+
* @param flush - Delivers pending operations.
|
|
7
|
+
* @returns Nothing.
|
|
8
|
+
*/
|
|
9
|
+
type EmissionScheduler = (flush: () => void) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Options for `createMutableState`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* createMutableState({ count: 0 }, { emitOn, strict: false })
|
|
15
|
+
*/
|
|
16
|
+
interface MutableStateOptions {
|
|
17
|
+
/**
|
|
18
|
+
* When the window flushes for all writes. Defaults to a microtask.
|
|
19
|
+
*/
|
|
20
|
+
readonly emitOn?: EmissionScheduler;
|
|
21
|
+
/**
|
|
22
|
+
* When true, throws at a dangerous edge, at the cause. Defaults to true.
|
|
23
|
+
*/
|
|
24
|
+
readonly strict?: boolean;
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
declare function
|
|
42
|
-
|
|
43
|
-
interface Channel<M extends object> {
|
|
44
|
-
transact(state: object, mutate: () => void, meta?: Partial<M>): void;
|
|
45
|
-
subscribe(group: Group, listener: (state: object, ops: ReadonlyArray<Op>, context: Context<M>) => void): () => void;
|
|
46
|
-
subscribe(state: object, listener: (ops: ReadonlyArray<Op>, context: Context<M>) => void): () => void;
|
|
47
|
-
applyOps(state: object, operations: ReadonlyArray<Operation>, meta?: Partial<M>): void;
|
|
48
|
-
}
|
|
49
|
-
declare function createChannel<M extends object>(defaults?: M): Channel<M>;
|
|
50
|
-
|
|
51
|
-
declare function createMutableState<T extends object>(properties: T, group?: Group): T;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a mutable state object.
|
|
28
|
+
*
|
|
29
|
+
* `ignore()` marks an object so every edge to it is untracked. `unsafeTrack()` marks an object so a node entering while marked, or entering beneath an exempt node, is exempt from strict.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam T - State shape.
|
|
32
|
+
* @param properties - Initial fields.
|
|
33
|
+
* @param options - Creation options.
|
|
34
|
+
* @returns The state.
|
|
35
|
+
*/
|
|
36
|
+
declare function createMutableState<T extends object>(properties: T, options?: MutableStateOptions): T;
|
|
52
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Returns a stable identity key for a value.
|
|
40
|
+
*
|
|
41
|
+
* @param value - Value to identify.
|
|
42
|
+
* @returns Identity key.
|
|
43
|
+
*/
|
|
53
44
|
declare function identify(value: object): object;
|
|
45
|
+
/**
|
|
46
|
+
* Returns whether two values share the same identity.
|
|
47
|
+
*
|
|
48
|
+
* @param first - First value.
|
|
49
|
+
* @param second - Second value.
|
|
50
|
+
* @returns True if they match.
|
|
51
|
+
*/
|
|
54
52
|
declare function isSameIdentity(first: object, second: object): boolean;
|
|
55
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Returns whether a value is an opshot state.
|
|
56
|
+
*
|
|
57
|
+
* @param value - Value to test.
|
|
58
|
+
* @returns True if it is a state.
|
|
59
|
+
*/
|
|
56
60
|
declare function isState(value: unknown): value is object;
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* A change to one key of a node.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* { node, key: "count", before: 0, after: 1, meta: undefined }
|
|
67
|
+
*/
|
|
68
|
+
interface Operation {
|
|
69
|
+
/**
|
|
70
|
+
* Node that changed.
|
|
71
|
+
*/
|
|
72
|
+
readonly node: object;
|
|
73
|
+
/**
|
|
74
|
+
* Key that changed.
|
|
75
|
+
*/
|
|
76
|
+
readonly key: string;
|
|
77
|
+
/**
|
|
78
|
+
* Value before, absent when the key was absent.
|
|
79
|
+
*/
|
|
80
|
+
readonly before?: unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Value after, absent when the key is absent.
|
|
83
|
+
*/
|
|
84
|
+
readonly after?: unknown;
|
|
85
|
+
/**
|
|
86
|
+
* Meta of the batch the write was made in.
|
|
87
|
+
*/
|
|
88
|
+
readonly meta: unknown;
|
|
89
|
+
}
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Marks an object so every edge to it is untracked in every state.
|
|
93
|
+
*
|
|
94
|
+
* @typeParam T - Value type.
|
|
95
|
+
* @param value - Value to mark or unmark.
|
|
96
|
+
* @param on - Whether the mark is set.
|
|
97
|
+
* @returns `value`.
|
|
98
|
+
*/
|
|
99
|
+
declare function ignore<T>(value: T, on?: boolean): T;
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Marks an object so a node entering a state while marked, or entering beneath an exempt node, is exempt from strict.
|
|
103
|
+
*
|
|
104
|
+
* @typeParam T - Value type.
|
|
105
|
+
* @param value - Value to mark or unmark.
|
|
106
|
+
* @param on - Whether the mark is set.
|
|
107
|
+
* @returns `value`.
|
|
108
|
+
*/
|
|
109
|
+
declare function unsafeTrack<T>(value: T, on?: boolean): T;
|
|
73
110
|
|
|
74
111
|
type DateConstructorArgs = [] | [value: number | string] | [
|
|
75
112
|
year: number,
|
|
@@ -80,10 +117,17 @@ type DateConstructorArgs = [] | [value: number | string] | [
|
|
|
80
117
|
seconds?: number,
|
|
81
118
|
milliseconds?: number
|
|
82
119
|
];
|
|
120
|
+
/**
|
|
121
|
+
* Tracked `Date` for use in state.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* createMutableState({ when: new TrackedDate() })
|
|
125
|
+
*/
|
|
83
126
|
declare class TrackedDate {
|
|
84
127
|
private epochMs;
|
|
85
128
|
constructor(...args: DateConstructorArgs);
|
|
86
129
|
private readDate;
|
|
130
|
+
private readEpochMs;
|
|
87
131
|
private write;
|
|
88
132
|
toString(): string;
|
|
89
133
|
toDateString(): string;
|
|
@@ -134,6 +178,12 @@ declare class TrackedDate {
|
|
|
134
178
|
readonly [Symbol.toStringTag]: "TrackedDate";
|
|
135
179
|
}
|
|
136
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Tracked `Map` for use in state.
|
|
183
|
+
*
|
|
184
|
+
* @typeParam K - Key type.
|
|
185
|
+
* @typeParam V - Value type.
|
|
186
|
+
*/
|
|
137
187
|
declare class TrackedMap<K, V> {
|
|
138
188
|
private slots;
|
|
139
189
|
private index;
|
|
@@ -153,6 +203,11 @@ declare class TrackedMap<K, V> {
|
|
|
153
203
|
readonly [Symbol.toStringTag]: "TrackedMap";
|
|
154
204
|
}
|
|
155
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Tracked `Set` for use in state.
|
|
208
|
+
*
|
|
209
|
+
* @typeParam T - Member type.
|
|
210
|
+
*/
|
|
156
211
|
declare class TrackedSet<T> {
|
|
157
212
|
private slots;
|
|
158
213
|
private index;
|
|
@@ -171,15 +226,47 @@ declare class TrackedSet<T> {
|
|
|
171
226
|
readonly [Symbol.toStringTag]: "TrackedSet";
|
|
172
227
|
}
|
|
173
228
|
|
|
174
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Listener for one state's changes.
|
|
231
|
+
*
|
|
232
|
+
* @param operations - Operations for the change.
|
|
233
|
+
*/
|
|
234
|
+
type StateListener = (operations: ReadonlyArray<Operation>) => void;
|
|
175
235
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Listens for changes to a state.
|
|
238
|
+
*
|
|
239
|
+
* @param state - State to listen to.
|
|
240
|
+
* @param listener - Called on each change.
|
|
241
|
+
* @returns Unsubscribe function.
|
|
242
|
+
*/
|
|
243
|
+
declare function subscribe(state: object, listener: StateListener): () => void;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Runs writes carrying `meta`.
|
|
247
|
+
*
|
|
248
|
+
* @param callback - Function that writes any states.
|
|
249
|
+
* @param meta - Carried by each write's operation.
|
|
250
|
+
*/
|
|
251
|
+
declare function batch(callback: () => void, meta?: unknown): void;
|
|
180
252
|
|
|
181
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Wraps a component so it re-renders only when fields it read change.
|
|
255
|
+
*
|
|
256
|
+
* @typeParam P - Props type.
|
|
257
|
+
* @param Component - Component to wrap.
|
|
258
|
+
* @returns The wrapped component.
|
|
259
|
+
*/
|
|
260
|
+
declare function scope<P extends object>(Component: ComponentType<P>): FC<P>;
|
|
182
261
|
|
|
183
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Creates mutable state for a component.
|
|
264
|
+
*
|
|
265
|
+
* @typeParam T - State shape.
|
|
266
|
+
* @param properties - Initial fields, or a function that returns them.
|
|
267
|
+
* @param options - Creation options.
|
|
268
|
+
* @returns The state.
|
|
269
|
+
*/
|
|
270
|
+
declare function useMutableState<T extends object>(properties: (() => T) | T, options?: MutableStateOptions): T;
|
|
184
271
|
|
|
185
|
-
export { type
|
|
272
|
+
export { type EmissionScheduler, type MutableStateOptions, type Operation, type StateListener, TrackedDate, TrackedMap, TrackedSet, batch, createMutableState, identify, ignore, isSameIdentity, isState, scope, subscribe, unsafeTrack, useMutableState };
|