teamplay 0.4.0-alpha.3 → 0.4.0-alpha.31
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 +103 -9
- package/orm/Compat/SignalCompat.js +173 -24
- package/orm/Compat/hooksCompat.js +57 -17
- package/orm/Compat/modelEvents.js +2 -4
- package/orm/Compat/refFallback.js +60 -0
- package/orm/Compat/startStopCompat.js +107 -0
- package/orm/Doc.js +7 -4
- package/orm/Query.js +24 -2
- package/orm/Reaction.js +2 -1
- package/orm/Signal.js +2 -5
- package/orm/SignalBase.js +43 -3
- package/orm/batchScheduler.js +62 -0
- package/orm/compatEnv.js +4 -0
- package/orm/dataTree.js +6 -1
- package/orm/getSignal.js +32 -3
- package/package.json +8 -8
- package/react/convertToObserver.js +2 -1
- package/react/promiseBatcher.js +27 -0
- package/react/trapRender.js +11 -4
- package/react/useSub.js +16 -2
- package/react/wrapIntoSuspense.js +1 -1
- package/utils/setDiffDeep.js +32 -12
- package/orm/Compat/REF.md +0 -315
package/orm/Compat/README.md
CHANGED
|
@@ -94,9 +94,11 @@ $.users.user1.name.parent(2) // $.users
|
|
|
94
94
|
Legacy path navigation. Accepts:
|
|
95
95
|
- string with dot path (`'a.b.c'`)
|
|
96
96
|
- integer index for arrays (`0`)
|
|
97
|
+
- multiple path segments (`'a', 'b', 0`)
|
|
97
98
|
|
|
98
99
|
```js
|
|
99
100
|
$.users.user1.at('profile.name')
|
|
101
|
+
$.users.user1.at('profile', 'name')
|
|
100
102
|
$.items.at(0)
|
|
101
103
|
```
|
|
102
104
|
|
|
@@ -106,6 +108,7 @@ Resolve a path from root, ignoring the current signal path.
|
|
|
106
108
|
|
|
107
109
|
```js
|
|
108
110
|
$.users.user1.scope('users.user2')
|
|
111
|
+
$.users.user1.scope('users', 'user2')
|
|
109
112
|
```
|
|
110
113
|
|
|
111
114
|
### ref(target) / ref(subpath, target)
|
|
@@ -171,6 +174,49 @@ $alias.get() === $user.get() // false
|
|
|
171
174
|
- No event emissions specific to refs.
|
|
172
175
|
- No support for racer-style ref meta/options beyond the basic signature.
|
|
173
176
|
|
|
177
|
+
### start(targetPath, ...deps, getter)
|
|
178
|
+
|
|
179
|
+
Legacy computed binding API from Racer/StartupJS.
|
|
180
|
+
Creates a reactive computation and writes its result into `targetPath`.
|
|
181
|
+
Source of truth is root API (`$root.start(...)`), but non-root calls are supported as sugar:
|
|
182
|
+
- `$scope.start('a.b', ...deps, getter)` → `$root.start('<scopePath>.a.b', ...deps, getter)`
|
|
183
|
+
|
|
184
|
+
- `targetPath`: string path where computed value is written.
|
|
185
|
+
- `deps`: dependencies used by `getter`.
|
|
186
|
+
- `getter`: function called as `getter(...resolvedDeps)`.
|
|
187
|
+
|
|
188
|
+
Dependency resolution:
|
|
189
|
+
- Signal-like dep (`$doc`, `$session.user`) → `dep.get()`.
|
|
190
|
+
- String dep (`'settings.theme'`) → `$root.get(dep)`.
|
|
191
|
+
- Any other dep → passed as-is.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
$root.start('_virtual.lesson', $.lessons[lessonId], '_session.userId', (lesson, userId) => {
|
|
195
|
+
if (!lesson) return undefined
|
|
196
|
+
return { stageIds: lesson.stageIds, userId }
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Behavior:
|
|
201
|
+
- Calling `start()` again for the same `targetPath` replaces previous reaction.
|
|
202
|
+
- `undefined` result applies compat delete semantics at target path.
|
|
203
|
+
- `null` result is stored as `null`.
|
|
204
|
+
- Returns target signal.
|
|
205
|
+
- If any dependency temporarily suspends (throws a Promise/thenable), compat skips the whole tick (getter is not called and target is not written).
|
|
206
|
+
- If `getter` throws a Promise, compat skips that tick and retries on next reactive update.
|
|
207
|
+
|
|
208
|
+
### stop(targetPath)
|
|
209
|
+
|
|
210
|
+
Stops a computation created with `start(targetPath, ...)`.
|
|
211
|
+
No-op if there is no active computation for the path.
|
|
212
|
+
Source of truth is root API (`$root.stop(...)`), but non-root calls are supported as sugar:
|
|
213
|
+
- `$scope.stop('a.b')` → `$root.stop('<scopePath>.a.b')`
|
|
214
|
+
- `$scope.stop()` → `$root.stop('<scopePath>')`
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
$root.stop('_virtual.lesson')
|
|
218
|
+
```
|
|
219
|
+
|
|
174
220
|
### query(collection, query, options?)
|
|
175
221
|
|
|
176
222
|
Creates a query signal **without** subscribing. Supports shorthand params:
|
|
@@ -236,6 +282,7 @@ Returns the current value and tracks reactivity.
|
|
|
236
282
|
const name = $.users.user1.name.get()
|
|
237
283
|
$root.get('$render.url')
|
|
238
284
|
$user.get('profile.name')
|
|
285
|
+
$user.get('profile', 'name')
|
|
239
286
|
```
|
|
240
287
|
|
|
241
288
|
### peek(subpath?)
|
|
@@ -245,6 +292,7 @@ Returns the current value **without** tracking reactivity.
|
|
|
245
292
|
```js
|
|
246
293
|
const name = $.users.user1.name.peek()
|
|
247
294
|
$user.peek('profile.name')
|
|
295
|
+
$user.peek('profile', 'name')
|
|
248
296
|
```
|
|
249
297
|
|
|
250
298
|
### getCopy(subpath)
|
|
@@ -306,6 +354,20 @@ for (const $doc of $query) {
|
|
|
306
354
|
}
|
|
307
355
|
```
|
|
308
356
|
|
|
357
|
+
### Mutator Semantics (Core vs Compat)
|
|
358
|
+
|
|
359
|
+
Compatibility mode intentionally aligns mutators with Racer. This differs from core `Signal` behavior.
|
|
360
|
+
|
|
361
|
+
| API | Core (`Signal`) | Compat (`SignalCompat`) |
|
|
362
|
+
| --- | --- | --- |
|
|
363
|
+
| `set` | Uses deep-diff path (`dataTree.set` + internal `setDiffDeep`). | Path-targeted replace semantics, Racer-like. `undefined` keeps delete semantics. |
|
|
364
|
+
| `setEach` | Not a special API in core mutators. | Per-key compat `set` (not `assign` merge/delete behavior). |
|
|
365
|
+
| `setDiffDeep` | Deep-diff engine (`utils/setDiffDeep.js`). | Recursive Racer-like diff implemented via compat mutators (`set` / `del`) on nested paths. |
|
|
366
|
+
| `setDiff` | N/A as compat shim. | Alias to compat `set` for both signatures: `setDiff(value)` and `setDiff(path, value)`. |
|
|
367
|
+
|
|
368
|
+
Migration note: compat behavior is intentionally Racer-aligned and may differ from core mutators.
|
|
369
|
+
Composite compat mutators (`setEach`, `setDiffDeep`) apply updates atomically for Teamplay-scheduled observers via the runtime batch scheduler.
|
|
370
|
+
|
|
309
371
|
### set(value) and set(path, value)
|
|
310
372
|
|
|
311
373
|
`SignalCompat` accepts both:
|
|
@@ -315,7 +377,14 @@ $.users.user1.name.set('Alice')
|
|
|
315
377
|
$.users.user1.set('profile.name', 'Alice')
|
|
316
378
|
```
|
|
317
379
|
|
|
318
|
-
In compat mode, `set` replaces
|
|
380
|
+
In compat mode, `set` replaces the value at the target path.
|
|
381
|
+
- `set(path, null)` stores `null`.
|
|
382
|
+
- `set(path, undefined)` applies current delete semantics.
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
await $.users.user1.set('profile', { name: 'Ann', role: 'student' })
|
|
386
|
+
await $.users.user1.set('profile', { name: 'Kate' }) // role is removed
|
|
387
|
+
```
|
|
319
388
|
|
|
320
389
|
### setNull(path?, value)
|
|
321
390
|
|
|
@@ -327,26 +396,47 @@ $.config.setNull('theme', 'light')
|
|
|
327
396
|
|
|
328
397
|
### setDiffDeep(path?, value)
|
|
329
398
|
|
|
330
|
-
Applies a
|
|
399
|
+
Applies a recursive Racer-like diff using compat mutators (`set` / `del`) on subpaths.
|
|
400
|
+
This is intentionally a compat implementation detail and differs from core deep-diff internals.
|
|
331
401
|
|
|
332
402
|
```js
|
|
333
|
-
$.users.user1.
|
|
403
|
+
await $.users.user1.set({ profile: { name: 'Ann', role: 'student' } })
|
|
404
|
+
await $.users.user1.setDiffDeep({ profile: { name: 'Kate' } }) // deep-diff path
|
|
334
405
|
```
|
|
335
406
|
|
|
336
407
|
### setDiff(path?, value)
|
|
337
408
|
|
|
338
|
-
Alias for `set
|
|
409
|
+
Alias for compat `set` in both forms:
|
|
410
|
+
- `setDiff(value)` -> same as `set(value)`
|
|
411
|
+
- `setDiff(path, value)` -> same as `set(path, value)`
|
|
339
412
|
|
|
340
413
|
```js
|
|
341
|
-
$.users.user1.setDiff({ profile: { name: '
|
|
414
|
+
await $.users.user1.setDiff({ profile: { name: 'Kate' } })
|
|
415
|
+
await $.users.user1.setDiff('profile', { name: 'Bob' })
|
|
342
416
|
```
|
|
343
417
|
|
|
344
418
|
### setEach(path?, object)
|
|
345
419
|
|
|
346
|
-
|
|
420
|
+
Racer-like per-key set. `setEach` iterates keys and applies compat `set` for each key.
|
|
421
|
+
- `setEach({ k: null })` stores `null`.
|
|
422
|
+
- `setEach({ k: undefined })` applies current delete semantics.
|
|
347
423
|
|
|
348
424
|
```js
|
|
349
|
-
$.users.user1.setEach({ name: 'Bob', age:
|
|
425
|
+
await $.users.user1.setEach({ name: 'Bob', age: null })
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Null / Undefined Matrix (Compat)
|
|
429
|
+
|
|
430
|
+
| Call | Result |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| `set(path, null)` | stores `null` at `path` |
|
|
433
|
+
| `set(path, undefined)` | applies delete semantics at `path` |
|
|
434
|
+
| `setEach({ k: null })` | stores `null` for `k` |
|
|
435
|
+
| `setEach({ k: undefined })` | applies delete semantics for `k` |
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
await $.users.user1.set('status', null) // status === null
|
|
439
|
+
await $.users.user1.setEach({ status: undefined }) // status deleted
|
|
350
440
|
```
|
|
351
441
|
|
|
352
442
|
### assign(object)
|
|
@@ -672,10 +762,12 @@ This matches StartupJS and makes updates easy:
|
|
|
672
762
|
$users[userId].name.set('New Name')
|
|
673
763
|
```
|
|
674
764
|
|
|
675
|
-
`useQuery$` returns the
|
|
765
|
+
`useQuery$` returns the **query signal**:
|
|
676
766
|
|
|
677
767
|
```js
|
|
678
|
-
const $
|
|
768
|
+
const $query = useQuery$('users', { active: true })
|
|
769
|
+
const ids = $query.getIds()
|
|
770
|
+
const docs = $query.get()
|
|
679
771
|
```
|
|
680
772
|
|
|
681
773
|
If `query == null`, a warning is logged and `{ _id: '__NON_EXISTENT__' }` is used.
|
|
@@ -758,6 +850,8 @@ const Component = observer(() => {
|
|
|
758
850
|
```js
|
|
759
851
|
const Component = observer(() => {
|
|
760
852
|
const [users, $users] = useQuery('users', { active: true })
|
|
853
|
+
const $query = useQuery$('users', { active: true })
|
|
854
|
+
const ids = $query.getIds()
|
|
761
855
|
return (
|
|
762
856
|
<>
|
|
763
857
|
{users.map(u => <div key={u._id}>{u.name}</div>)}
|
|
@@ -8,15 +8,14 @@ import {
|
|
|
8
8
|
isPublicCollectionSignal,
|
|
9
9
|
isPublicDocumentSignal
|
|
10
10
|
} from '../SignalBase.js'
|
|
11
|
-
import { getRoot } from '../Root.js'
|
|
11
|
+
import { getRoot, ROOT } from '../Root.js'
|
|
12
12
|
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 } from '../idFields.js'
|
|
16
|
+
import { getIdFieldsForSegments, isIdFieldPath, normalizeIdFields, isPlainObject } from '../idFields.js'
|
|
17
17
|
import {
|
|
18
18
|
setReplace as _setReplace,
|
|
19
|
-
setPublicDocReplace as _setPublicDocReplace,
|
|
20
19
|
incrementPublic as _incrementPublic,
|
|
21
20
|
arrayPush as _arrayPush,
|
|
22
21
|
arrayUnshift as _arrayUnshift,
|
|
@@ -40,11 +39,17 @@ import {
|
|
|
40
39
|
import { on as onCustomEvent, removeListener as removeCustomEventListener } from './eventsCompat.js'
|
|
41
40
|
import { normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
|
|
42
41
|
import { setRefLink, removeRefLink } from './refRegistry.js'
|
|
42
|
+
import { REF_TARGET, resolveRefSignalSafe } from './refFallback.js'
|
|
43
|
+
import { runInBatch } from '../batchScheduler.js'
|
|
43
44
|
|
|
44
45
|
class SignalCompat extends Signal {
|
|
45
46
|
static ID_FIELDS = ['_id', 'id']
|
|
46
47
|
static [GETTERS] = [...DEFAULT_GETTERS, 'getCopy', 'getDeepCopy']
|
|
47
48
|
|
|
49
|
+
get root () {
|
|
50
|
+
return this.scope()
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
path (subpath) {
|
|
49
54
|
if (arguments.length > 1) throw Error('Signal.path() expects a single argument')
|
|
50
55
|
if (arguments.length === 0) return super.path()
|
|
@@ -54,8 +59,9 @@ class SignalCompat extends Signal {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
at (subpath) {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const segments = arguments.length > 1
|
|
63
|
+
? parseAtSegments(arguments, 'Signal.at()')
|
|
64
|
+
: parseAtSubpath(subpath, arguments.length, 'Signal.at()')
|
|
59
65
|
if (segments.length === 0) return this
|
|
60
66
|
let $cursor = this
|
|
61
67
|
for (const segment of segments) {
|
|
@@ -118,8 +124,18 @@ class SignalCompat extends Signal {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
get () {
|
|
121
|
-
if (arguments.length > 1)
|
|
127
|
+
if (arguments.length > 1) {
|
|
128
|
+
const segments = parseAtSegments(arguments, 'Signal.get()')
|
|
129
|
+
const $base = resolveRefSignal(this)
|
|
130
|
+
const $target = resolveSignal($base, segments)
|
|
131
|
+
return Signal.prototype.get.call($target)
|
|
132
|
+
}
|
|
122
133
|
if (arguments.length === 1) {
|
|
134
|
+
if (arguments[0] == null) {
|
|
135
|
+
const $target = resolveRefSignal(this)
|
|
136
|
+
if ($target !== this) return Signal.prototype.get.apply($target, [])
|
|
137
|
+
return Signal.prototype.get.apply(this, [])
|
|
138
|
+
}
|
|
123
139
|
const segments = parseAtSubpath(arguments[0], 1, 'Signal.get()')
|
|
124
140
|
const $base = resolveRefSignal(this)
|
|
125
141
|
const $target = resolveSignal($base, segments)
|
|
@@ -131,8 +147,18 @@ class SignalCompat extends Signal {
|
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
peek () {
|
|
134
|
-
if (arguments.length > 1)
|
|
150
|
+
if (arguments.length > 1) {
|
|
151
|
+
const segments = parseAtSegments(arguments, 'Signal.peek()')
|
|
152
|
+
const $base = resolveRefSignal(this)
|
|
153
|
+
const $target = resolveSignal($base, segments)
|
|
154
|
+
return Signal.prototype.peek.call($target)
|
|
155
|
+
}
|
|
135
156
|
if (arguments.length === 1) {
|
|
157
|
+
if (arguments[0] == null) {
|
|
158
|
+
const $target = resolveRefSignal(this)
|
|
159
|
+
if ($target !== this) return Signal.prototype.peek.apply($target, [])
|
|
160
|
+
return Signal.prototype.peek.apply(this, [])
|
|
161
|
+
}
|
|
136
162
|
const segments = parseAtSubpath(arguments[0], 1, 'Signal.peek()')
|
|
137
163
|
const $base = resolveRefSignal(this)
|
|
138
164
|
const $target = resolveSignal($base, segments)
|
|
@@ -154,7 +180,24 @@ class SignalCompat extends Signal {
|
|
|
154
180
|
value = path
|
|
155
181
|
}
|
|
156
182
|
const $target = resolveSignal(this, segments)
|
|
157
|
-
return Signal.prototype.set.call($target, value)
|
|
183
|
+
if (value === undefined) return Signal.prototype.set.call($target, value)
|
|
184
|
+
return setReplaceOnSignal($target, value)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async add (collectionOrValue, value) {
|
|
188
|
+
const isRoot = this[SEGMENTS].length === 0
|
|
189
|
+
const isRootCollectionCall = isRoot && typeof collectionOrValue === 'string'
|
|
190
|
+
|
|
191
|
+
if (isRootCollectionCall) {
|
|
192
|
+
if (arguments.length !== 2) throw Error('Signal.add() expects (collection, object)')
|
|
193
|
+
if (!value || typeof value !== 'object') throw Error('Signal.add() expects an object argument')
|
|
194
|
+
const $root = getRoot(this) || this
|
|
195
|
+
const $collection = resolveSignal($root, [collectionOrValue])
|
|
196
|
+
return $collection.add(value)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (arguments.length > 1) throw Error('Signal.add() expects a single argument')
|
|
200
|
+
return Signal.prototype.add.call(this, collectionOrValue)
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
async setNull (path, value) {
|
|
@@ -183,7 +226,7 @@ class SignalCompat extends Signal {
|
|
|
183
226
|
value = path
|
|
184
227
|
}
|
|
185
228
|
const $target = resolveSignal(this, segments)
|
|
186
|
-
return
|
|
229
|
+
return runInBatch(() => setDiffDeepOnSignal($target, value))
|
|
187
230
|
}
|
|
188
231
|
|
|
189
232
|
async setDiff (path, value) {
|
|
@@ -191,7 +234,7 @@ class SignalCompat extends Signal {
|
|
|
191
234
|
if (forwarded) return forwarded
|
|
192
235
|
if (arguments.length > 2) throw Error('Signal.setDiff() expects one or two arguments')
|
|
193
236
|
if (arguments.length === 1) {
|
|
194
|
-
return
|
|
237
|
+
return this.set(path)
|
|
195
238
|
}
|
|
196
239
|
return this.set(path, value)
|
|
197
240
|
}
|
|
@@ -207,7 +250,17 @@ class SignalCompat extends Signal {
|
|
|
207
250
|
object = path
|
|
208
251
|
}
|
|
209
252
|
const $target = resolveSignal(this, segments)
|
|
210
|
-
|
|
253
|
+
if (!object) return
|
|
254
|
+
if (typeof object !== 'object') {
|
|
255
|
+
throw Error('Signal.setEach() expects an object argument, got: ' + typeof object)
|
|
256
|
+
}
|
|
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
|
+
})
|
|
211
264
|
}
|
|
212
265
|
|
|
213
266
|
async del (path) {
|
|
@@ -509,11 +562,11 @@ class SignalCompat extends Signal {
|
|
|
509
562
|
}
|
|
510
563
|
|
|
511
564
|
scope (path) {
|
|
512
|
-
if (arguments.length > 1) throw Error('Signal.scope() expects a single argument')
|
|
513
565
|
const $root = getRoot(this) || this
|
|
514
566
|
if (arguments.length === 0) return $root
|
|
515
|
-
|
|
516
|
-
|
|
567
|
+
const segments = arguments.length > 1
|
|
568
|
+
? parseAtSegments(arguments, 'Signal.scope()')
|
|
569
|
+
: parseAtSubpath(path, arguments.length, 'Signal.scope()')
|
|
517
570
|
if (segments.length === 0) return $root
|
|
518
571
|
let $cursor = $root
|
|
519
572
|
for (const segment of segments) {
|
|
@@ -524,7 +577,6 @@ class SignalCompat extends Signal {
|
|
|
524
577
|
}
|
|
525
578
|
|
|
526
579
|
const REFS = Symbol('compat refs')
|
|
527
|
-
const REF_TARGET = Symbol('compat ref target')
|
|
528
580
|
|
|
529
581
|
function getRefStore ($signal) {
|
|
530
582
|
const $root = getRoot($signal) || $signal
|
|
@@ -559,14 +611,7 @@ function trackDeep (value, seen = new Set()) {
|
|
|
559
611
|
}
|
|
560
612
|
|
|
561
613
|
function resolveRefSignal ($signal) {
|
|
562
|
-
|
|
563
|
-
const seen = new Set()
|
|
564
|
-
while (current && current[REF_TARGET]) {
|
|
565
|
-
if (seen.has(current)) break
|
|
566
|
-
seen.add(current)
|
|
567
|
-
current = current[REF_TARGET]
|
|
568
|
-
}
|
|
569
|
-
return current
|
|
614
|
+
return resolveRefSignalSafe($signal) || $signal
|
|
570
615
|
}
|
|
571
616
|
|
|
572
617
|
function forwardRef ($signal, methodName, args) {
|
|
@@ -583,6 +628,10 @@ function isSignalLike (value) {
|
|
|
583
628
|
return value && typeof value.path === 'function' && typeof value.get === 'function'
|
|
584
629
|
}
|
|
585
630
|
|
|
631
|
+
function isReactLike (value) {
|
|
632
|
+
return !!(value && typeof value === 'object' && typeof value.$$typeof === 'symbol')
|
|
633
|
+
}
|
|
634
|
+
|
|
586
635
|
function resolveRefTarget ($signal, target, methodName) {
|
|
587
636
|
if (isSignalLike(target)) return target
|
|
588
637
|
if (typeof target === 'string') {
|
|
@@ -600,6 +649,23 @@ function parseAtSubpath (subpath, argsLength, methodName) {
|
|
|
600
649
|
throw Error(`${methodName} expects a string or integer argument`)
|
|
601
650
|
}
|
|
602
651
|
|
|
652
|
+
function parseAtSegments (args, methodName) {
|
|
653
|
+
const segments = []
|
|
654
|
+
for (const arg of Array.from(args)) {
|
|
655
|
+
if (typeof arg === 'string') {
|
|
656
|
+
const parts = arg.split('.').filter(Boolean)
|
|
657
|
+
segments.push(...parts)
|
|
658
|
+
continue
|
|
659
|
+
}
|
|
660
|
+
if (typeof arg === 'number' && Number.isFinite(arg) && Number.isInteger(arg)) {
|
|
661
|
+
segments.push(arg)
|
|
662
|
+
continue
|
|
663
|
+
}
|
|
664
|
+
throw Error(`${methodName} expects string or integer path segments`)
|
|
665
|
+
}
|
|
666
|
+
return segments
|
|
667
|
+
}
|
|
668
|
+
|
|
603
669
|
function resolveSignal ($signal, segments) {
|
|
604
670
|
let $cursor = $signal
|
|
605
671
|
for (const segment of segments) {
|
|
@@ -608,6 +674,89 @@ function resolveSignal ($signal, segments) {
|
|
|
608
674
|
return $cursor
|
|
609
675
|
}
|
|
610
676
|
|
|
677
|
+
async function setDiffDeepOnSignal ($target, value) {
|
|
678
|
+
if ($target[SEGMENTS].length === 0) throw Error('Can\'t set the root signal data')
|
|
679
|
+
const before = $target.get()
|
|
680
|
+
await diffDeepCompat($target, before, value)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function diffDeepCompat ($signal, before, after) {
|
|
684
|
+
if (before === after) return
|
|
685
|
+
|
|
686
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
687
|
+
if (deepEqualCompat(before, after)) return
|
|
688
|
+
const changedIndexes = getChangedArrayIndexes(before, after)
|
|
689
|
+
if (before.length === after.length && changedIndexes.length === 1) {
|
|
690
|
+
const index = changedIndexes[0]
|
|
691
|
+
await diffDeepCompat(getChildSignal($signal, index), before[index], after[index])
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
await SignalCompat.prototype.set.call($signal, after)
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (isDiffableObject(before, after)) {
|
|
699
|
+
for (const key of Object.keys(before)) {
|
|
700
|
+
if (Object.prototype.hasOwnProperty.call(after, key)) continue
|
|
701
|
+
await SignalCompat.prototype.del.call(getChildSignal($signal, key))
|
|
702
|
+
}
|
|
703
|
+
for (const key of Object.keys(after)) {
|
|
704
|
+
await diffDeepCompat(getChildSignal($signal, key), before[key], after[key])
|
|
705
|
+
}
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
await SignalCompat.prototype.set.call($signal, after)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function isDiffableObject (before, after) {
|
|
713
|
+
if (!isPlainObject(before) || !isPlainObject(after)) return false
|
|
714
|
+
if (isReactLike(before) || isReactLike(after)) return false
|
|
715
|
+
return true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function getChangedArrayIndexes (before, after) {
|
|
719
|
+
if (!Array.isArray(before) || !Array.isArray(after)) return []
|
|
720
|
+
const maxLength = Math.max(before.length, after.length)
|
|
721
|
+
const changed = []
|
|
722
|
+
for (let i = 0; i < maxLength; i++) {
|
|
723
|
+
if (!deepEqualCompat(before[i], after[i])) changed.push(i)
|
|
724
|
+
}
|
|
725
|
+
return changed
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function getChildSignal ($parent, key) {
|
|
729
|
+
const $child = new SignalCompat([...$parent[SEGMENTS], key])
|
|
730
|
+
const $root = getRoot($parent)
|
|
731
|
+
if ($root) $child[ROOT] = $root
|
|
732
|
+
return $child
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function deepEqualCompat (left, right) {
|
|
736
|
+
if (left === right) return true
|
|
737
|
+
if (left == null || right == null) return false
|
|
738
|
+
if (typeof left !== 'object' || typeof right !== 'object') return false
|
|
739
|
+
if (Array.isArray(left) !== Array.isArray(right)) return false
|
|
740
|
+
|
|
741
|
+
if (Array.isArray(left)) {
|
|
742
|
+
if (left.length !== right.length) return false
|
|
743
|
+
for (let i = 0; i < left.length; i++) {
|
|
744
|
+
if (!deepEqualCompat(left[i], right[i])) return false
|
|
745
|
+
}
|
|
746
|
+
return true
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (!isPlainObject(left) || !isPlainObject(right)) return false
|
|
750
|
+
const leftKeys = Object.keys(left)
|
|
751
|
+
const rightKeys = Object.keys(right)
|
|
752
|
+
if (leftKeys.length !== rightKeys.length) return false
|
|
753
|
+
for (const key of leftKeys) {
|
|
754
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) return false
|
|
755
|
+
if (!deepEqualCompat(left[key], right[key])) return false
|
|
756
|
+
}
|
|
757
|
+
return true
|
|
758
|
+
}
|
|
759
|
+
|
|
611
760
|
function getSignalValueAt ($signal, segments) {
|
|
612
761
|
const $target = resolveSignal($signal, segments)
|
|
613
762
|
return $target.get()
|
|
@@ -622,7 +771,7 @@ async function setReplaceOnSignal ($signal, value) {
|
|
|
622
771
|
value = normalizeIdFields(value, idFields, segments[1])
|
|
623
772
|
}
|
|
624
773
|
if (isPublicCollection(segments[0])) {
|
|
625
|
-
return
|
|
774
|
+
return Signal.prototype.set.call($signal, value)
|
|
626
775
|
}
|
|
627
776
|
if (publicOnly) throw Error(ERRORS.publicOnly)
|
|
628
777
|
return _setReplace(segments, value)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import * as promiseBatcher from '../../react/promiseBatcher.js'
|
|
4
5
|
|
|
5
6
|
const $root = getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
|
|
6
7
|
|
|
@@ -72,8 +73,10 @@ export function usePage (path) {
|
|
|
72
73
|
return useLocal(prefixLocalPath('_page', path))
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
export function useBatch () {
|
|
77
|
+
const promise = promiseBatcher.getPromiseAll()
|
|
78
|
+
if (promise) throw promise
|
|
79
|
+
}
|
|
77
80
|
|
|
78
81
|
export function useDoc$ (collection, id, options) {
|
|
79
82
|
const $doc = getDocSignal(collection, id, 'useDoc')
|
|
@@ -86,13 +89,16 @@ export function useDoc (collection, id, options) {
|
|
|
86
89
|
return [$doc.get(), $doc]
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
// Batch variants are aliases to non-batch versions (no batching in teamplay).
|
|
90
92
|
export function useBatchDoc (collection, id, options) {
|
|
91
|
-
|
|
93
|
+
const $doc = useBatchDoc$(collection, id, options)
|
|
94
|
+
if (!$doc) return [undefined, undefined]
|
|
95
|
+
return [$doc.get(), $doc]
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
export function useBatchDoc$ (collection, id,
|
|
95
|
-
|
|
98
|
+
export function useBatchDoc$ (collection, id, _options) {
|
|
99
|
+
const $doc = getDocSignal(collection, id, 'useBatchDoc')
|
|
100
|
+
const options = _options ? { ..._options, ...BATCH_SUB_OPTIONS } : BATCH_SUB_OPTIONS
|
|
101
|
+
return useSub($doc, undefined, options)
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
export function useAsyncDoc$ (collection, id, options) {
|
|
@@ -109,8 +115,8 @@ export function useAsyncDoc (collection, id, options) {
|
|
|
109
115
|
export function useQuery$ (collection, query, options) {
|
|
110
116
|
const $collection = getCollectionSignal(collection, query, 'useQuery')
|
|
111
117
|
const normalizedOptions = options ? { ...options, async: false } : options
|
|
112
|
-
useSub($collection, normalizeQuery(query, 'useQuery'), normalizedOptions)
|
|
113
|
-
return $
|
|
118
|
+
const $query = useSub($collection, normalizeQuery(query, 'useQuery'), normalizedOptions)
|
|
119
|
+
return $query
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
export function useQuery (collection, query, options) {
|
|
@@ -122,8 +128,8 @@ export function useQuery (collection, query, options) {
|
|
|
122
128
|
|
|
123
129
|
export function useAsyncQuery$ (collection, query, options) {
|
|
124
130
|
const $collection = getCollectionSignal(collection, query, 'useAsyncQuery')
|
|
125
|
-
useAsyncSub($collection, normalizeQuery(query, 'useAsyncQuery'), options)
|
|
126
|
-
return $
|
|
131
|
+
const $query = useAsyncSub($collection, normalizeQuery(query, 'useAsyncQuery'), options)
|
|
132
|
+
return $query
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
export function useAsyncQuery (collection, query, options) {
|
|
@@ -133,13 +139,17 @@ export function useAsyncQuery (collection, query, options) {
|
|
|
133
139
|
return [$query.get(), $collection]
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
export function useBatchQuery$ (collection, query, _options) {
|
|
143
|
+
const $collection = getCollectionSignal(collection, query, 'useBatchQuery')
|
|
144
|
+
const options = _options ? { ..._options, ...BATCH_SUB_OPTIONS } : BATCH_SUB_OPTIONS
|
|
145
|
+
return useSub($collection, normalizeQuery(query, 'useBatchQuery'), options)
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
export function useBatchQuery (collection, query, options) {
|
|
142
|
-
|
|
149
|
+
const $collection = getCollectionSignal(collection, query, 'useBatchQuery')
|
|
150
|
+
const $query = useBatchQuery$(collection, query, options)
|
|
151
|
+
if (!$query) return [undefined, $collection]
|
|
152
|
+
return [$query.get(), $collection]
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
export function useQueryIds (collection, ids = [], options = {}) {
|
|
@@ -157,7 +167,17 @@ export function useQueryIds (collection, ids = [], options = {}) {
|
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
export function useBatchQueryIds (collection, ids = [], options = {}) {
|
|
160
|
-
|
|
170
|
+
const list = Array.isArray(ids) ? ids.slice() : []
|
|
171
|
+
if (options?.reverse) list.reverse()
|
|
172
|
+
const [docs, $collection] = useBatchQuery(collection, { _id: { $in: list } }, options)
|
|
173
|
+
if (!docs) return [docs, $collection]
|
|
174
|
+
const docsById = new Map()
|
|
175
|
+
for (const doc of docs) {
|
|
176
|
+
const id = doc?._id ?? doc?.id
|
|
177
|
+
if (id != null) docsById.set(id, doc)
|
|
178
|
+
}
|
|
179
|
+
const items = list.map(id => docsById.get(id)).filter(Boolean)
|
|
180
|
+
return [items, $collection]
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
export function useAsyncQueryIds (collection, ids = [], options = {}) {
|
|
@@ -194,11 +214,23 @@ export function useQueryDoc$ (collection, query, options) {
|
|
|
194
214
|
}
|
|
195
215
|
|
|
196
216
|
export function useBatchQueryDoc (collection, query, options) {
|
|
197
|
-
|
|
217
|
+
const normalized = normalizeQuery(query, 'useBatchQueryDoc')
|
|
218
|
+
const queryDoc = {
|
|
219
|
+
...normalized,
|
|
220
|
+
$limit: 1,
|
|
221
|
+
$sort: normalized.$sort || { createdAt: -1 }
|
|
222
|
+
}
|
|
223
|
+
const [docs, $collection] = useBatchQuery(collection, queryDoc, options)
|
|
224
|
+
if (!docs) return [undefined, undefined]
|
|
225
|
+
const doc = docs && docs[0]
|
|
226
|
+
const docId = doc?._id ?? doc?.id
|
|
227
|
+
const $doc = docId != null ? $collection[docId] : undefined
|
|
228
|
+
return [doc, $doc]
|
|
198
229
|
}
|
|
199
230
|
|
|
200
231
|
export function useBatchQueryDoc$ (collection, query, options) {
|
|
201
|
-
|
|
232
|
+
const [, $doc] = useBatchQueryDoc(collection, query, options)
|
|
233
|
+
return $doc
|
|
202
234
|
}
|
|
203
235
|
|
|
204
236
|
export function useAsyncQueryDoc (collection, query, options) {
|
|
@@ -276,3 +308,11 @@ function normalizeQuery (query, hookName) {
|
|
|
276
308
|
}
|
|
277
309
|
return query
|
|
278
310
|
}
|
|
311
|
+
|
|
312
|
+
const BATCH_SUB_OPTIONS = Object.freeze({
|
|
313
|
+
async: false,
|
|
314
|
+
batch: true,
|
|
315
|
+
// Batch hooks are a hard suspense barrier. Deferred params can skip the barrier
|
|
316
|
+
// on route transitions and cause immediate reads from stale/empty local nodes.
|
|
317
|
+
defer: false
|
|
318
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getRefLinks } from './refRegistry.js'
|
|
2
|
+
import { isCompatEnv } from '../compatEnv.js'
|
|
2
3
|
|
|
3
4
|
const modelListeners = {
|
|
4
5
|
change: new Map(),
|
|
@@ -6,10 +7,7 @@ const modelListeners = {
|
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function isModelEventsEnabled () {
|
|
9
|
-
return (
|
|
10
|
-
globalThis?.teamplayCompatibilityMode ??
|
|
11
|
-
(typeof process !== 'undefined' && process?.env?.TEAMPLAY_COMPAT === '1')
|
|
12
|
-
)
|
|
10
|
+
return isCompatEnv()
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export function normalizePattern (pattern, methodName) {
|