teamplay 0.4.0-alpha.76 → 0.4.0-alpha.78

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.
@@ -13,7 +13,7 @@ import { publicOnly, fetchOnly, setFetchOnly } from '../connection.js'
13
13
  import { docSubscriptions } from '../Doc.js'
14
14
  import { IS_QUERY, getQuerySignal, querySubscriptions } from '../Query.js'
15
15
  import { IS_AGGREGATION, aggregationSubscriptions, getAggregationSignal } from '../Aggregation.js'
16
- import { getIdFieldsForSegments, isIdFieldPath, normalizeIdFields, isPlainObject } from '../idFields.js'
16
+ import { getIdFieldsForSegments, isIdFieldPath, isPublicDocPath, normalizeIdFields, isPlainObject } from '../idFields.js'
17
17
  import {
18
18
  del as _del,
19
19
  setReplace as _setReplace,
@@ -38,6 +38,7 @@ import {
38
38
  stringRemovePublic as _stringRemovePublic
39
39
  } from '../dataTree.js'
40
40
  import { on as onCustomEvent, removeListener as removeCustomEventListener } from './eventsCompat.js'
41
+ import { waitForImperativeQueryReady } from './queryReadiness.js'
41
42
  import { normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
42
43
  import { setRefLink, removeRefLink, getRefLinks } from './refRegistry.js'
43
44
  import { REF_TARGET, resolveRefSignalSafe, resolveRefSegmentsSafe } from './refFallback.js'
@@ -1014,7 +1015,7 @@ function setReplacePrivateCompatSync ($signal, value) {
1014
1015
  if (segments.length === 0) throw Error('Can\'t set the root signal data')
1015
1016
  const idFields = getIdFieldsForSegments(segments)
1016
1017
  if (isIdFieldPath(segments, idFields)) return
1017
- if (segments.length === 2) {
1018
+ if (isPublicDocPath(segments)) {
1018
1019
  value = normalizeIdFields(value, idFields, segments[1])
1019
1020
  }
1020
1021
  _setReplace(segments, value)
@@ -1064,7 +1065,7 @@ async function setReplaceOnSignal ($signal, value) {
1064
1065
  if (segments.length === 0) throw Error('Can\'t set the root signal data')
1065
1066
  const idFields = getIdFieldsForSegments(segments)
1066
1067
  if (isIdFieldPath(segments, idFields)) return
1067
- if (segments.length === 2) {
1068
+ if (isPublicDocPath(segments)) {
1068
1069
  value = normalizeIdFields(value, idFields, segments[1])
1069
1070
  }
1070
1071
  if (isPublicCollection(segments[0])) {
@@ -1284,8 +1285,18 @@ function flattenItems (items, result = []) {
1284
1285
  }
1285
1286
 
1286
1287
  function subscribeSelf ($signal) {
1287
- if ($signal[IS_QUERY]) return querySubscriptions.subscribe($signal)
1288
- if ($signal[IS_AGGREGATION]) return aggregationSubscriptions.subscribe($signal)
1288
+ if ($signal[IS_QUERY]) {
1289
+ return (async () => {
1290
+ await querySubscriptions.subscribe($signal)
1291
+ await waitForImperativeQueryReady($signal)
1292
+ })()
1293
+ }
1294
+ if ($signal[IS_AGGREGATION]) {
1295
+ return (async () => {
1296
+ await aggregationSubscriptions.subscribe($signal)
1297
+ await waitForImperativeQueryReady($signal)
1298
+ })()
1299
+ }
1289
1300
  if (isPublicDocumentSignal($signal)) return docSubscriptions.subscribe($signal)
1290
1301
  if (isPublicCollectionSignal($signal)) {
1291
1302
  throw Error('Signal.subscribe() expects a query signal. Use .query() for collections.')
@@ -2,10 +2,8 @@ import { getRootSignal, GLOBAL_ROOT_ID } from '../Root.js'
2
2
  import useSub, { useAsyncSub } from '../../react/useSub.js'
3
3
  import universal$ from '../../react/universal$.js'
4
4
  import * as promiseBatcher from '../../react/promiseBatcher.js'
5
- import { getRaw } from '../dataTree.js'
6
- import { getConnection } from '../connection.js'
7
5
  import { isCompatEnv } from '../compatEnv.js'
8
- import { isMissingShareDoc } from '../missingDoc.js'
6
+ import { isQueryReady } from './queryReadiness.js'
9
7
 
10
8
  const $root = getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
11
9
  const emittedCompatWarnings = new Set()
@@ -361,50 +359,6 @@ function normalizeSyncSubOptions (options) {
361
359
  }
362
360
  }
363
361
 
364
- function isQueryReady (
365
- collection,
366
- idsSegments,
367
- docsSegments,
368
- extraSegments,
369
- aggregationSegments,
370
- isAggregate,
371
- hasExtraResult
372
- ) {
373
- if (hasExtraResult) {
374
- return getRaw(extraSegments) !== undefined
375
- }
376
- if (isAggregate) {
377
- const docs = getRaw(docsSegments)
378
- if (Array.isArray(docs)) return true
379
- if (getRaw(extraSegments) !== undefined) return true
380
- return getRaw(aggregationSegments) !== undefined
381
- }
382
- const ids = getRaw(idsSegments)
383
- if (!Array.isArray(ids)) return false
384
- for (const id of ids) {
385
- if (id == null) continue
386
- if (!isDocReady([collection, id])) return false
387
- }
388
- return true
389
- }
390
-
391
- function isDocReady (segments) {
392
- const rawDoc = getRaw(segments)
393
- if (rawDoc !== undefined) return true
394
- const [collection, id] = segments
395
- const shareDoc = getShareDoc(collection, id)
396
- // Missing docs should not block the batch barrier forever.
397
- return isMissingShareDoc(shareDoc)
398
- }
399
-
400
- function getShareDoc (collection, id) {
401
- try {
402
- return getConnection().get(collection, id)
403
- } catch {
404
- return undefined
405
- }
406
- }
407
-
408
362
  export const __COMPAT_BATCH_READY__ = {
409
363
  isQueryReady
410
364
  }
@@ -0,0 +1,165 @@
1
+ import { getRaw, set as _set } from '../dataTree.js'
2
+ import { getConnection } from '../connection.js'
3
+ import { isMissingShareDoc } from '../missingDoc.js'
4
+ import { QUERIES, HASH, PARAMS, COLLECTION_NAME } from '../Query.js'
5
+ import { AGGREGATIONS, IS_AGGREGATION } from '../Aggregation.js'
6
+
7
+ let imperativeQueryReadyTimeoutMs = 1000
8
+
9
+ export function isQueryReady (
10
+ collection,
11
+ idsSegments,
12
+ docsSegments,
13
+ extraSegments,
14
+ aggregationSegments,
15
+ isAggregate,
16
+ hasExtraResult
17
+ ) {
18
+ if (hasExtraResult) {
19
+ return getRaw(extraSegments) !== undefined
20
+ }
21
+ if (isAggregate) {
22
+ const docs = getRaw(docsSegments)
23
+ if (Array.isArray(docs)) return true
24
+ if (getRaw(extraSegments) !== undefined) return true
25
+ return getRaw(aggregationSegments) !== undefined
26
+ }
27
+ const ids = getRaw(idsSegments)
28
+ if (!Array.isArray(ids)) return false
29
+ for (const id of ids) {
30
+ if (id == null) continue
31
+ if (!isDocReady([collection, id])) return false
32
+ }
33
+ return true
34
+ }
35
+
36
+ export function isDocReady (segments) {
37
+ const rawDoc = getRaw(segments)
38
+ if (rawDoc !== undefined) return true
39
+ const [collection, id] = segments
40
+ const shareDoc = getShareDoc(collection, id)
41
+ // Missing docs should not block the batch barrier forever.
42
+ return isMissingShareDoc(shareDoc)
43
+ }
44
+
45
+ export async function waitForImperativeQueryReady ($query) {
46
+ const timeoutMs = imperativeQueryReadyTimeoutMs
47
+ const startedAt = Date.now()
48
+ while (true) {
49
+ if (isImperativeQueryReady($query)) {
50
+ syncQueryDocsFromCollection($query)
51
+ return
52
+ }
53
+ if (Date.now() - startedAt >= timeoutMs) {
54
+ throw createImperativeQueryReadinessError($query, timeoutMs)
55
+ }
56
+ await new Promise(resolve => setTimeout(resolve, 0))
57
+ }
58
+ }
59
+
60
+ export function __setImperativeQueryReadyTimeoutForTests (timeoutMs) {
61
+ imperativeQueryReadyTimeoutMs = timeoutMs
62
+ }
63
+
64
+ export function __resetImperativeQueryReadyTimeoutForTests () {
65
+ imperativeQueryReadyTimeoutMs = 1000
66
+ }
67
+
68
+ function isImperativeQueryReady ($query) {
69
+ const collection = $query[COLLECTION_NAME]
70
+ const hash = $query[HASH]
71
+ const params = $query[PARAMS]
72
+ const hasExtraResult = isExtraQuery(params)
73
+ if (hasExtraResult) return getRaw([QUERIES, hash, 'extra']) !== undefined
74
+
75
+ const isAggregate = !!$query[IS_AGGREGATION] || isAggregationQuery(params)
76
+ if (isAggregate) {
77
+ return isQueryReady(
78
+ collection,
79
+ [QUERIES, hash, 'ids'],
80
+ [QUERIES, hash, 'docs'],
81
+ [QUERIES, hash, 'extra'],
82
+ [AGGREGATIONS, hash],
83
+ true,
84
+ false
85
+ )
86
+ }
87
+
88
+ const ids = getRaw([QUERIES, hash, 'ids'])
89
+ if (!Array.isArray(ids)) return false
90
+ for (const id of ids) {
91
+ if (id == null) continue
92
+ if (getRaw([collection, id]) === undefined) return false
93
+ }
94
+ return true
95
+ }
96
+
97
+ function syncQueryDocsFromCollection ($query) {
98
+ const params = $query[PARAMS]
99
+ if ($query[IS_AGGREGATION] || isAggregationQuery(params) || isExtraQuery(params)) return
100
+
101
+ const collection = $query[COLLECTION_NAME]
102
+ const hash = $query[HASH]
103
+ const ids = getRaw([QUERIES, hash, 'ids'])
104
+ if (!Array.isArray(ids)) return
105
+
106
+ const docs = []
107
+ for (const id of ids) {
108
+ if (id == null) continue
109
+ const doc = getRaw([collection, id])
110
+ if (doc === undefined) {
111
+ throw createImperativeQueryReadinessError($query, imperativeQueryReadyTimeoutMs)
112
+ }
113
+ docs.push(doc)
114
+ }
115
+
116
+ _set([QUERIES, hash, 'docs'], docs)
117
+ }
118
+
119
+ function createImperativeQueryReadinessError ($query, timeoutMs) {
120
+ const collection = $query[COLLECTION_NAME]
121
+ const hash = $query[HASH]
122
+ const params = $query[PARAMS]
123
+ const ids = getRaw([QUERIES, hash, 'ids'])
124
+ const missingDocs = []
125
+
126
+ if (Array.isArray(ids)) {
127
+ for (const id of ids) {
128
+ if (id == null) continue
129
+ const doc = getRaw([collection, id])
130
+ if (doc !== undefined) continue
131
+ const shareDoc = getShareDoc(collection, id)
132
+ missingDocs.push({
133
+ id,
134
+ missingShareDoc: isMissingShareDoc(shareDoc)
135
+ })
136
+ }
137
+ }
138
+
139
+ return Error(`
140
+ Compat query did not fully materialize within ${timeoutMs}ms.
141
+ Collection: ${collection}
142
+ Params: ${JSON.stringify(params)}
143
+ Hash: ${hash}
144
+ Ids: ${JSON.stringify(ids)}
145
+ Missing docs: ${JSON.stringify(missingDocs)}
146
+ `)
147
+ }
148
+
149
+ function getShareDoc (collection, id) {
150
+ try {
151
+ return getConnection().get(collection, id)
152
+ } catch {
153
+ return undefined
154
+ }
155
+ }
156
+
157
+ function isExtraQuery (query) {
158
+ if (!query || typeof query !== 'object') return false
159
+ return !!(query.$count || query.$queryName)
160
+ }
161
+
162
+ function isAggregationQuery (query) {
163
+ if (!query || typeof query !== 'object') return false
164
+ return !!(query.$aggregate || query.$aggregationName)
165
+ }
package/orm/SignalBase.js CHANGED
@@ -45,7 +45,15 @@ import { IS_QUERY, HASH, QUERIES } from './Query.js'
45
45
  import { AGGREGATIONS, IS_AGGREGATION, getAggregationCollectionName, getAggregationDocId } from './Aggregation.js'
46
46
  import { ROOT_FUNCTION, getRoot } from './Root.js'
47
47
  import { publicOnly } from './connection.js'
48
- import { DEFAULT_ID_FIELDS, getIdFieldsForSegments, isIdFieldPath, normalizeIdFields } from './idFields.js'
48
+ import {
49
+ DEFAULT_ID_FIELDS,
50
+ getIdFieldsForSegments,
51
+ isIdFieldPath,
52
+ isPublicDocPath,
53
+ normalizeIdFields,
54
+ prepareAddPayload,
55
+ resolveAddDocId
56
+ } from './idFields.js'
49
57
  import { isCompatEnv } from './compatEnv.js'
50
58
  import { resolveRefSegmentsSafe, resolveRefSignalSafe } from './Compat/refFallback.js'
51
59
  import { compatStartOnRoot, compatStopOnRoot, joinScopePath } from './Compat/startStopCompat.js'
@@ -262,7 +270,7 @@ export class Signal extends Function {
262
270
  if (this[SEGMENTS].length === 0) throw Error('Can\'t set the root signal data')
263
271
  const idFields = getIdFieldsForSegments(this[SEGMENTS])
264
272
  if (isIdFieldPath(this[SEGMENTS], idFields)) return
265
- if (this[SEGMENTS].length === 2) {
273
+ if (isPublicDocPath(this[SEGMENTS])) {
266
274
  value = normalizeIdFields(value, idFields, this[SEGMENTS][1])
267
275
  }
268
276
  if (isPublicCollection(this[SEGMENTS][0])) {
@@ -424,24 +432,9 @@ export class Signal extends Function {
424
432
 
425
433
  async add (value) {
426
434
  if (arguments.length > 1) throw Error('Signal.add() expects a single argument')
427
- if (!value || typeof value !== 'object') throw Error('Signal.add() expects an object argument')
428
- const hasId = value.id != null
429
- const hasUnderscoreId = value._id != null
430
- if (hasId && hasUnderscoreId && value.id !== value._id) {
431
- throw Error(
432
- `Signal.add() got conflicting "id" (${JSON.stringify(value.id)}) and "_id" (${JSON.stringify(value._id)})`
433
- )
434
- }
435
- let id = value.id ?? value._id
436
- id ??= uuid()
435
+ const id = resolveAddDocId(value, uuid)
437
436
  const idFields = getIdFieldsForSegments([this[SEGMENTS][0], id])
438
- if (idFields.includes('_id')) value._id = id
439
- if (idFields.includes('id')) {
440
- value.id = id
441
- } else if (value.id === id) {
442
- delete value.id
443
- }
444
- await this[id].set(value)
437
+ await this[id].set(prepareAddPayload(value, idFields, id))
445
438
  return id
446
439
  }
447
440
 
package/orm/dataTree.js CHANGED
@@ -3,7 +3,7 @@ import jsonDiff from 'json0-ot-diff'
3
3
  import diffMatchPatch from 'diff-match-patch'
4
4
  import { getConnection } from './connection.js'
5
5
  import setDiffDeep from '../utils/setDiffDeep.js'
6
- import { getIdFieldsForSegments, injectIdFields, stripIdFields, isPlainObject } from './idFields.js'
6
+ import { getIdFieldsForSegments, injectIdFields, stripIdFields, isPlainObject, isIdFieldPath } from './idFields.js'
7
7
  import { emitModelChange, isModelEventsEnabled } from './Compat/modelEvents.js'
8
8
  import { isSilentContextActive } from './Compat/silentContext.js'
9
9
  import { isCompatEnv } from './compatEnv.js'
@@ -158,7 +158,7 @@ export async function setPublicDoc (segments, value, deleteValue = false) {
158
158
  if (docId === 'undefined') throw Error(ERRORS.publicDocIdUndefined(segments))
159
159
  if (!(collection && docId)) throw Error(ERRORS.publicDoc(segments))
160
160
  const idFields = getIdFieldsForSegments([collection, docId])
161
- if (segments.length >= 3 && idFields.includes(segments[segments.length - 1])) return
161
+ if (isIdFieldPath(segments, idFields)) return
162
162
  const doc = getConnection().get(collection, docId)
163
163
  let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData: true })
164
164
  if (!docState.exists && segments.length > 2) {
@@ -247,7 +247,7 @@ export async function setPublicDocReplace (segments, value) {
247
247
  if (docId === 'undefined') throw Error(ERRORS.publicDocIdUndefined(segments))
248
248
  if (!(collection && docId)) throw Error(ERRORS.publicDoc(segments))
249
249
  const idFields = getIdFieldsForSegments([collection, docId])
250
- if (segments.length >= 3 && idFields.includes(segments[segments.length - 1])) return
250
+ if (isIdFieldPath(segments, idFields)) return
251
251
  const doc = getConnection().get(collection, docId)
252
252
  let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData: true })
253
253
  if (!docState.exists && segments.length > 2) {
package/orm/idFields.js CHANGED
@@ -50,8 +50,39 @@ export function stripIdFields (value, idFields) {
50
50
  return next
51
51
  }
52
52
 
53
+ export function resolveAddDocId (value, getDefaultId) {
54
+ if (!value || typeof value !== 'object') throw Error('Signal.add() expects an object argument')
55
+ const hasId = value.id != null
56
+ const hasUnderscoreId = value._id != null
57
+ if (hasId && hasUnderscoreId && value.id !== value._id) {
58
+ throw Error(
59
+ `Signal.add() got conflicting "id" (${JSON.stringify(value.id)}) and "_id" (${JSON.stringify(value._id)})`
60
+ )
61
+ }
62
+ return value.id ?? value._id ?? getDefaultId()
63
+ }
64
+
65
+ export function prepareAddPayload (value, idFields, docId) {
66
+ if (idFields.includes('_id')) value._id = docId
67
+ if (idFields.includes('id')) {
68
+ value.id = docId
69
+ } else if (value.id === docId) {
70
+ delete value.id
71
+ }
72
+ return value
73
+ }
74
+
75
+ export function isPublicDocPath (segments) {
76
+ if (!Array.isArray(segments) || segments.length !== 2) return false
77
+ const [collection, docId] = segments
78
+ if (typeof collection !== 'string' || !collection) return false
79
+ if (collection[0] === '_' || collection[0] === '$') return false
80
+ return docId != null
81
+ }
82
+
53
83
  export function isIdFieldPath (segments, idFields) {
54
- if (segments.length < 3) return false
55
- const last = segments[segments.length - 1]
84
+ if (!Array.isArray(segments) || segments.length !== 3) return false
85
+ if (!isPublicDocPath(segments.slice(0, 2))) return false
86
+ const last = segments[2]
56
87
  return idFields.includes(last)
57
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.76",
3
+ "version": "0.4.0-alpha.78",
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": "954f4352b0641286b46c015a152175929eb979bc"
86
+ "gitHead": "6ced13d745e3ccb000b2fc9c34a31e51810cadbc"
87
87
  }