sia-reactor 0.0.34 → 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 +170 -31
- package/dist/{TimeTravelOverlay-DTqM5uJB.d.cts → TimeTravelConsole-BLDKZ0U1.d.ts} +11 -9
- package/dist/{TimeTravelOverlay-cSlHzoKB.d.ts → TimeTravelConsole-zvu8eqTZ.d.cts} +11 -9
- package/dist/adapters/react.cjs +196 -164
- package/dist/adapters/react.d.cts +10 -8
- package/dist/adapters/react.d.ts +10 -8
- package/dist/adapters/react.js +10 -12
- package/dist/adapters/vanilla.cjs +346 -314
- 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-4PLMBUCP.js → chunk-2ZYYYSZ4.js} +122 -36
- package/dist/chunk-4S44OX5N.js +288 -0
- package/dist/{chunk-VIDZLTP2.js → chunk-A3ZCYWWM.js} +67 -43
- package/dist/{chunk-T2CAL5F4.js → chunk-OTBKVZ4L.js} +99 -103
- package/dist/index-CuLuFYu3.d.cts +222 -0
- package/dist/index-D_U8Nai1.d.ts +222 -0
- package/dist/{index-0TjDsae1.d.cts → index-rWwvrfdn.d.cts} +436 -348
- package/dist/{index-0TjDsae1.d.ts → index-rWwvrfdn.d.ts} +436 -348
- package/dist/index.cjs +137 -123
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/modules.cjs +643 -278
- package/dist/modules.d.cts +350 -19
- package/dist/modules.d.ts +350 -19
- package/dist/modules.js +386 -112
- package/dist/styles/{time-travel-overlay.css → time-travel-console.css} +72 -32
- package/dist/super.d.ts +787 -472
- package/dist/super.global.js +696 -316
- package/dist/utils.cjs +134 -40
- package/dist/utils.d.cts +17 -11
- package/dist/utils.d.ts +17 -11
- package/dist/utils.js +21 -11
- package/package.json +7 -4
- package/dist/chunk-3OT72G7R.js +0 -39
- package/dist/chunk-CEPDD5XN.js +0 -274
- package/dist/timeTravel-CX100S8f.d.ts +0 -353
- package/dist/timeTravel-wU23uudD.d.cts +0 -353
- package/dist/{chunk-IBAPWB27.js → chunk-JGEI2Q4M.js} +5 -5
package/README.md
CHANGED
|
@@ -104,11 +104,11 @@ import { setPath, getPath, mergeObjs } from "sia-reactor/utils";
|
|
|
104
104
|
|
|
105
105
|
```javascript
|
|
106
106
|
import { reactive, Reactor } from "sia-reactor";
|
|
107
|
-
import "sia-reactor/utils"; // deep object helpers (setPath/getPath/deletePath/hasPath/parsePathObj/fanout/mergeObjs/deepClone/nuke...) take note of `fanout`!
|
|
107
|
+
import "sia-reactor/utils"; // deep object helpers (setPath/getPath/deletePath/hasPath/parsePathObj/fanout/force/mergeObjs/deepClone/nuke...) take note of `fanout`!
|
|
108
108
|
import "sia-reactor/modules"; // built-in modules + storage adapters
|
|
109
|
-
import "sia-reactor/adapters/vanilla"; // Autotracker + effect API +
|
|
109
|
+
import "sia-reactor/adapters/vanilla"; // Autotracker + effect API + TimeTravelConsole class
|
|
110
110
|
import "sia-reactor/adapters/react"; // useReactor/useSelector/usePath hooks
|
|
111
|
-
import "sia-reactor/styles/time-travel-
|
|
111
|
+
import "sia-reactor/styles/time-travel-console.css"; // TimeTravelConsole CSS
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
### CDN / Browser (Global)
|
|
@@ -166,7 +166,7 @@ All methods are available on `Reactor` instances or objects wrapped in `reactive
|
|
|
166
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.
|
|
167
167
|
|
|
168
168
|
#### **Listeners (Asynchronous/Batched UI Observers)**
|
|
169
|
-
- **`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 }`.
|
|
170
170
|
- **`once(path, callback, options)`**: Fires once and self-destructs, others have too: `sonce(...)`, `gonce(...)`, `donce(...)`, `wonce(...)`.
|
|
171
171
|
|
|
172
172
|
#### **Lifecycle & Utilities**
|
|
@@ -201,22 +201,23 @@ The engine provides native React bindings utilizing `useSyncExternalStore` and a
|
|
|
201
201
|
|
|
202
202
|
```javascript
|
|
203
203
|
import { reactive } from "sia-reactor";
|
|
204
|
-
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";
|
|
205
205
|
|
|
206
206
|
const store = reactive({ user: { name: "Kosi", age: 25 }, theme: "dark" });
|
|
207
207
|
|
|
208
208
|
// 1. The Tracked State (Valtio-style)
|
|
209
209
|
function Profile() {
|
|
210
|
-
const sameStore = useReactor(store); // `useReactorSnapshot()` if mutable issues arise
|
|
210
|
+
const sameStore = useReactor(store); // `useReactorSnapshot()` if mutable issues arise, e.g. rare zombie child
|
|
211
211
|
useAnyReactor(); // when you just want state from any reactor
|
|
212
|
-
|
|
213
|
-
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!
|
|
214
214
|
} // no snapshots like Valtio, you can read or write to anything
|
|
215
215
|
|
|
216
216
|
// 2. The Slice Selector (Zustand-style)
|
|
217
217
|
function Theme() {
|
|
218
218
|
const theme = useSelector(store, (s) => s.theme); // `useSelectorSnapshot()` if mutable issues arise
|
|
219
219
|
const newName = useAnySelector(() => store.user.name + spouseStore.user.name); // when you just want to derive any state from any reactor
|
|
220
|
+
|
|
220
221
|
return <div>Theme: {theme}</div>;
|
|
221
222
|
}
|
|
222
223
|
|
|
@@ -227,15 +228,17 @@ function AgeObserver() {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// 4. Vanilla Side Effects (Runs anywhere, framework agnostic)
|
|
230
|
-
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
|
+
|
|
231
233
|
```
|
|
234
|
+
*NOTE: They support all listener and watcher options with an additional `sync: boolean` to switch between `on` and `watch` behavior.*
|
|
232
235
|
|
|
233
236
|
### Modules: The Extension Port
|
|
234
237
|
|
|
235
|
-
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.
|
|
236
239
|
|
|
237
240
|
#### The Persistence Module
|
|
238
|
-
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.
|
|
239
242
|
|
|
240
243
|
```javascript
|
|
241
244
|
import { reactive, Reactor, getReactor } from "sia-reactor";
|
|
@@ -249,6 +252,9 @@ const persist = new PersistModule({
|
|
|
249
252
|
throttle: 2500, // ms between saves
|
|
250
253
|
fanout: true, // async hydration use leaf writes to sync initialized listeners.
|
|
251
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()`
|
|
252
258
|
}, getReactor(store)); // `Reactor` in second arg for path inference
|
|
253
259
|
store.use(persist); // calls `.setup()`, use after all attachments, `id` is the second param too.
|
|
254
260
|
|
|
@@ -259,29 +265,42 @@ persist.config.whitelist = { ui: ["settings.theme"], app: ["settings.volume"] };
|
|
|
259
265
|
```
|
|
260
266
|
|
|
261
267
|
#### The Time Travel Module
|
|
262
|
-
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.
|
|
263
269
|
|
|
264
270
|
```javascript
|
|
265
271
|
import { TimeTravelModule } from "sia-reactor/modules";
|
|
266
|
-
import { effect,
|
|
267
|
-
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))
|
|
268
286
|
|
|
269
|
-
|
|
270
|
-
|
|
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 :)
|
|
271
291
|
|
|
272
|
-
//
|
|
273
|
-
persist.state
|
|
274
|
-
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
|
|
275
294
|
|
|
276
|
-
const overlay = new
|
|
295
|
+
const overlay = new TimeTravelConsole(time, { color: "#e26e02", startOpen: false, devOnly: true, container: document.body }); // optional debug interface for visualization
|
|
277
296
|
```
|
|
278
297
|
```jsx
|
|
279
|
-
import {
|
|
298
|
+
import { TimeTravelConsole } from "sia-reactor/adapters/react";
|
|
280
299
|
|
|
281
|
-
<
|
|
300
|
+
<TimeTravelConsole time={time} color="#e26e02" startOpen devOnly /> // react-safe instance lifecycle management, e.g. for HMR predictability.
|
|
282
301
|
```
|
|
283
302
|
|
|
284
|
-
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)`.
|
|
285
304
|
|
|
286
305
|
### Reactor Build Options
|
|
287
306
|
|
|
@@ -290,9 +309,11 @@ These are some core build options accepted by `new Reactor(core, build)` and `re
|
|
|
290
309
|
- **`debug?`**: 1-time set. Enables debug logging and diagnostics of core operations. (default: `false`)
|
|
291
310
|
- **`crossRealms?`**: Enables cross-realm object detection support by using slower but safer type checks. (e.g. iframes) (default: `false`).
|
|
292
311
|
- **`smartCloning?`**: Enables structural-sharing snapshot behavior (requires `referenceTracking: true`) (default: `false`).
|
|
293
|
-
- **`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"`).
|
|
294
314
|
- **`lineageTracing?`**: Enables path lineage tracing for reference lookups on property access (requires `referenceTracking: true`) (default: `false`).
|
|
295
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`).
|
|
296
317
|
- **`equalityFunction?`**: Custom equality used by setters and adapter comparisons (default: `Object.is`).
|
|
297
318
|
- **`batchingFunction?`**: Custom batching scheduler for listener notification flushes (default: `queueMicrotask`)
|
|
298
319
|
- **`referenceTracking?`**: Enables identity/reference tracking features in the runtime. (default: `false`).
|
|
@@ -353,8 +374,8 @@ player.on("intent.playing", (e) => {
|
|
|
353
374
|
### Troubleshooting
|
|
354
375
|
|
|
355
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.
|
|
356
|
-
- Listeners don't react to changes: use `fanout(target, object, { depth: n })` instead of direct object sets to keep immutable semantics.
|
|
357
|
-
- `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.
|
|
358
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.
|
|
359
380
|
- Cross-frame data is skipped: enable `crossRealms: true` for iframe/other realm objects.
|
|
360
381
|
- Class/prototype behavior is odd: enable `preserveContext: true` (tradeoff: slower hot paths).
|
|
@@ -377,8 +398,8 @@ rtr.set("user.age", (value, terminated, payload) => {
|
|
|
377
398
|
console.log(payload.type); // "set" | "get" | "delete"
|
|
378
399
|
console.log(payload.target); // The exact anatomy of the mutation (see below)
|
|
379
400
|
console.log(payload.root); // Reference to the entire state tree
|
|
380
|
-
console.log(payload.terminated); // Boolean: Did a previous mediator kill this action?
|
|
381
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?
|
|
382
403
|
}); // you could use external callbacks but typed with `Payload<T, "user.age">`
|
|
383
404
|
rtr.get("user.age", (value, payload) => {});
|
|
384
405
|
rtr.delete("user.age", (terminated, payload) => {});
|
|
@@ -419,7 +440,7 @@ If you mutate `store.user.profile.name = "Kosi"`, the event wave travels like th
|
|
|
419
440
|
2. **Target Phase:** `user.profile.name`
|
|
420
441
|
3. **Bubble Phase:** `user.profile` ➔ `user` ➔ `*` (Root)
|
|
421
442
|
|
|
422
|
-
*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.*
|
|
423
444
|
|
|
424
445
|
#### The Event Anatomy (`REvent` type)
|
|
425
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.
|
|
@@ -435,8 +456,11 @@ rtr.on("user.profile", (e) => {
|
|
|
435
456
|
console.log(e.oldValue); // "John" (The previous value)
|
|
436
457
|
// 2. Political Routing
|
|
437
458
|
console.log(e.eventPhase); // 3 (Bubbling Phase)
|
|
438
|
-
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?)
|
|
439
462
|
// 3. Misc
|
|
463
|
+
console.log(e.timestamp); // 1697059200000 (`DOMHighResTimeStamp`, configurable via `eventTimestamp` option)
|
|
440
464
|
console.log(e.composedPath()); // ["Kosi", { name: "Kosi", age: 26 }, { profile: { name: "Kosi", age: 26 } }, { user: { profile: { name: "Kosi", age: 26 } } }] (refs, target -> root)
|
|
441
465
|
}); // you could use external callbacks but typed with `REvent<T, "user.age">`
|
|
442
466
|
```
|
|
@@ -468,7 +492,7 @@ To help you instantly differentiate between the object *itself* being replaced,
|
|
|
468
492
|
* If `store.user.profile = {}` happens, the listener receives `e.type === "set"`.
|
|
469
493
|
* If `store.user.profile.name = "Kosi"` happens, the parent listener receives `e.type === "update"`.
|
|
470
494
|
|
|
471
|
-
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:
|
|
472
496
|
|
|
473
497
|
```javascript
|
|
474
498
|
rtr.on("todos", (e) => console.log(e), { depth : 1 }); // only sees updates on direct children
|
|
@@ -487,13 +511,128 @@ rtr.on("todos", (e: REvent<User, "todos", 1>) => {
|
|
|
487
511
|
const { path, key } = e.target;
|
|
488
512
|
console.log(path, key); // or e.target.path, e.target.key
|
|
489
513
|
}
|
|
490
|
-
}, { depth: 1 }); //
|
|
514
|
+
}, { depth: 1 }); // REvent generic is used for external callbacks
|
|
491
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).*
|
|
492
517
|
|
|
493
518
|
---
|
|
494
519
|
|
|
495
520
|
## Architectural Tricks
|
|
496
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
|
+
|
|
497
636
|
### The CSS Black Box
|
|
498
637
|
|
|
499
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 };
|