teamplay 0.4.0-alpha.96 → 0.4.0-alpha.98
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 +4 -1
- package/orm/Compat/SignalCompat.js +33 -5
- package/orm/Compat/modelEvents.js +48 -1
- package/orm/dataTree.js +17 -0
- package/package.json +2 -2
- package/react/useSub.js +6 -1
package/orm/Compat/README.md
CHANGED
|
@@ -119,6 +119,10 @@ Reads (`get`/`peek`) are forwarded to the target while the ref is active.
|
|
|
119
119
|
Ref mirroring is scheduled through Teamplay runtime scheduler, so updates remain batch-friendly
|
|
120
120
|
and do not leak intermediate ref states during a single batched cycle.
|
|
121
121
|
|
|
122
|
+
Source path restriction:
|
|
123
|
+
- The ref source path (`$from`) must be in a private collection (`_session`, `_page`, `$local`, etc.).
|
|
124
|
+
- Public source paths are not supported.
|
|
125
|
+
|
|
122
126
|
```js
|
|
123
127
|
const $local = $.local.value
|
|
124
128
|
const $user = $.users.user1
|
|
@@ -201,7 +205,6 @@ $table.dataSource.get()
|
|
|
201
205
|
|
|
202
206
|
**Limitations vs Racer**
|
|
203
207
|
- No `refList`, `refMap`.
|
|
204
|
-
- No automatic list index patching on insert/remove/move.
|
|
205
208
|
- No event emissions specific to refs.
|
|
206
209
|
- No support for racer-style ref meta/options beyond the basic signature.
|
|
207
210
|
|
|
@@ -29,11 +29,11 @@ import {
|
|
|
29
29
|
} from '../dataTree.js'
|
|
30
30
|
import { on as onCustomEvent, removeListener as removeCustomEventListener } from './eventsCompat.js'
|
|
31
31
|
import { waitForImperativeQueryReady } from './queryReadiness.js'
|
|
32
|
-
import { normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
|
|
32
|
+
import { isModelEventsEnabled, normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
|
|
33
33
|
import { setRefLink, removeRefLink, getAllRefLinks } from './refRegistry.js'
|
|
34
34
|
import { REF_TARGET, resolveRefSignalSafe, resolveRefSegmentsSafe } from './refFallback.js'
|
|
35
35
|
import { runInBatch } from '../batchScheduler.js'
|
|
36
|
-
import { runInSilentContext, runInModelEventsSilentContext } from './silentContext.js'
|
|
36
|
+
import { runInSilentContext, runInModelEventsSilentContext, isSilentContextActive } from './silentContext.js'
|
|
37
37
|
import universal$ from '../../react/universal$.js'
|
|
38
38
|
import { getRootContext } from '../rootContext.js'
|
|
39
39
|
import disposeRootContext from '../disposeRootContext.js'
|
|
@@ -618,6 +618,7 @@ class SignalCompat extends Signal {
|
|
|
618
618
|
}
|
|
619
619
|
if (!$to) throw Error('Signal.ref() expects a target path or signal')
|
|
620
620
|
if ($from === $to) return $from
|
|
621
|
+
ensurePrivateRefSource($from, 'Signal.ref()')
|
|
621
622
|
const store = getRefStore($from)
|
|
622
623
|
const fromPath = $from.path()
|
|
623
624
|
const existing = store.get(fromPath)
|
|
@@ -1060,7 +1061,9 @@ function setReplacePrivateCompatSync ($signal, value) {
|
|
|
1060
1061
|
value = normalizeIdFields(value, idFields, segments[1])
|
|
1061
1062
|
}
|
|
1062
1063
|
setReplacePrivateData(getOwningRootId($signal), segments, value)
|
|
1063
|
-
|
|
1064
|
+
if (shouldMirrorPrivateRefMutationLocally()) {
|
|
1065
|
+
mirrorRefMutationFromTarget(segments, value)
|
|
1066
|
+
}
|
|
1064
1067
|
}
|
|
1065
1068
|
|
|
1066
1069
|
function delPrivateCompatSync ($signal, options) {
|
|
@@ -1115,12 +1118,16 @@ async function setReplaceOnSignal ($signal, value) {
|
|
|
1115
1118
|
}
|
|
1116
1119
|
if (isPublicCollection(segments[0])) {
|
|
1117
1120
|
const result = await _setPublicDocReplace(segments, value)
|
|
1118
|
-
|
|
1121
|
+
if (shouldMirrorPublicRefMutationLocally(segments)) {
|
|
1122
|
+
mirrorRefMutationFromTarget(segments, value)
|
|
1123
|
+
}
|
|
1119
1124
|
return result
|
|
1120
1125
|
}
|
|
1121
1126
|
if (isPrivateMutationForbidden()) throw Error(ERRORS.publicOnly)
|
|
1122
1127
|
const result = setReplacePrivateData(getOwningRootId($signal), segments, value)
|
|
1123
|
-
|
|
1128
|
+
if (shouldMirrorPrivateRefMutationLocally()) {
|
|
1129
|
+
mirrorRefMutationFromTarget(segments, value)
|
|
1130
|
+
}
|
|
1124
1131
|
return result
|
|
1125
1132
|
}
|
|
1126
1133
|
|
|
@@ -1252,6 +1259,27 @@ function getOwningRootId ($signal) {
|
|
|
1252
1259
|
return $root?.[ROOT_ID]
|
|
1253
1260
|
}
|
|
1254
1261
|
|
|
1262
|
+
function ensurePrivateRefSource ($signal, methodName) {
|
|
1263
|
+
const segments = $signal?.[SEGMENTS]
|
|
1264
|
+
const collection = segments?.[0]
|
|
1265
|
+
if (typeof collection === 'string' && /^[_$]/.test(collection)) return
|
|
1266
|
+
throw Error(`${methodName} source path must be in a private collection`)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function shouldMirrorPublicRefMutationLocally (segments) {
|
|
1270
|
+
if (isSilentContextActive()) return true
|
|
1271
|
+
if (!Array.isArray(segments) || segments.length < 2) return true
|
|
1272
|
+
// Public doc ops emit compat model events only when there is an initialized
|
|
1273
|
+
// Doc runtime (subscribed/fetched). Without runtime we must mirror immediately.
|
|
1274
|
+
const transportHash = JSON.stringify([segments[0], segments[1]])
|
|
1275
|
+
return !docSubscriptions.hasRuntime(transportHash)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function shouldMirrorPrivateRefMutationLocally () {
|
|
1279
|
+
if (isSilentContextActive()) return true
|
|
1280
|
+
return !isModelEventsEnabled()
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1255
1283
|
function shallowCopy (value) {
|
|
1256
1284
|
const rawValue = raw(value)
|
|
1257
1285
|
if (Array.isArray(rawValue)) return rawValue.slice()
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getRefLinks, getRefRootIds } from './refRegistry.js'
|
|
2
2
|
import { isCompatEnv } from '../compatEnv.js'
|
|
3
|
-
import { isSilentContextActive, isModelEventsSilentContextActive } from './silentContext.js'
|
|
3
|
+
import { isSilentContextActive, isModelEventsSilentContextActive, runInModelEventsSilentContext } from './silentContext.js'
|
|
4
4
|
import { normalizeRootId } from '../rootScope.js'
|
|
5
5
|
import { getRootContext, getRootContexts } from '../rootContext.js'
|
|
6
|
+
import { setReplace as setReplaceInDataTree, del as delFromDataTree } from '../dataTree.js'
|
|
7
|
+
import { setReplacePrivateData, delPrivateData } from '../privateData.js'
|
|
6
8
|
|
|
7
9
|
const MODEL_EVENT_NAMES = ['change', 'all']
|
|
8
10
|
|
|
@@ -70,6 +72,8 @@ export function emitModelChange (path, value, prevValue, meta) {
|
|
|
70
72
|
if (!isPathPrefix(link.toSegments, segments)) continue
|
|
71
73
|
if (link.mirrorOnly && typeof link.onChange === 'function') {
|
|
72
74
|
link.onChange()
|
|
75
|
+
} else if (!link.mirrorOnly) {
|
|
76
|
+
mirrorRefAliasFromTargetSegments(rootId, link, segments, value, meta)
|
|
73
77
|
}
|
|
74
78
|
const suffix = segments.slice(link.toSegments.length)
|
|
75
79
|
const nextSegments = link.fromSegments.concat(suffix)
|
|
@@ -107,6 +111,49 @@ function splitPattern (pattern) {
|
|
|
107
111
|
return pattern.split('.').filter(Boolean)
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
function mirrorRefAliasFromTargetSegments (rootId, link, targetSegments, value, meta) {
|
|
115
|
+
const suffix = targetSegments.slice(link.toSegments.length)
|
|
116
|
+
const fromSegments = link.fromSegments.concat(suffix)
|
|
117
|
+
const fromRootId = normalizeRootId(link.fromRootId ?? rootId)
|
|
118
|
+
const shouldDelete = shouldDeleteMirroredPath(value, meta)
|
|
119
|
+
runInModelEventsSilentContext(() => {
|
|
120
|
+
if (isPrivateSegments(fromSegments)) {
|
|
121
|
+
if (shouldDelete) {
|
|
122
|
+
delPrivateData(fromRootId, fromSegments)
|
|
123
|
+
} else {
|
|
124
|
+
setReplacePrivateData(fromRootId, fromSegments, cloneValue(value))
|
|
125
|
+
}
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (shouldDelete) {
|
|
129
|
+
delFromDataTree(fromSegments)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
setReplaceInDataTree(fromSegments, cloneValue(value))
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isPrivateSegments (segments) {
|
|
137
|
+
if (!Array.isArray(segments) || !segments.length) return false
|
|
138
|
+
return /^[_$]/.test(String(segments[0]))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function shouldDeleteMirroredPath (value, meta) {
|
|
142
|
+
if (meta?.op === 'setReplace') return false
|
|
143
|
+
if (meta?.op === 'del') return true
|
|
144
|
+
return value === undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function cloneValue (value) {
|
|
148
|
+
if (Array.isArray(value)) return value.map(cloneValue)
|
|
149
|
+
if (value && typeof value === 'object') {
|
|
150
|
+
const cloned = {}
|
|
151
|
+
for (const key of Object.keys(value)) cloned[key] = cloneValue(value[key])
|
|
152
|
+
return cloned
|
|
153
|
+
}
|
|
154
|
+
return value
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
function getModelEventRootStore (eventName, rootId, create = false) {
|
|
111
158
|
return getRootContext(normalizeRootId(rootId), create)?.getModelEventStore(eventName, create)
|
|
112
159
|
}
|
package/orm/dataTree.js
CHANGED
|
@@ -326,6 +326,13 @@ export async function setPublicDocReplace (segments, value) {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
const relativePath = segments.slice(2)
|
|
329
|
+
// json0 direct replace ops require every ancestor container to already exist.
|
|
330
|
+
// Racer-like compat set, however, materializes missing/primitive parents while
|
|
331
|
+
// descending into the path. Fall back to the older diff-based path when the
|
|
332
|
+
// direct op would target a non-existent/non-object ancestor.
|
|
333
|
+
if (!canApplyDirectReplaceOp(docState.snapshot || {}, relativePath)) {
|
|
334
|
+
return setPublicDoc(segments, value)
|
|
335
|
+
}
|
|
329
336
|
const previous = getRaw(segments)
|
|
330
337
|
const normalizedPrevious = normalizeUndefined(
|
|
331
338
|
relativePath.length === 0 ? stripIdFields(previous, idFields) : previous
|
|
@@ -444,6 +451,16 @@ function normalizeUndefined (value) {
|
|
|
444
451
|
return value === undefined ? null : value
|
|
445
452
|
}
|
|
446
453
|
|
|
454
|
+
function canApplyDirectReplaceOp (docSnapshot, relativePath) {
|
|
455
|
+
if (relativePath.length === 0) return true
|
|
456
|
+
let node = docSnapshot
|
|
457
|
+
for (let i = 0; i < relativePath.length - 1; i++) {
|
|
458
|
+
if (node == null || typeof node !== 'object') return false
|
|
459
|
+
node = node[relativePath[i]]
|
|
460
|
+
}
|
|
461
|
+
return node != null && typeof node === 'object'
|
|
462
|
+
}
|
|
463
|
+
|
|
447
464
|
function normalizeValueForOp (value) {
|
|
448
465
|
let result = raw(value)
|
|
449
466
|
if (result != null && typeof result === 'object') result = JSON.parse(JSON.stringify(result))
|
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.98",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"license": "MIT",
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "ae4e0e6ee39e395fa9eb48a2099cc1d5b2a4630d"
|
|
87
87
|
}
|
package/react/useSub.js
CHANGED
|
@@ -44,8 +44,8 @@ export function useSubDeferred (signal, params, { async = false, defer, batch =
|
|
|
44
44
|
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
45
45
|
if (promiseOrSignal.then) {
|
|
46
46
|
const promise = maybeThrottle(promiseOrSignal)
|
|
47
|
+
const hasPreviousSignal = !!$signalRef.current
|
|
47
48
|
if (batch) {
|
|
48
|
-
const hasPreviousSignal = !!$signalRef.current
|
|
49
49
|
// Batch suspense must block only on initial load.
|
|
50
50
|
// On resubscribe we keep rendering previous signal and refresh in background.
|
|
51
51
|
if (!hasPreviousSignal) {
|
|
@@ -61,6 +61,11 @@ export function useSubDeferred (signal, params, { async = false, defer, batch =
|
|
|
61
61
|
scheduleUpdate(promise)
|
|
62
62
|
return
|
|
63
63
|
}
|
|
64
|
+
// Keep previous snapshot during update re-subscribe and refresh in background.
|
|
65
|
+
if (hasPreviousSignal) {
|
|
66
|
+
scheduleUpdate(promise)
|
|
67
|
+
return $signalRef.current
|
|
68
|
+
}
|
|
64
69
|
if (compatAttemptCleanup) registerCompatAttemptCleanup(signal, params)
|
|
65
70
|
throw promise
|
|
66
71
|
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|