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.
@@ -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
- const before = $target.get()
920
- await diffDeepCompat($target, before, value)
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 !!(shareDoc && shareDoc.type === null && shareDoc.data == null)
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 maybePromise = $target.set(detachStartValue(nextValue))
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)
@@ -0,0 +1,3 @@
1
+ export function isMissingShareDoc (doc) {
2
+ return !!doc && doc.type === null && doc.version === 0
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.73",
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": "d17c2236244682019cd737d8d3b4e853c771fa2b"
86
+ "gitHead": "e1b1ccfa91fde6dbd8fba60513c0ce0bc007c61b"
87
87
  }