sia-reactor 0.0.33 → 0.0.35
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 +173 -33
- package/dist/{TimeTravelOverlay-BlVhj8n1.d.cts → TimeTravelConsole-BLDKZ0U1.d.ts} +11 -9
- package/dist/{TimeTravelOverlay-D8jAd_Fz.d.ts → TimeTravelConsole-zvu8eqTZ.d.cts} +11 -9
- package/dist/adapters/react.cjs +203 -173
- package/dist/adapters/react.d.cts +10 -8
- package/dist/adapters/react.d.ts +10 -8
- package/dist/adapters/react.js +12 -14
- package/dist/adapters/vanilla.cjs +352 -322
- package/dist/adapters/vanilla.d.cts +4 -4
- package/dist/adapters/vanilla.d.ts +4 -4
- package/dist/adapters/vanilla.js +7 -9
- package/dist/{chunk-RI45W4O6.js → chunk-243U74PP.js} +49 -6
- package/dist/{chunk-EZ4VRGYI.js → chunk-2ZYYYSZ4.js} +134 -52
- package/dist/chunk-4S44OX5N.js +288 -0
- package/dist/{chunk-QPJNSYXT.js → chunk-A3ZCYWWM.js} +67 -43
- package/dist/{chunk-6HZSS2TX.js → chunk-OTBKVZ4L.js} +105 -107
- package/dist/index-CuLuFYu3.d.cts +222 -0
- package/dist/index-D_U8Nai1.d.ts +222 -0
- package/dist/{index-CWbDYjby.d.cts → index-rWwvrfdn.d.cts} +446 -358
- package/dist/{index-CWbDYjby.d.ts → index-rWwvrfdn.d.ts} +446 -358
- package/dist/index.cjs +143 -131
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/modules.cjs +654 -287
- package/dist/modules.d.cts +350 -19
- package/dist/modules.d.ts +350 -19
- package/dist/modules.js +391 -113
- package/dist/styles/{time-travel-overlay.css → time-travel-console.css} +72 -32
- package/dist/super.d.ts +802 -487
- package/dist/super.global.js +713 -331
- package/dist/utils.cjs +151 -61
- package/dist/utils.d.cts +17 -11
- package/dist/utils.d.ts +17 -11
- package/dist/utils.js +30 -20
- package/package.json +9 -6
- package/dist/chunk-3OT72G7R.js +0 -39
- package/dist/chunk-UXQ5NJIO.js +0 -274
- package/dist/timeTravel-BsVQ5z7v.d.ts +0 -353
- package/dist/timeTravel-DoWtLH_e.d.cts +0 -353
- package/dist/{chunk-IBAPWB27.js → chunk-JGEI2Q4M.js} +5 -5
package/README.md
CHANGED
|
@@ -50,8 +50,9 @@ const player = reactive({
|
|
|
50
50
|
|
|
51
51
|
// Logic layer (capture phase)
|
|
52
52
|
player.on("intent.playing", (e) => {
|
|
53
|
-
if (!ready) return e.reject();
|
|
53
|
+
if (!ready) return e.reject(); // warning optimistic UI
|
|
54
54
|
player.state.playing = true;
|
|
55
|
+
e.resolve(); // claimed and handled
|
|
55
56
|
}, { capture: true });
|
|
56
57
|
|
|
57
58
|
// UI layer
|
|
@@ -92,7 +93,7 @@ pnpm add sia-reactor
|
|
|
92
93
|
import { reactive, Reactor, TERMINATOR } from "sia-reactor";
|
|
93
94
|
|
|
94
95
|
// 2. Deep Object Utilities
|
|
95
|
-
import {
|
|
96
|
+
import { setPath, getPath, mergeObjs } from "sia-reactor/utils";
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
---
|
|
@@ -103,11 +104,11 @@ import { setAny, getAny, mergeObjs } from "sia-reactor/utils";
|
|
|
103
104
|
|
|
104
105
|
```javascript
|
|
105
106
|
import { reactive, Reactor } from "sia-reactor";
|
|
106
|
-
import "sia-reactor/utils"; // deep object helpers (
|
|
107
|
+
import "sia-reactor/utils"; // deep object helpers (setPath/getPath/deletePath/hasPath/parsePathObj/fanout/force/mergeObjs/deepClone/nuke...) take note of `fanout`!
|
|
107
108
|
import "sia-reactor/modules"; // built-in modules + storage adapters
|
|
108
|
-
import "sia-reactor/adapters/vanilla"; // Autotracker + effect API +
|
|
109
|
+
import "sia-reactor/adapters/vanilla"; // Autotracker + effect API + TimeTravelConsole class
|
|
109
110
|
import "sia-reactor/adapters/react"; // useReactor/useSelector/usePath hooks
|
|
110
|
-
import "sia-reactor/styles/time-travel-
|
|
111
|
+
import "sia-reactor/styles/time-travel-console.css"; // TimeTravelConsole CSS
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
### CDN / Browser (Global)
|
|
@@ -165,7 +166,7 @@ All methods are available on `Reactor` instances or objects wrapped in `reactive
|
|
|
165
166
|
- **`watch(path, callback, options)` <-> `nowatch(path, callback)`**: Fires instantly after a mutation. Use strictly for critical internal engine syncing on leaf paths preferably, sees only direct operations.
|
|
166
167
|
|
|
167
168
|
#### **Listeners (Asynchronous/Batched UI Observers)**
|
|
168
|
-
- **`on(path, callback, options)` <-> `off(path, callback, options)`**: Attach DOM-style event listeners that respect `depth`. Supports `{ capture: true, depth: 1, once: true,
|
|
169
|
+
- **`on(path, callback, options)` <-> `off(path, callback, options)`**: Attach DOM-style event listeners that respect `depth`. Supports `{ capture: true, depth: 1, once: true, init: true }`.
|
|
169
170
|
- **`once(path, callback, options)`**: Fires once and self-destructs, others have too: `sonce(...)`, `gonce(...)`, `donce(...)`, `wonce(...)`.
|
|
170
171
|
|
|
171
172
|
#### **Lifecycle & Utilities**
|
|
@@ -200,22 +201,23 @@ The engine provides native React bindings utilizing `useSyncExternalStore` and a
|
|
|
200
201
|
|
|
201
202
|
```javascript
|
|
202
203
|
import { reactive } from "sia-reactor";
|
|
203
|
-
import { useReactor, useAnyReactor, useSelector, useAnySelector, usePath, effect } from "sia-reactor/adapters/react";
|
|
204
|
+
import { useReactor, useReactorSnapshot, useAnyReactor, useSelector, useSelectorSnapshot, useAnySelector, usePath, effect } from "sia-reactor/adapters/react";
|
|
204
205
|
|
|
205
206
|
const store = reactive({ user: { name: "Kosi", age: 25 }, theme: "dark" });
|
|
206
207
|
|
|
207
208
|
// 1. The Tracked State (Valtio-style)
|
|
208
209
|
function Profile() {
|
|
209
|
-
const sameStore = useReactor(store); // `useReactorSnapshot()` if mutable issues arise
|
|
210
|
+
const sameStore = useReactor(store); // `useReactorSnapshot()` if mutable issues arise, e.g. rare zombie child
|
|
210
211
|
useAnyReactor(); // when you just want state from any reactor
|
|
211
|
-
|
|
212
|
-
return <div>{sameStore.user.name + otherStore.user.name}</div>;
|
|
212
|
+
|
|
213
|
+
return <div>{sameStore.user.name + otherStore.user.name}</div>; // Only re-renders if store.user.name mutates. Completely ignores age and theme!
|
|
213
214
|
} // no snapshots like Valtio, you can read or write to anything
|
|
214
215
|
|
|
215
216
|
// 2. The Slice Selector (Zustand-style)
|
|
216
217
|
function Theme() {
|
|
217
218
|
const theme = useSelector(store, (s) => s.theme); // `useSelectorSnapshot()` if mutable issues arise
|
|
218
219
|
const newName = useAnySelector(() => store.user.name + spouseStore.user.name); // when you just want to derive any state from any reactor
|
|
220
|
+
|
|
219
221
|
return <div>Theme: {theme}</div>;
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -226,15 +228,17 @@ function AgeObserver() {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
// 4. Vanilla Side Effects (Runs anywhere, framework agnostic)
|
|
229
|
-
const stopTracking = effect(() => console.log("User name changed to:", store.user.name)
|
|
231
|
+
const stopTracking = effect(() => console.log("User name changed to:", store.user.name), { sync: false });
|
|
232
|
+
|
|
230
233
|
```
|
|
234
|
+
*NOTE: They support all listener and watcher options with an additional `sync: boolean` to switch between `on` and `watch` behavior.*
|
|
231
235
|
|
|
232
236
|
### Modules: The Extension Port
|
|
233
237
|
|
|
234
|
-
The `Reactor` is designed to be a lightweight core. Extended capabilities are attached via Modules. Use `.attach(target: Reactor | Reactive<T>, id)` to chain reactors then `.setup(target, id)` which the `Reactor.use()` also calls to init, the `id` param will be direct keys on the final object, pass dotted paths to manipulate the shape.
|
|
238
|
+
The `Reactor` is designed to be a lightweight core. Extended capabilities are attached via Modules. Use `.attach(target: Reactor | Reactive<T>, id)` to chain reactors then `.setup(target, id)` which the `Reactor.use()` also calls to init, the `id` param will be direct keys on the final object, pass dotted paths to manipulate the shape. `this` context is preserved for module methods, they're auto-bound.
|
|
235
239
|
|
|
236
240
|
#### The Persistence Module
|
|
237
|
-
Automatically syncs your State to LocalStorage, SessionStorage, Memory or IndexedDB. Always use this module first to avoid re-initialization issues.
|
|
241
|
+
Automatically syncs your State to LocalStorage, SessionStorage, Memory or IndexedDB. Always `use` this module first to avoid re-initialization issues.
|
|
238
242
|
|
|
239
243
|
```javascript
|
|
240
244
|
import { reactive, Reactor, getReactor } from "sia-reactor";
|
|
@@ -248,6 +252,9 @@ const persist = new PersistModule({
|
|
|
248
252
|
throttle: 2500, // ms between saves
|
|
249
253
|
fanout: true, // async hydration use leaf writes to sync initialized listeners.
|
|
250
254
|
adapter: new IndexedDBAdapter({ dbName: "Session", version: 1, onversionchange: () => location.reload(), useSnapshot: true }) // or `LocalStorageAdapter` (instance or signature)
|
|
255
|
+
mirrorReads: true, // intents are requests, listen on their changes but only their state factual mirror is reliable data
|
|
256
|
+
mirrorWrites: true, // states are facts, can be stored but only their intent live mirror is reliable for effects, e.g. hydration
|
|
257
|
+
cachePayload: true // store the initial hydration payload to hydrate late-attaching reactors, free memory with `.clearCache()`
|
|
251
258
|
}, getReactor(store)); // `Reactor` in second arg for path inference
|
|
252
259
|
store.use(persist); // calls `.setup()`, use after all attachments, `id` is the second param too.
|
|
253
260
|
|
|
@@ -258,29 +265,42 @@ persist.config.whitelist = { ui: ["settings.theme"], app: ["settings.volume"] };
|
|
|
258
265
|
```
|
|
259
266
|
|
|
260
267
|
#### The Time Travel Module
|
|
261
|
-
Record state frames, step through history, and optionally attach a ready-to-use vanilla debug overlay. Beware of paradoxes,
|
|
268
|
+
Record state frames, step through history, and optionally attach a ready-to-use vanilla debug overlay. Beware of paradoxes, one timeline is used by default to keep things linear and predictable.
|
|
262
269
|
|
|
263
270
|
```javascript
|
|
264
271
|
import { TimeTravelModule } from "sia-reactor/modules";
|
|
265
|
-
import { effect,
|
|
266
|
-
import "sia-reactor/css/time-travel-
|
|
272
|
+
import { effect, TimeTravelConsole } from "sia-reactor/adapters/vanilla";
|
|
273
|
+
import "sia-reactor/css/time-travel-console.css";
|
|
274
|
+
|
|
275
|
+
const time = new TimeTravelModule({
|
|
276
|
+
limit: 300, // max frames to keep in memory, older frames are dropped
|
|
277
|
+
loop: false, // if true, stepping past the last frame will wrap to the first frame and vice versa
|
|
278
|
+
playbackRate: 150, // multiplier for the delay between events during playback
|
|
279
|
+
whitelist: ["store.playing", "store.currentTime"], // all paths if omitted, use object if multiple reactors
|
|
280
|
+
beforeEntry: composeHeuristics(
|
|
281
|
+
createTxPathMerger(), // O(1) compression of duplicate paths inside transaction envelopes
|
|
282
|
+
createTextBundler({ whitelist: ["store.search.query", "store.profile.bio"] }) // Smart string diffing for text inputs
|
|
283
|
+
) // Intercept and compress history in-flight before it settles!
|
|
284
|
+
});
|
|
285
|
+
store.use(time); // can chain (.use(persist).use(time))
|
|
267
286
|
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
// If persist uses an async adapter (e.g. IndexedDB), stop tracking till after hydration:
|
|
288
|
+
time.untrack(); // immediately after or at creation (new Time...ule.().untrack())
|
|
289
|
+
persist.state.once("hydrated", time.track); // `hydrated` starts `false`, wait until it flips.
|
|
290
|
+
effect(() => persist.state.hydrated && time.track(), { once: true }) // same logic, different look :)
|
|
270
291
|
|
|
271
|
-
//
|
|
272
|
-
persist.state
|
|
273
|
-
effect(() => persist.state.hydrated && store.use(time), { once: true }) // same logic, different look :)
|
|
292
|
+
// Here's a cool trick:
|
|
293
|
+
persist.attach(time.state, "timeTravel.state") // now, history will survive reloads
|
|
274
294
|
|
|
275
|
-
const overlay = new
|
|
295
|
+
const overlay = new TimeTravelConsole(time, { color: "#e26e02", startOpen: false, devOnly: true, container: document.body }); // optional debug interface for visualization
|
|
276
296
|
```
|
|
277
297
|
```jsx
|
|
278
|
-
import {
|
|
298
|
+
import { TimeTravelConsole } from "sia-reactor/adapters/react";
|
|
279
299
|
|
|
280
|
-
<
|
|
300
|
+
<TimeTravelConsole time={time} color="#e26e02" startOpen devOnly /> // react-safe instance lifecycle management, e.g. for HMR predictability.
|
|
281
301
|
```
|
|
282
302
|
|
|
283
|
-
Useful methods: `play()`, `pause()`, `rewind()`, `
|
|
303
|
+
Useful methods: `play()`, `pause()`, `rewind()`, `undo()`, `redo()`, `step(n, forward)`, `jumpTo(frame)`, `track()`, `untrack()`, `clear()`, `export(replacer)`, `import(json, reviver)`.
|
|
284
304
|
|
|
285
305
|
### Reactor Build Options
|
|
286
306
|
|
|
@@ -289,9 +309,11 @@ These are some core build options accepted by `new Reactor(core, build)` and `re
|
|
|
289
309
|
- **`debug?`**: 1-time set. Enables debug logging and diagnostics of core operations. (default: `false`)
|
|
290
310
|
- **`crossRealms?`**: Enables cross-realm object detection support by using slower but safer type checks. (e.g. iframes) (default: `false`).
|
|
291
311
|
- **`smartCloning?`**: Enables structural-sharing snapshot behavior (requires `referenceTracking: true`) (default: `false`).
|
|
292
|
-
- **`eventBubbling?`**: Enables event bubbling across ancestor paths (default: `true`)
|
|
312
|
+
- **`eventBubbling?`**: Enables event bubbling across ancestor paths (default: `true`).
|
|
313
|
+
- **`eventCapturing?`**: Enables event capturing across ancestor paths, `"auto"` is `true` for rejectable events. (default: `"auto"`).
|
|
293
314
|
- **`lineageTracing?`**: Enables path lineage tracing for reference lookups on property access (requires `referenceTracking: true`) (default: `false`).
|
|
294
315
|
- **`preserveContext?`**: Preserves Reflect trap context; safer with ~8x slowdown in hot paths, allows more types to be proxied (e.g. classes) (default: `false`).
|
|
316
|
+
- **`eventTimeStamps?`**: Enables high resolution timestamps on events (default: `false`).
|
|
295
317
|
- **`equalityFunction?`**: Custom equality used by setters and adapter comparisons (default: `Object.is`).
|
|
296
318
|
- **`batchingFunction?`**: Custom batching scheduler for listener notification flushes (default: `queueMicrotask`)
|
|
297
319
|
- **`referenceTracking?`**: Enables identity/reference tracking features in the runtime. (default: `false`).
|
|
@@ -352,8 +374,8 @@ player.on("intent.playing", (e) => {
|
|
|
352
374
|
### Troubleshooting
|
|
353
375
|
|
|
354
376
|
- Listener timing feels late: `on(path, ...)` is microtask-batched by design; use `watch(path, ...)` only for strict immediate engine sync on leaf paths preferably.
|
|
355
|
-
- Listeners don't react to changes: use `fanout(target, object, { depth: n })` instead of direct object sets to keep immutable semantics.
|
|
356
|
-
- `reject()` appears ignored: call it in capture phase and ensure branch is wrapped in `intent(...)`, also remember it's the
|
|
377
|
+
- Listeners don't react to changes: use `fanout(target, object, { depth: n })` instead of direct object sets to keep immutable semantics or `force(() => ...)` to bypass equality checks.
|
|
378
|
+
- `reject()` appears ignored: call it in capture phase and ensure branch is wrapped in `intent(...)`, also remember it's the listeners' choice to comply.
|
|
357
379
|
- Snapshot behavior feels stale: enable `referenceTracking: true` with `smartCloning: true`, also use these when persisting to environments that don't take proxies, e.g. IndexedDB.
|
|
358
380
|
- Cross-frame data is skipped: enable `crossRealms: true` for iframe/other realm objects.
|
|
359
381
|
- Class/prototype behavior is odd: enable `preserveContext: true` (tradeoff: slower hot paths).
|
|
@@ -376,8 +398,8 @@ rtr.set("user.age", (value, terminated, payload) => {
|
|
|
376
398
|
console.log(payload.type); // "set" | "get" | "delete"
|
|
377
399
|
console.log(payload.target); // The exact anatomy of the mutation (see below)
|
|
378
400
|
console.log(payload.root); // Reference to the entire state tree
|
|
379
|
-
console.log(payload.terminated); // Boolean: Did a previous mediator kill this action?
|
|
380
401
|
console.log(payload.rejectable); // Boolean: Is this target wrapped in `intent()`?
|
|
402
|
+
console.log(payload.terminated); // Boolean: Did a previous mediator kill this action?
|
|
381
403
|
}); // you could use external callbacks but typed with `Payload<T, "user.age">`
|
|
382
404
|
rtr.get("user.age", (value, payload) => {});
|
|
383
405
|
rtr.delete("user.age", (terminated, payload) => {});
|
|
@@ -418,7 +440,7 @@ If you mutate `store.user.profile.name = "Kosi"`, the event wave travels like th
|
|
|
418
440
|
2. **Target Phase:** `user.profile.name`
|
|
419
441
|
3. **Bubble Phase:** `user.profile` ➔ `user` ➔ `*` (Root)
|
|
420
442
|
|
|
421
|
-
*NOTE: Only `on` does this since it is batched to stay within recursive limits.*
|
|
443
|
+
*NOTE: Only `on` does this since it is batched to stay within recursive limits. There are `Reactor` options (`eventCapturing`, `eventBubbling`) that toggle phases. See [details](#reactor-build-options) above.*
|
|
422
444
|
|
|
423
445
|
#### The Event Anatomy (`REvent` type)
|
|
424
446
|
Listeners receive a `ReactorEvent` (`REvent`). This object *inherits* everything from the `Payload`, but adds **Political Event Routing**, providing absolute surgical awareness of what is happening in the tree.
|
|
@@ -434,8 +456,11 @@ rtr.on("user.profile", (e) => {
|
|
|
434
456
|
console.log(e.oldValue); // "John" (The previous value)
|
|
435
457
|
// 2. Political Routing
|
|
436
458
|
console.log(e.eventPhase); // 3 (Bubbling Phase)
|
|
437
|
-
console.log(e.bubbles); // true/false
|
|
459
|
+
console.log(e.bubbles); // true/false (configure via eventBubbling option)
|
|
460
|
+
console.log(e.captures); // true/false (configure via eventCapturing option)
|
|
461
|
+
console.log(e.rejectable); // true/false (Is this from an intent() path that can be rejected?)
|
|
438
462
|
// 3. Misc
|
|
463
|
+
console.log(e.timestamp); // 1697059200000 (`DOMHighResTimeStamp`, configurable via `eventTimestamp` option)
|
|
439
464
|
console.log(e.composedPath()); // ["Kosi", { name: "Kosi", age: 26 }, { profile: { name: "Kosi", age: 26 } }, { user: { profile: { name: "Kosi", age: 26 } } }] (refs, target -> root)
|
|
440
465
|
}); // you could use external callbacks but typed with `REvent<T, "user.age">`
|
|
441
466
|
```
|
|
@@ -467,7 +492,7 @@ To help you instantly differentiate between the object *itself* being replaced,
|
|
|
467
492
|
* If `store.user.profile = {}` happens, the listener receives `e.type === "set"`.
|
|
468
493
|
* If `store.user.profile.name = "Kosi"` happens, the parent listener receives `e.type === "update"`.
|
|
469
494
|
|
|
470
|
-
This allows for highly fine-grained syncing bridges across your application without writing heavy for-loop diffing algorithms! Use `{ depth: n }` to control how deep the path bubbles you see are
|
|
495
|
+
This allows for highly fine-grained syncing bridges across your application without writing heavy for-loop diffing algorithms! Use `{ depth: n }` to control how deep the path bubbles you see are:
|
|
471
496
|
|
|
472
497
|
```javascript
|
|
473
498
|
rtr.on("todos", (e) => console.log(e), { depth : 1 }); // only sees updates on direct children
|
|
@@ -486,13 +511,128 @@ rtr.on("todos", (e: REvent<User, "todos", 1>) => {
|
|
|
486
511
|
const { path, key } = e.target;
|
|
487
512
|
console.log(path, key); // or e.target.path, e.target.key
|
|
488
513
|
}
|
|
489
|
-
}, { depth: 1 }); //
|
|
514
|
+
}, { depth: 1 }); // REvent generic is used for external callbacks
|
|
490
515
|
```
|
|
516
|
+
*NOTE: Use with caution, it can be blind to fanouts, e.g during async hydration. Advised for just arrays as theirs default to atomic, i.e. 1-level deep (`.length`). For rare cases, cast `e` to a desired depth in the callback after custom conditions, e.g. `getDepth(e.target.path)` > (getDepth(e.currentTarget.path) + 1).*
|
|
491
517
|
|
|
492
518
|
---
|
|
493
519
|
|
|
494
520
|
## Architectural Tricks
|
|
495
521
|
|
|
522
|
+
### Perks worth Highlighting
|
|
523
|
+
|
|
524
|
+
#### 1. Transactions & Grouping
|
|
525
|
+
By default, every mutation is recorded as a single frame. For UI gestures like dragging a slider, you want to group hundreds of rapid mutations into a single Undo/Redo step for the `TimeTravelModule`.
|
|
526
|
+
|
|
527
|
+
Transactions natively support **deep nesting** and preserve your semantic labels. When paired with `createTxPathMerger`, duplicate paths inside these envelopes are compressed in-flight with zero overhead.
|
|
528
|
+
|
|
529
|
+
```javascript
|
|
530
|
+
import { transaction, startTx, endTx } from "sia-reactor/modules";
|
|
531
|
+
|
|
532
|
+
// Option A: Synchronous Grouping
|
|
533
|
+
transaction(() => {
|
|
534
|
+
store.user.name = "Kosi";
|
|
535
|
+
store.user.age = 19;
|
|
536
|
+
|
|
537
|
+
transaction(() => {
|
|
538
|
+
store.user.pretty = true;
|
|
539
|
+
}, "Countenance Update"); // Deeply nested transactions are isolated and fully supported
|
|
540
|
+
}, "Profile Update"); // All mutations undo/redo as one semantic envelope, fanout uses this internally
|
|
541
|
+
|
|
542
|
+
// Option B: Multi-Tick Gestures (e.g., Slider Drag)
|
|
543
|
+
let tx;
|
|
544
|
+
slider.addEventListener("pointerdown", () => tx = startTx("Volume Scrub"));
|
|
545
|
+
slider.addEventListener("pointerup", () => endTx(tx)); // Thousands of slider mutations happen here, natively compressed by `createTxPathMerger`
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### 2. Heuristics & Text Bundling
|
|
550
|
+
Text inputs generate a massive amount of rapid, noisy history. However, if you are building a **truly deterministic, replayable time machine**, you cannot rely on the browser's isolated history. You must capture the text *and* the cursor state, and bundle rapid keystrokes into semantic, human-readable chunks.
|
|
551
|
+
|
|
552
|
+
The `createTextBundler` heuristic intelligently groups keystrokes and respects word boundaries. You pair this with `setValueWithCursor` to flawlessly sync the DOM. You are essentially building your own browser input physics, skip if using basic controlled or uncontrolled inputs.
|
|
553
|
+
|
|
554
|
+
```javascript
|
|
555
|
+
import { TimeTravelModule } from "sia-reactor/modules";
|
|
556
|
+
import { createTextBundler, setValueWithCursor } from "sia-reactor/modules/timeTravel/heuristics";
|
|
557
|
+
import { effect } from "sia-reactor/adapters/vanilla";
|
|
558
|
+
|
|
559
|
+
// 1. The Setup: State storing [text, selectionStart, selectionEnd, selectionDirection]
|
|
560
|
+
const chatTime = new TimeTravelModule({
|
|
561
|
+
whitelist: ["chatbox"],
|
|
562
|
+
beforeEntry: createTextBundler({
|
|
563
|
+
toString: (v) => v[0], // Extract the string at index [0] so the bundler can intelligently diff the typing
|
|
564
|
+
throttle: 700, // max delay between edits
|
|
565
|
+
boundaryRegex: /[\s.,!?;:\n()[\]{}'"`]/, // chars considered "hard boundaries"
|
|
566
|
+
maxGrowth: 48, // prevents giant paragraph merging
|
|
567
|
+
bundleInserts: true, // `true` for chat apps; `false` for code editors
|
|
568
|
+
bundleDeletes: true, // `true` for most use cases
|
|
569
|
+
strictMerges: true, // typing -> deleting -> typing becomes separate frames
|
|
570
|
+
})
|
|
571
|
+
});
|
|
572
|
+
store.use(chatTime);
|
|
573
|
+
|
|
574
|
+
// 2. The UI Integration (React Example)
|
|
575
|
+
import { keyEventAllowed } from "sia-reactor/utils";
|
|
576
|
+
|
|
577
|
+
function ChatInput() {
|
|
578
|
+
const s = useReactor(store);
|
|
579
|
+
const ref = useRef(null);
|
|
580
|
+
const keySettings = { overrides: ["ctrl+z", "meta+z"], shortcuts: { undo: "ctrl+z", redo: ["ctrl+y", "ctrl+shift+z", "meta+shift+z"] }, blocks: ["ctrl+shift+r"] }; // more info in editor tooltips
|
|
581
|
+
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
return effect(() => {
|
|
584
|
+
const [text, start, end, dir] = store.chatbox; // works perfectly during playback as module updates the store.
|
|
585
|
+
setValueWithCursor(ref.current, text, start, end, dir);
|
|
586
|
+
}); // it's not React's state hence our `effect` API, cleans up on unmount
|
|
587
|
+
}, []);
|
|
588
|
+
|
|
589
|
+
return (
|
|
590
|
+
<textarea
|
|
591
|
+
ref={ref}
|
|
592
|
+
defaultValue={s.chatbox[0]}
|
|
593
|
+
onInput={(e) => {
|
|
594
|
+
const t = e.target;
|
|
595
|
+
s.chatbox = [t.value, t.selectionStart, t.selectionEnd, t.selectionDirection]; // Save the raw text alongside the user's exact cursor selection bounds
|
|
596
|
+
}}
|
|
597
|
+
onKeyDown={(e) => {
|
|
598
|
+
const action = keyEventAllowed(e, keySettings); // utility to interpret key combos according to your settings
|
|
599
|
+
action === "undo" ? chatTime.undo() : action === "redo" && chatTime.redo(); // trigger time travel actions accordingly.
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
602
|
+
);
|
|
603
|
+
} // This is how you take over the browser
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### 3. The Meta Context
|
|
608
|
+
|
|
609
|
+
S.I.A. Reactor features a high-performance, synchronous meta-context engine. It allows you to inject temporary data (like flags or transaction IDs) into the event loop without polluting your core state even without function signatures.
|
|
610
|
+
|
|
611
|
+
```javascript
|
|
612
|
+
import { withMeta } from "sia-reactor/utils";
|
|
613
|
+
import { silence } from "sia-reactor/modules";
|
|
614
|
+
|
|
615
|
+
// 1. The Silence Wrapper: Executes mutations that the TimeTravelModule will not record. same as withMeta({ silent: true }, () => ...)
|
|
616
|
+
silence(() => {
|
|
617
|
+
player.state.volume = 100;
|
|
618
|
+
}); // Useful for internal side-effects that shouldn't ruin the undo/redo history.
|
|
619
|
+
|
|
620
|
+
// 2. Custom Meta Injection: Injects data seamlessly into the ReactorEvent payload for listeners to read.
|
|
621
|
+
withMeta({ customSource: "gamepad" }, () => {
|
|
622
|
+
player.intent.playing = true;
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
player.on("intent.playing", (e) => {
|
|
626
|
+
console.log(e.customSource); // "gamepad"
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
declare module "sia-reactor" {
|
|
630
|
+
interface ReactorEventMeta {
|
|
631
|
+
customSource?: "gamepad" | "api" | string;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
496
636
|
### The CSS Black Box
|
|
497
637
|
|
|
498
638
|
Imagine you have 50 different CSS variables in your state (`settings.css.containerWidth`, `settings.css.themeColor`, etc.). Registering 50 individual `watch()` or `on()` listeners would need manual css crawling that will be blind to dynamically added variables.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { R as Reactive } from './index-rWwvrfdn.js';
|
|
2
|
+
import { T as TimeTravelModule } from './index-D_U8Nai1.js';
|
|
3
3
|
|
|
4
4
|
/** Reactive options for the TimeTravel overlay instance. */
|
|
5
|
-
interface
|
|
5
|
+
interface TimeTravelConsoleConfig {
|
|
6
6
|
/** Header text shown at the top of the overlay panel. */
|
|
7
7
|
title: string;
|
|
8
8
|
/** Accent color used to derive panel theme variables. */
|
|
@@ -19,24 +19,26 @@ interface TimeTravelOverlayConfig {
|
|
|
19
19
|
* - Mounts a docked HUD into the configured container, syncs its UI with module state, and forwards keyboard/button actions to the TimeTravelModule.
|
|
20
20
|
* Supports reactive `config` updates (title/color/container/devOnly) and maintains local overlay UI state (`open` and `import` payload text).
|
|
21
21
|
*/
|
|
22
|
-
declare class
|
|
22
|
+
declare class TimeTravelConsole {
|
|
23
23
|
static count: number;
|
|
24
24
|
index: number;
|
|
25
|
-
config:
|
|
25
|
+
config: Reactive<TimeTravelConsoleConfig>;
|
|
26
26
|
readonly state: Reactive<{
|
|
27
27
|
open: boolean;
|
|
28
28
|
import: string;
|
|
29
|
+
stride: number;
|
|
29
30
|
}, undefined>;
|
|
30
31
|
readonly time: TimeTravelModule;
|
|
31
|
-
readonly
|
|
32
|
+
readonly host: HTMLElement;
|
|
32
33
|
private clups;
|
|
33
|
-
private
|
|
34
|
+
private keydown;
|
|
35
|
+
private keyup;
|
|
34
36
|
/** Creates a docked TimeTravel overlay bound to a module instance.
|
|
35
37
|
* @param time TimeTravel module instance that owns timeline operations.
|
|
36
38
|
* @param build Optional initial overlay config overrides.
|
|
37
39
|
*/
|
|
38
|
-
constructor(time: TimeTravelModule, build?: Partial<
|
|
40
|
+
constructor(time: TimeTravelModule, build?: Partial<TimeTravelConsoleConfig>);
|
|
39
41
|
destroy(): void;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export {
|
|
44
|
+
export { TimeTravelConsole as T, type TimeTravelConsoleConfig as a };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { R as Reactive } from './index-rWwvrfdn.cjs';
|
|
2
|
+
import { T as TimeTravelModule } from './index-CuLuFYu3.cjs';
|
|
3
3
|
|
|
4
4
|
/** Reactive options for the TimeTravel overlay instance. */
|
|
5
|
-
interface
|
|
5
|
+
interface TimeTravelConsoleConfig {
|
|
6
6
|
/** Header text shown at the top of the overlay panel. */
|
|
7
7
|
title: string;
|
|
8
8
|
/** Accent color used to derive panel theme variables. */
|
|
@@ -19,24 +19,26 @@ interface TimeTravelOverlayConfig {
|
|
|
19
19
|
* - Mounts a docked HUD into the configured container, syncs its UI with module state, and forwards keyboard/button actions to the TimeTravelModule.
|
|
20
20
|
* Supports reactive `config` updates (title/color/container/devOnly) and maintains local overlay UI state (`open` and `import` payload text).
|
|
21
21
|
*/
|
|
22
|
-
declare class
|
|
22
|
+
declare class TimeTravelConsole {
|
|
23
23
|
static count: number;
|
|
24
24
|
index: number;
|
|
25
|
-
config:
|
|
25
|
+
config: Reactive<TimeTravelConsoleConfig>;
|
|
26
26
|
readonly state: Reactive<{
|
|
27
27
|
open: boolean;
|
|
28
28
|
import: string;
|
|
29
|
+
stride: number;
|
|
29
30
|
}, undefined>;
|
|
30
31
|
readonly time: TimeTravelModule;
|
|
31
|
-
readonly
|
|
32
|
+
readonly host: HTMLElement;
|
|
32
33
|
private clups;
|
|
33
|
-
private
|
|
34
|
+
private keydown;
|
|
35
|
+
private keyup;
|
|
34
36
|
/** Creates a docked TimeTravel overlay bound to a module instance.
|
|
35
37
|
* @param time TimeTravel module instance that owns timeline operations.
|
|
36
38
|
* @param build Optional initial overlay config overrides.
|
|
37
39
|
*/
|
|
38
|
-
constructor(time: TimeTravelModule, build?: Partial<
|
|
40
|
+
constructor(time: TimeTravelModule, build?: Partial<TimeTravelConsoleConfig>);
|
|
39
41
|
destroy(): void;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export {
|
|
44
|
+
export { TimeTravelConsole as T, type TimeTravelConsoleConfig as a };
|