teamplay 0.4.0-alpha.30 → 0.4.0-alpha.32
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/orm/Compat/README.md
CHANGED
|
@@ -116,6 +116,8 @@ $.users.user1.scope('users', 'user2')
|
|
|
116
116
|
Creates a lightweight alias between signals (minimal Racer-style ref).
|
|
117
117
|
Mutations on the alias are forwarded to the target. The alias mirrors target updates.
|
|
118
118
|
Reads (`get`/`peek`) are forwarded to the target while the ref is active.
|
|
119
|
+
Ref mirroring is scheduled through Teamplay runtime scheduler, so updates remain batch-friendly
|
|
120
|
+
and do not leak intermediate ref states during a single batched cycle.
|
|
119
121
|
|
|
120
122
|
```js
|
|
121
123
|
const $local = $.local.value
|
|
@@ -199,7 +201,8 @@ $root.start('_virtual.lesson', $.lessons[lessonId], '_session.userId', (lesson,
|
|
|
199
201
|
|
|
200
202
|
Behavior:
|
|
201
203
|
- Calling `start()` again for the same `targetPath` replaces previous reaction.
|
|
202
|
-
- `undefined`
|
|
204
|
+
- `undefined` result applies compat delete semantics at target path.
|
|
205
|
+
- `null` result is stored as `null`.
|
|
203
206
|
- Returns target signal.
|
|
204
207
|
- If any dependency temporarily suspends (throws a Promise/thenable), compat skips the whole tick (getter is not called and target is not written).
|
|
205
208
|
- If `getter` throws a Promise, compat skips that tick and retries on next reactive update.
|
|
@@ -365,6 +368,7 @@ Compatibility mode intentionally aligns mutators with Racer. This differs from c
|
|
|
365
368
|
| `setDiff` | N/A as compat shim. | Alias to compat `set` for both signatures: `setDiff(value)` and `setDiff(path, value)`. |
|
|
366
369
|
|
|
367
370
|
Migration note: compat behavior is intentionally Racer-aligned and may differ from core mutators.
|
|
371
|
+
Composite compat mutators (`setEach`, `setDiffDeep`) apply updates atomically for Teamplay-scheduled observers via the runtime batch scheduler.
|
|
368
372
|
|
|
369
373
|
### set(value) and set(path, value)
|
|
370
374
|
|
|
@@ -40,6 +40,7 @@ import { on as onCustomEvent, removeListener as removeCustomEventListener } from
|
|
|
40
40
|
import { normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
|
|
41
41
|
import { setRefLink, removeRefLink } from './refRegistry.js'
|
|
42
42
|
import { REF_TARGET, resolveRefSignalSafe } from './refFallback.js'
|
|
43
|
+
import { runInBatch, scheduleReaction } from '../batchScheduler.js'
|
|
43
44
|
|
|
44
45
|
class SignalCompat extends Signal {
|
|
45
46
|
static ID_FIELDS = ['_id', 'id']
|
|
@@ -225,7 +226,7 @@ class SignalCompat extends Signal {
|
|
|
225
226
|
value = path
|
|
226
227
|
}
|
|
227
228
|
const $target = resolveSignal(this, segments)
|
|
228
|
-
return setDiffDeepOnSignal($target, value)
|
|
229
|
+
return runInBatch(() => setDiffDeepOnSignal($target, value))
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
async setDiff (path, value) {
|
|
@@ -253,11 +254,13 @@ class SignalCompat extends Signal {
|
|
|
253
254
|
if (typeof object !== 'object') {
|
|
254
255
|
throw Error('Signal.setEach() expects an object argument, got: ' + typeof object)
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
return runInBatch(async () => {
|
|
258
|
+
const promises = []
|
|
259
|
+
for (const key of Object.keys(object)) {
|
|
260
|
+
promises.push(SignalCompat.prototype.set.call($target[key], object[key]))
|
|
261
|
+
}
|
|
262
|
+
await Promise.all(promises)
|
|
263
|
+
})
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
async del (path) {
|
|
@@ -574,6 +577,7 @@ class SignalCompat extends Signal {
|
|
|
574
577
|
}
|
|
575
578
|
|
|
576
579
|
const REFS = Symbol('compat refs')
|
|
580
|
+
const SKIP_REF_TICK = Symbol('compat ref skip tick')
|
|
577
581
|
|
|
578
582
|
function getRefStore ($signal) {
|
|
579
583
|
const $root = getRoot($signal) || $signal
|
|
@@ -583,15 +587,25 @@ function getRefStore ($signal) {
|
|
|
583
587
|
|
|
584
588
|
function createRefLink ($from, $to) {
|
|
585
589
|
const toReaction = observe(() => {
|
|
586
|
-
const value = $to
|
|
590
|
+
const value = readRefValue($to)
|
|
591
|
+
if (value === SKIP_REF_TICK) return
|
|
587
592
|
trackDeep(value)
|
|
588
593
|
setDiffDeepBypassRef($from, deepCopy(value))
|
|
589
|
-
})
|
|
594
|
+
}, { scheduler: scheduleReaction })
|
|
590
595
|
return () => {
|
|
591
596
|
unobserve(toReaction)
|
|
592
597
|
}
|
|
593
598
|
}
|
|
594
599
|
|
|
600
|
+
function readRefValue ($signal) {
|
|
601
|
+
try {
|
|
602
|
+
return $signal.get()
|
|
603
|
+
} catch (err) {
|
|
604
|
+
if (isThenable(err)) return SKIP_REF_TICK
|
|
605
|
+
throw err
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
595
609
|
function trackDeep (value, seen = new Set()) {
|
|
596
610
|
if (!value || typeof value !== 'object') return
|
|
597
611
|
if (seen.has(value)) return
|
|
@@ -629,6 +643,10 @@ function isReactLike (value) {
|
|
|
629
643
|
return !!(value && typeof value === 'object' && typeof value.$$typeof === 'symbol')
|
|
630
644
|
}
|
|
631
645
|
|
|
646
|
+
function isThenable (value) {
|
|
647
|
+
return !!value && typeof value.then === 'function'
|
|
648
|
+
}
|
|
649
|
+
|
|
632
650
|
function resolveRefTarget ($signal, target, methodName) {
|
|
633
651
|
if (isSignalLike(target)) return target
|
|
634
652
|
if (typeof target === 'string') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { observe, unobserve } from '@nx-js/observer-util'
|
|
2
2
|
import { getRoot } from '../Root.js'
|
|
3
|
+
import { scheduleReaction } from '../batchScheduler.js'
|
|
3
4
|
|
|
4
5
|
const START_REACTIONS = Symbol('compat start reactions')
|
|
5
6
|
const SKIP_TICK = Symbol('compat start skip tick')
|
|
@@ -39,7 +40,7 @@ export function compatStartOnRoot ($root, targetPath, ...depsAndGetter) {
|
|
|
39
40
|
}
|
|
40
41
|
const maybePromise = $target.set(nextValue)
|
|
41
42
|
if (maybePromise?.catch) maybePromise.catch(ignorePromiseRejection)
|
|
42
|
-
})
|
|
43
|
+
}, { scheduler: scheduleReaction })
|
|
43
44
|
store.set(targetKey, { stop: () => unobserve(reaction) })
|
|
44
45
|
return $target
|
|
45
46
|
}
|
package/orm/Reaction.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SEGMENTS } from './Signal.js'
|
|
|
3
3
|
import { set as _set, del as _del } from './dataTree.js'
|
|
4
4
|
import { LOCAL } from './Value.js'
|
|
5
5
|
import FinalizationRegistry from '../utils/MockFinalizationRegistry.js'
|
|
6
|
+
import { scheduleReaction } from './batchScheduler.js'
|
|
6
7
|
|
|
7
8
|
// this is `let` to be able to directly change it if needed in tests or in the app
|
|
8
9
|
export let DELETION_DELAY = 0 // eslint-disable-line prefer-const
|
|
@@ -18,7 +19,7 @@ class ReactionSubscriptions {
|
|
|
18
19
|
if (this.initialized.has(id)) return
|
|
19
20
|
|
|
20
21
|
this.initialized.set(id, true)
|
|
21
|
-
const reactionScheduler = reaction => runReaction(id, reaction)
|
|
22
|
+
const reactionScheduler = reaction => scheduleReaction(() => runReaction(id, reaction))
|
|
22
23
|
const reaction = observe(fn, { lazy: true, scheduler: reactionScheduler })
|
|
23
24
|
this.fr.register($value, [id, reaction])
|
|
24
25
|
runReaction(id, reaction)
|
package/orm/SignalBase.js
CHANGED
|
@@ -49,6 +49,7 @@ import { DEFAULT_ID_FIELDS, getIdFieldsForSegments, isIdFieldPath, normalizeIdFi
|
|
|
49
49
|
import { isCompatEnv } from './compatEnv.js'
|
|
50
50
|
import { resolveRefSegmentsSafe, resolveRefSignalSafe } from './Compat/refFallback.js'
|
|
51
51
|
import { compatStartOnRoot, compatStopOnRoot, joinScopePath } from './Compat/startStopCompat.js'
|
|
52
|
+
import { runInBatch } from './batchScheduler.js'
|
|
52
53
|
|
|
53
54
|
export const SEGMENTS = Symbol('path segments targeting the particular node in the data tree')
|
|
54
55
|
export const ARRAY_METHOD = Symbol('run array method on the signal')
|
|
@@ -105,7 +106,7 @@ export class Signal extends Function {
|
|
|
105
106
|
if (arguments.length > 1) throw Error('Signal.batch() expects a single argument')
|
|
106
107
|
if (fn == null) return
|
|
107
108
|
if (typeof fn !== 'function') throw Error('Signal.batch() expects a function argument')
|
|
108
|
-
return fn
|
|
109
|
+
return runInBatch(fn)
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
[GET] (method) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
let batchDepth = 0
|
|
2
|
+
let isFlushing = false
|
|
3
|
+
const queuedReactions = new Set()
|
|
4
|
+
|
|
5
|
+
export function beginBatch () {
|
|
6
|
+
batchDepth += 1
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function endBatch () {
|
|
10
|
+
if (batchDepth === 0) return
|
|
11
|
+
batchDepth -= 1
|
|
12
|
+
if (batchDepth === 0) flushReactions()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function inBatch () {
|
|
16
|
+
return batchDepth > 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function runInBatch (fn) {
|
|
20
|
+
beginBatch()
|
|
21
|
+
let result
|
|
22
|
+
try {
|
|
23
|
+
result = fn()
|
|
24
|
+
} catch (err) {
|
|
25
|
+
endBatch()
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
if (result?.then) {
|
|
29
|
+
return Promise.resolve(result).finally(endBatch)
|
|
30
|
+
}
|
|
31
|
+
endBatch()
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function scheduleReaction (reactionFn) {
|
|
36
|
+
if (typeof reactionFn !== 'function') return
|
|
37
|
+
if (inBatch() || isFlushing) {
|
|
38
|
+
queuedReactions.add(reactionFn)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
reactionFn()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function flushReactions () {
|
|
45
|
+
if (isFlushing) return
|
|
46
|
+
isFlushing = true
|
|
47
|
+
try {
|
|
48
|
+
while (queuedReactions.size > 0) {
|
|
49
|
+
const queue = Array.from(queuedReactions)
|
|
50
|
+
queuedReactions.clear()
|
|
51
|
+
for (const reactionFn of queue) reactionFn()
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
isFlushing = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function __resetBatchSchedulerForTests () {
|
|
59
|
+
batchDepth = 0
|
|
60
|
+
isFlushing = false
|
|
61
|
+
queuedReactions.clear()
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.32",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
]
|
|
82
82
|
},
|
|
83
83
|
"license": "MIT",
|
|
84
|
-
"gitHead": "
|
|
84
|
+
"gitHead": "384c14383a45af21bad2f752011db1462f7d8628"
|
|
85
85
|
}
|
|
@@ -6,6 +6,7 @@ import { __increment, __decrement } from '@teamplay/debug'
|
|
|
6
6
|
import executionContextTracker from './executionContextTracker.js'
|
|
7
7
|
import { pipeComponentMeta, useUnmount, useId, useTriggerUpdate } from './helpers.js'
|
|
8
8
|
import trapRender from './trapRender.js'
|
|
9
|
+
import { scheduleReaction } from '../orm/batchScheduler.js'
|
|
9
10
|
|
|
10
11
|
const DEFAULT_THROTTLE_TIMEOUT = 100
|
|
11
12
|
|
|
@@ -52,7 +53,7 @@ export default function convertToObserver (BaseComponent, {
|
|
|
52
53
|
componentId
|
|
53
54
|
})
|
|
54
55
|
reactionRef.current = observe(trappedRender, {
|
|
55
|
-
scheduler: update,
|
|
56
|
+
scheduler: () => scheduleReaction(update),
|
|
56
57
|
lazy: true
|
|
57
58
|
})
|
|
58
59
|
}
|