teamplay 0.4.0-alpha.73 → 0.4.0-alpha.75
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 +13 -0
- package/orm/Compat/SignalCompat.js +59 -2
- package/orm/Compat/hooksCompat.js +2 -1
- package/orm/Compat/startStopCompat.js +6 -1
- package/orm/Doc.js +15 -0
- package/orm/dataTree.js +6 -0
- package/orm/missingDoc.js +3 -0
- package/package.json +2 -2
package/orm/Compat/README.md
CHANGED
|
@@ -913,6 +913,19 @@ After `useBatch()` stops throwing in compat mode, immediate reads via
|
|
|
913
913
|
`useLocal(...).get(...)` for already requested batch entities should not produce
|
|
914
914
|
transient `undefined` caused by materialization races.
|
|
915
915
|
|
|
916
|
+
### Missing ShareDB Docs
|
|
917
|
+
|
|
918
|
+
Compat now mirrors Racer behavior for **missing public docs** (`type === null`,
|
|
919
|
+
`version === 0`) after subscribe/fetch:
|
|
920
|
+
|
|
921
|
+
- `connection.get(collection, id).data` becomes a truthy empty observable object;
|
|
922
|
+
- but the compat/model path still stays unresolved, so `$.collection[id].get()`
|
|
923
|
+
continues to return `undefined` until the document is actually created.
|
|
924
|
+
|
|
925
|
+
This matters for legacy consumers which read `shareDoc.data` directly (for
|
|
926
|
+
example readonly rich-text paths) while still expecting normal public-doc
|
|
927
|
+
creation semantics from model mutators.
|
|
928
|
+
|
|
916
929
|
## Examples
|
|
917
930
|
|
|
918
931
|
### useDoc with Suspense
|
|
@@ -15,6 +15,7 @@ import { IS_QUERY, getQuerySignal, querySubscriptions } from '../Query.js'
|
|
|
15
15
|
import { IS_AGGREGATION, aggregationSubscriptions, getAggregationSignal } from '../Aggregation.js'
|
|
16
16
|
import { getIdFieldsForSegments, isIdFieldPath, normalizeIdFields, isPlainObject } from '../idFields.js'
|
|
17
17
|
import {
|
|
18
|
+
del as _del,
|
|
18
19
|
setReplace as _setReplace,
|
|
19
20
|
incrementPublic as _incrementPublic,
|
|
20
21
|
arrayPush as _arrayPush,
|
|
@@ -916,8 +917,15 @@ function isMissingPublicDocDeleteError ($signal, error) {
|
|
|
916
917
|
|
|
917
918
|
async function setDiffDeepOnSignal ($target, value) {
|
|
918
919
|
if ($target[SEGMENTS].length === 0) throw Error('Can\'t set the root signal data')
|
|
919
|
-
|
|
920
|
-
|
|
920
|
+
// Use peek() here. compat start() writes via setDiffDeep inside an observer and must not
|
|
921
|
+
// subscribe to its own target, otherwise later local edits on child signals cause start()
|
|
922
|
+
// to rerun and overwrite them from source.
|
|
923
|
+
const before = $target.peek()
|
|
924
|
+
if (isPublicCollection($target[SEGMENTS][0])) {
|
|
925
|
+
await diffDeepCompat($target, before, value)
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
diffDeepCompatSync($target, before, value)
|
|
921
929
|
}
|
|
922
930
|
|
|
923
931
|
async function diffDeepCompat ($signal, before, after) {
|
|
@@ -949,6 +957,35 @@ async function diffDeepCompat ($signal, before, after) {
|
|
|
949
957
|
await SignalCompat.prototype.set.call($signal, after)
|
|
950
958
|
}
|
|
951
959
|
|
|
960
|
+
function diffDeepCompatSync ($signal, before, after) {
|
|
961
|
+
if (before === after) return
|
|
962
|
+
|
|
963
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
964
|
+
if (deepEqualCompat(before, after)) return
|
|
965
|
+
const changedIndexes = getChangedArrayIndexes(before, after)
|
|
966
|
+
if (before.length === after.length && changedIndexes.length === 1) {
|
|
967
|
+
const index = changedIndexes[0]
|
|
968
|
+
diffDeepCompatSync(getChildSignal($signal, index), before[index], after[index])
|
|
969
|
+
return
|
|
970
|
+
}
|
|
971
|
+
setReplacePrivateCompatSync($signal, after)
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (isDiffableObject(before, after)) {
|
|
976
|
+
for (const key of Object.keys(before)) {
|
|
977
|
+
if (Object.prototype.hasOwnProperty.call(after, key)) continue
|
|
978
|
+
delPrivateCompatSync(getChildSignal($signal, key))
|
|
979
|
+
}
|
|
980
|
+
for (const key of Object.keys(after)) {
|
|
981
|
+
diffDeepCompatSync(getChildSignal($signal, key), before[key], after[key])
|
|
982
|
+
}
|
|
983
|
+
return
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
setReplacePrivateCompatSync($signal, after)
|
|
987
|
+
}
|
|
988
|
+
|
|
952
989
|
function isDiffableObject (before, after) {
|
|
953
990
|
if (!isPlainObject(before) || !isPlainObject(after)) return false
|
|
954
991
|
if (isReactLike(before) || isReactLike(after)) return false
|
|
@@ -972,6 +1009,26 @@ function getChildSignal ($parent, key) {
|
|
|
972
1009
|
return $child
|
|
973
1010
|
}
|
|
974
1011
|
|
|
1012
|
+
function setReplacePrivateCompatSync ($signal, value) {
|
|
1013
|
+
const segments = $signal[SEGMENTS]
|
|
1014
|
+
if (segments.length === 0) throw Error('Can\'t set the root signal data')
|
|
1015
|
+
const idFields = getIdFieldsForSegments(segments)
|
|
1016
|
+
if (isIdFieldPath(segments, idFields)) return
|
|
1017
|
+
if (segments.length === 2) {
|
|
1018
|
+
value = normalizeIdFields(value, idFields, segments[1])
|
|
1019
|
+
}
|
|
1020
|
+
_setReplace(segments, value)
|
|
1021
|
+
mirrorRefMutationFromTarget(segments, value)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function delPrivateCompatSync ($signal) {
|
|
1025
|
+
const segments = $signal[SEGMENTS]
|
|
1026
|
+
if (segments.length === 0) throw Error('Can\'t delete the root signal data')
|
|
1027
|
+
const idFields = getIdFieldsForSegments(segments)
|
|
1028
|
+
if (isIdFieldPath(segments, idFields)) return
|
|
1029
|
+
_del(segments)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
975
1032
|
function deepEqualCompat (left, right) {
|
|
976
1033
|
if (left === right) return true
|
|
977
1034
|
if (left == null || right == null) return false
|
|
@@ -5,6 +5,7 @@ import * as promiseBatcher from '../../react/promiseBatcher.js'
|
|
|
5
5
|
import { getRaw } from '../dataTree.js'
|
|
6
6
|
import { getConnection } from '../connection.js'
|
|
7
7
|
import { isCompatEnv } from '../compatEnv.js'
|
|
8
|
+
import { isMissingShareDoc } from '../missingDoc.js'
|
|
8
9
|
|
|
9
10
|
const $root = getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
|
|
10
11
|
const emittedCompatWarnings = new Set()
|
|
@@ -393,7 +394,7 @@ function isDocReady (segments) {
|
|
|
393
394
|
const [collection, id] = segments
|
|
394
395
|
const shareDoc = getShareDoc(collection, id)
|
|
395
396
|
// Missing docs should not block the batch barrier forever.
|
|
396
|
-
return
|
|
397
|
+
return isMissingShareDoc(shareDoc)
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
function getShareDoc (collection, id) {
|
|
@@ -38,7 +38,12 @@ export function compatStartOnRoot ($root, targetPath, ...depsAndGetter) {
|
|
|
38
38
|
if (isThenable(err)) return
|
|
39
39
|
throw err
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const detachedValue = detachStartValue(nextValue)
|
|
42
|
+
// Keep the detached snapshot to avoid aliasing source and target.
|
|
43
|
+
// Old racer start() writes through diffDeep by default. In compat mode we must preserve
|
|
44
|
+
// that behavior, but also avoid reading the target reactively inside start(), otherwise
|
|
45
|
+
// start() subscribes to its own output and local child edits get immediately overwritten.
|
|
46
|
+
const maybePromise = $target.setDiffDeep(detachedValue)
|
|
42
47
|
if (maybePromise?.catch) maybePromise.catch(ignorePromiseRejection)
|
|
43
48
|
}, { scheduler: scheduleReaction })
|
|
44
49
|
store.set(targetKey, { stop: () => unobserve(reaction) })
|
package/orm/Doc.js
CHANGED
|
@@ -7,6 +7,7 @@ import SubscriptionState from './SubscriptionState.js'
|
|
|
7
7
|
import { getIdFieldsForSegments, injectIdFields, isPlainObject } from './idFields.js'
|
|
8
8
|
import { emitModelChange, isModelEventsEnabled } from './Compat/modelEvents.js'
|
|
9
9
|
import { getSubscriptionGcDelay } from './subscriptionGcDelay.js'
|
|
10
|
+
import { isMissingShareDoc } from './missingDoc.js'
|
|
10
11
|
|
|
11
12
|
const ERROR_ON_EXCESSIVE_UNSUBSCRIBES = false
|
|
12
13
|
|
|
@@ -103,6 +104,20 @@ class Doc {
|
|
|
103
104
|
|
|
104
105
|
_refData () {
|
|
105
106
|
const doc = getConnection().get(this.collection, this.docId)
|
|
107
|
+
// Racer/react-sharedb-hooks normalizes a missing ShareDB doc into a truthy
|
|
108
|
+
// observable placeholder on the shareDoc itself (`observable(undefined) -> {}`),
|
|
109
|
+
// while still keeping the model tree path unresolved. Some legacy consumers
|
|
110
|
+
// (for example readonly RTEditor paths) rely on this exact contract by reading
|
|
111
|
+
// `connection.get(...).data` directly and only checking for truthiness.
|
|
112
|
+
//
|
|
113
|
+
// We intentionally mirror that behavior here:
|
|
114
|
+
// - missing doc => keep model path undefined
|
|
115
|
+
// - but make shareDoc.data truthy/observable so direct ShareDB consumers behave
|
|
116
|
+
// the same way they do under Racer.
|
|
117
|
+
if (isMissingShareDoc(doc) && doc.data === undefined) {
|
|
118
|
+
if (!isObservable(doc.data)) doc.data = observable(undefined)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
106
121
|
if (doc.data == null) return
|
|
107
122
|
const idFields = getIdFieldsForSegments([this.collection, this.docId])
|
|
108
123
|
if (isPlainObject(doc.data)) injectIdFields(doc.data, idFields, this.docId)
|
package/orm/dataTree.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getIdFieldsForSegments, injectIdFields, stripIdFields, isPlainObject }
|
|
|
7
7
|
import { emitModelChange, isModelEventsEnabled } from './Compat/modelEvents.js'
|
|
8
8
|
import { isSilentContextActive } from './Compat/silentContext.js'
|
|
9
9
|
import { isCompatEnv } from './compatEnv.js'
|
|
10
|
+
import { isMissingShareDoc } from './missingDoc.js'
|
|
10
11
|
|
|
11
12
|
const ALLOW_PARTIAL_DOC_CREATION = false
|
|
12
13
|
|
|
@@ -344,6 +345,10 @@ function resolvePublicDocState ({
|
|
|
344
345
|
}) {
|
|
345
346
|
ensureLocalDocSyncedWithShareDoc({ collection, docId, doc, idFields })
|
|
346
347
|
|
|
348
|
+
if (isMissingShareDoc(doc)) {
|
|
349
|
+
return { exists: false, snapshot: undefined, source: 'none' }
|
|
350
|
+
}
|
|
351
|
+
|
|
347
352
|
if (doc?.data != null) {
|
|
348
353
|
return {
|
|
349
354
|
exists: true,
|
|
@@ -390,6 +395,7 @@ function ensureLocalDocSyncedWithShareDoc ({
|
|
|
390
395
|
doc,
|
|
391
396
|
idFields
|
|
392
397
|
}) {
|
|
398
|
+
if (isMissingShareDoc(doc)) return
|
|
393
399
|
if (doc?.data == null) return
|
|
394
400
|
if (isPlainObject(doc.data)) injectIdFields(doc.data, idFields, docId)
|
|
395
401
|
const shared = raw(doc.data)
|
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.75",
|
|
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": "e1b1ccfa91fde6dbd8fba60513c0ce0bc007c61b"
|
|
87
87
|
}
|