teamplay 0.4.0-alpha.7 → 0.4.0-alpha.71

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/README.md CHANGED
@@ -19,6 +19,18 @@ Features:
19
19
 
20
20
  For installation and documentation see [teamplay.dev](https://teamplay.dev)
21
21
 
22
+ ## ORM Compat Helpers
23
+
24
+ For legacy Racer-style model mixins (for example versioning libraries which call
25
+ `getAssociations()`), use ORM compat helpers from the `teamplay/orm` subpath:
26
+
27
+ ```js
28
+ import BaseModel, { hasMany, hasOne, belongsTo } from 'teamplay/orm'
29
+ ```
30
+
31
+ These helpers attach class-level associations and expose them through
32
+ `$doc.getAssociations()` on model signals.
33
+
22
34
  ## License
23
35
 
24
36
  MIT
package/index.d.ts CHANGED
@@ -94,6 +94,8 @@ export function useDidUpdate (fn: () => EffectCleanup, deps?: any[]): void
94
94
  export function useOnce (condition: any, fn: () => EffectCleanup): void
95
95
  export function useSyncEffect (fn: () => EffectCleanup, deps?: any[]): void
96
96
  export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
97
+ export function getSubscriptionGcDelay (): number
98
+ export function setSubscriptionGcDelay (ms?: number | null): number
97
99
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
98
100
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
99
101
  export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
package/index.js CHANGED
@@ -66,6 +66,7 @@ export {
66
66
  useSyncEffect
67
67
  } from './react/helpers.js'
68
68
  export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
69
+ export { getSubscriptionGcDelay, setSubscriptionGcDelay } from './orm/subscriptionGcDelay.js'
69
70
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
70
71
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
71
72
  export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
@@ -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)
@@ -113,6 +116,8 @@ $.users.user1.scope('users.user2')
113
116
  Creates a lightweight alias between signals (minimal Racer-style ref).
114
117
  Mutations on the alias are forwarded to the target. The alias mirrors target updates.
115
118
  Reads (`get`/`peek`) are forwarded to the target while the ref is active.
119
+ Ref mirroring is scheduled through Teamplay runtime scheduler, so updates remain batch-friendly
120
+ and do not leak intermediate ref states during a single batched cycle.
116
121
 
117
122
  ```js
118
123
  const $local = $.local.value
@@ -171,6 +176,49 @@ $alias.get() === $user.get() // false
171
176
  - No event emissions specific to refs.
172
177
  - No support for racer-style ref meta/options beyond the basic signature.
173
178
 
179
+ ### start(targetPath, ...deps, getter)
180
+
181
+ Legacy computed binding API from Racer/StartupJS.
182
+ Creates a reactive computation and writes its result into `targetPath`.
183
+ Source of truth is root API (`$root.start(...)`), but non-root calls are supported as sugar:
184
+ - `$scope.start('a.b', ...deps, getter)` → `$root.start('<scopePath>.a.b', ...deps, getter)`
185
+
186
+ - `targetPath`: string path where computed value is written.
187
+ - `deps`: dependencies used by `getter`.
188
+ - `getter`: function called as `getter(...resolvedDeps)`.
189
+
190
+ Dependency resolution:
191
+ - Signal-like dep (`$doc`, `$session.user`) → `dep.get()`.
192
+ - String dep (`'settings.theme'`) → `$root.get(dep)`.
193
+ - Any other dep → passed as-is.
194
+
195
+ ```js
196
+ $root.start('_virtual.lesson', $.lessons[lessonId], '_session.userId', (lesson, userId) => {
197
+ if (!lesson) return undefined
198
+ return { stageIds: lesson.stageIds, userId }
199
+ })
200
+ ```
201
+
202
+ Behavior:
203
+ - Calling `start()` again for the same `targetPath` replaces previous reaction.
204
+ - `undefined` result applies compat delete semantics at target path.
205
+ - `null` result is stored as `null`.
206
+ - Returns target signal.
207
+ - If any dependency temporarily suspends (throws a Promise/thenable), compat skips the whole tick (getter is not called and target is not written).
208
+ - If `getter` throws a Promise, compat skips that tick and retries on next reactive update.
209
+
210
+ ### stop(targetPath)
211
+
212
+ Stops a computation created with `start(targetPath, ...)`.
213
+ No-op if there is no active computation for the path.
214
+ Source of truth is root API (`$root.stop(...)`), but non-root calls are supported as sugar:
215
+ - `$scope.stop('a.b')` → `$root.stop('<scopePath>.a.b')`
216
+ - `$scope.stop()` → `$root.stop('<scopePath>')`
217
+
218
+ ```js
219
+ $root.stop('_virtual.lesson')
220
+ ```
221
+
174
222
  ### query(collection, query, options?)
175
223
 
176
224
  Creates a query signal **without** subscribing. Supports shorthand params:
@@ -202,6 +250,19 @@ await $.subscribe($user, $$active)
202
250
  $.unsubscribe($user, $$active)
203
251
  ```
204
252
 
253
+ ### close(callback?)
254
+
255
+ Compatibility shim for legacy `model.close()` calls.
256
+
257
+ - In Teamplay, `$`/`model` is a global root signal (not a per-request Racer model instance).
258
+ - Therefore `close()` is intentionally a no-op.
259
+ - Optional callback is supported and called immediately.
260
+
261
+ ```js
262
+ model.close()
263
+ model.close(() => console.log('closed'))
264
+ ```
265
+
205
266
  ### fetch(...signals) / unfetch(...signals)
206
267
 
207
268
  Fetch-only variants of `subscribe` / `unsubscribe`. They load data once without a live subscription.
@@ -236,6 +297,7 @@ Returns the current value and tracks reactivity.
236
297
  const name = $.users.user1.name.get()
237
298
  $root.get('$render.url')
238
299
  $user.get('profile.name')
300
+ $user.get('profile', 'name')
239
301
  ```
240
302
 
241
303
  ### peek(subpath?)
@@ -245,6 +307,7 @@ Returns the current value **without** tracking reactivity.
245
307
  ```js
246
308
  const name = $.users.user1.name.peek()
247
309
  $user.peek('profile.name')
310
+ $user.peek('profile', 'name')
248
311
  ```
249
312
 
250
313
  ### getCopy(subpath)
@@ -306,6 +369,44 @@ for (const $doc of $query) {
306
369
  }
307
370
  ```
308
371
 
372
+ ### Mutator Semantics (Core vs Compat)
373
+
374
+ Compatibility mode intentionally aligns mutators with Racer. This differs from core `Signal` behavior.
375
+
376
+ | API | Core (`Signal`) | Compat (`SignalCompat`) |
377
+ | --- | --- | --- |
378
+ | `set` | Uses deep-diff path (`dataTree.set` + internal `setDiffDeep`). | Path-targeted replace semantics, Racer-like. `undefined` keeps delete semantics. |
379
+ | `setEach` | Not a special API in core mutators. | Per-key compat `set` (not `assign` merge/delete behavior). |
380
+ | `setDiffDeep` | Deep-diff engine (`utils/setDiffDeep.js`). | Recursive Racer-like diff implemented via compat mutators (`set` / `del`) on nested paths. |
381
+ | `setDiff` | N/A as compat shim. | Alias to compat `set` for both signatures: `setDiff(value)` and `setDiff(path, value)`. |
382
+
383
+ Migration note: compat behavior is intentionally Racer-aligned and may differ from core mutators.
384
+ Composite compat mutators (`setEach`, `setDiffDeep`) apply updates atomically for Teamplay-scheduled observers via the runtime batch scheduler.
385
+
386
+ ### Subscription GC Delay (Compat)
387
+
388
+ To reduce UI blink on rapid `unsub -> sub` cycles, compat uses an unload grace period for docs/queries.
389
+
390
+ - Default in compat: `300ms`
391
+ - Default in non-compat: `0ms` (immediate cleanup)
392
+
393
+ You can tune it globally:
394
+
395
+ ```js
396
+ import { getSubscriptionGcDelay, setSubscriptionGcDelay } from 'teamplay'
397
+
398
+ setSubscriptionGcDelay(500)
399
+ console.log(getSubscriptionGcDelay()) // 500
400
+ ```
401
+
402
+ When refCount drops to `0`, unsubscribe/destroy is scheduled after this delay.
403
+ If a new subscribe arrives before timeout, pending destroy is cancelled and the same doc/query instance is reused.
404
+
405
+ Compat queries also retain lifecycle ownership of docs they materialize into DataTree.
406
+ This means a doc that arrived through `useQuery` / `useBatchQuery` will stay available
407
+ for immediate `useLocal` / `useModel` reads while that query remains subscribed, even if
408
+ some unrelated `useDoc` subscriber for the same `collection.id` unmounts.
409
+
309
410
  ### set(value) and set(path, value)
310
411
 
311
412
  `SignalCompat` accepts both:
@@ -315,7 +416,14 @@ $.users.user1.name.set('Alice')
315
416
  $.users.user1.set('profile.name', 'Alice')
316
417
  ```
317
418
 
318
- In compat mode, `set` replaces values at the target path.
419
+ In compat mode, `set` replaces the value at the target path.
420
+ - `set(path, null)` stores `null`.
421
+ - `set(path, undefined)` applies current delete semantics.
422
+
423
+ ```js
424
+ await $.users.user1.set('profile', { name: 'Ann', role: 'student' })
425
+ await $.users.user1.set('profile', { name: 'Kate' }) // role is removed
426
+ ```
319
427
 
320
428
  ### setNull(path?, value)
321
429
 
@@ -327,26 +435,47 @@ $.config.setNull('theme', 'light')
327
435
 
328
436
  ### setDiffDeep(path?, value)
329
437
 
330
- Applies a diff-deep update (uses base `Signal.set` internally).
438
+ Applies a recursive Racer-like diff using compat mutators (`set` / `del`) on subpaths.
439
+ This is intentionally a compat implementation detail and differs from core deep-diff internals.
331
440
 
332
441
  ```js
333
- $.users.user1.setDiffDeep({ profile: { name: 'Alice' } })
442
+ await $.users.user1.set({ profile: { name: 'Ann', role: 'student' } })
443
+ await $.users.user1.setDiffDeep({ profile: { name: 'Kate' } }) // deep-diff path
334
444
  ```
335
445
 
336
446
  ### setDiff(path?, value)
337
447
 
338
- Alias for `set()` in compat. Accepts the same arguments and semantics.
448
+ Alias for compat `set` in both forms:
449
+ - `setDiff(value)` -> same as `set(value)`
450
+ - `setDiff(path, value)` -> same as `set(path, value)`
339
451
 
340
452
  ```js
341
- $.users.user1.setDiff({ profile: { name: 'Alice' } })
453
+ await $.users.user1.setDiff({ profile: { name: 'Kate' } })
454
+ await $.users.user1.setDiff('profile', { name: 'Bob' })
342
455
  ```
343
456
 
344
457
  ### setEach(path?, object)
345
458
 
346
- Shorthand for assign. Sets or deletes fields from an object.
459
+ Racer-like per-key set. `setEach` iterates keys and applies compat `set` for each key.
460
+ - `setEach({ k: null })` stores `null`.
461
+ - `setEach({ k: undefined })` applies current delete semantics.
347
462
 
348
463
  ```js
349
- $.users.user1.setEach({ name: 'Bob', age: 30 })
464
+ await $.users.user1.setEach({ name: 'Bob', age: null })
465
+ ```
466
+
467
+ ### Null / Undefined Matrix (Compat)
468
+
469
+ | Call | Result |
470
+ | --- | --- |
471
+ | `set(path, null)` | stores `null` at `path` |
472
+ | `set(path, undefined)` | applies delete semantics at `path` |
473
+ | `setEach({ k: null })` | stores `null` for `k` |
474
+ | `setEach({ k: undefined })` | applies delete semantics for `k` |
475
+
476
+ ```js
477
+ await $.users.user1.set('status', null) // status === null
478
+ await $.users.user1.setEach({ status: undefined }) // status deleted
350
479
  ```
351
480
 
352
481
  ### assign(object)
@@ -360,6 +489,8 @@ $.users.user1.assign({ name: 'Bob', age: null })
360
489
  ### del(path?)
361
490
 
362
491
  Deletes a value. Can be used with a subpath.
492
+ In compat mode, deleting a non-existing **public** document (or its subpath) is a no-op
493
+ to match legacy racer behavior.
363
494
 
364
495
  ```js
365
496
  $.users.user1.del('profile.name')
@@ -478,8 +609,12 @@ They are designed to behave close to StartupJS hooks, but adapted to Teamplay’
478
609
  General notes:
479
610
  - Hooks should be used inside `observer()` components to get reactive updates.
480
611
  - Sync hooks (`useDoc`, `useQuery`) use Suspense by default (via `useSub`).
612
+ - In compatibility mode, sync hooks are strict (`defer: false`) to match racer-like
613
+ semantics and avoid transient `undefined` / empty snapshots during fast navigation.
614
+ This is enforced by compat hooks (user `defer` option is ignored for sync hooks).
481
615
  - Async hooks (`useAsyncDoc`, `useAsyncQuery`) never throw; they return `undefined` until ready.
482
- - Batch hooks are **aliases**, no batching is implemented.
616
+ - Batch hooks use a Suspense batch barrier (`useBatch`) and wait for both
617
+ subscribe promises and DataTree materialization readiness.
483
618
 
484
619
  ### Events
485
620
 
@@ -652,10 +787,12 @@ if (!user) return 'Loading...'
652
787
 
653
788
  Returns `undefined` until subscription resolves.
654
789
 
655
- #### Batch aliases
790
+ #### Batch variants
656
791
 
657
- `useBatchDoc` / `useBatchDoc$` are aliases to `useDoc` / `useDoc$`.
658
- Batching is not implemented in Teamplay.
792
+ `useBatchDoc` / `useBatchDoc$` participate in batch Suspense flow:
793
+ - they register subscribe promises for `useBatch()`;
794
+ - they also register a **materialization readiness check**:
795
+ doc is considered ready only when it is visible in DataTree (or explicitly missing).
659
796
 
660
797
  ### Query Hooks
661
798
 
@@ -672,10 +809,12 @@ This matches StartupJS and makes updates easy:
672
809
  $users[userId].name.set('New Name')
673
810
  ```
674
811
 
675
- `useQuery$` returns the collection signal as well:
812
+ `useQuery$` returns the **query signal**:
676
813
 
677
814
  ```js
678
- const $users = useQuery$('users', { active: true })
815
+ const $query = useQuery$('users', { active: true })
816
+ const ids = $query.getIds()
817
+ const docs = $query.get()
679
818
  ```
680
819
 
681
820
  If `query == null`, a warning is logged and `{ _id: '__NON_EXISTENT__' }` is used.
@@ -690,9 +829,18 @@ if (!users) return 'Loading...'
690
829
 
691
830
  Async variant: no Suspense, returns `undefined` until ready.
692
831
 
693
- #### Batch aliases
832
+ #### Batch variants
694
833
 
695
- `useBatchQuery` / `useBatchQuery$` are aliases to `useQuery` / `useQuery$`.
834
+ `useBatchQuery` / `useBatchQuery$` participate in batch Suspense flow:
835
+ - they register subscribe promises for `useBatch()`;
836
+ - they register a **query readiness check**:
837
+ query ids must be materialized in DataTree, and each `collection.id` from ids must
838
+ be visible in DataTree (or explicitly missing).
839
+ - for `$aggregate` queries, readiness is query-level:
840
+ DataTree must have `$queries.<hash>.docs` (array, including empty), or `extra`.
841
+ Aggregate rows are not required to exist as `collection.<id>` docs.
842
+ Presence of `$queries.<hash>.ids` alone does not mark aggregate readiness.
843
+ For Teamplay aggregation subscriptions, `$aggregations.<hash>` also marks readiness.
696
844
 
697
845
  ### Query Helpers
698
846
 
@@ -706,7 +854,7 @@ const [users] = useQueryIds('users', ['b', 'a'])
706
854
  Options:
707
855
  - `reverse: true` — reverse order of IDs before mapping.
708
856
 
709
- `useBatchQueryIds` and `useAsyncQueryIds` are alias/async variants.
857
+ `useBatchQueryIds` and `useAsyncQueryIds` are batch/async variants.
710
858
 
711
859
  #### `useQueryDoc`
712
860
 
@@ -721,16 +869,20 @@ Implementation details:
721
869
  - Adds default `$sort: { createdAt: -1 }` if `$sort` is missing
722
870
 
723
871
  `useQueryDoc$` returns only the doc signal (or `undefined`).
724
- `useBatchQueryDoc` / `useAsyncQueryDoc` are alias/async variants.
872
+ `useBatchQueryDoc` / `useAsyncQueryDoc` are batch/async variants.
725
873
 
726
- ### Batching Placeholder
874
+ ### Batch Barrier
727
875
 
728
- `useBatch()` is a no-op placeholder.
729
- All batch hooks are **aliases** to their non-batch versions.
876
+ `useBatch()` is a Suspense barrier for batch hooks.
730
877
 
731
- ```js
732
- useBatch() // does nothing in Teamplay
733
- ```
878
+ It throws while:
879
+ - batch subscribe promises are pending;
880
+ - or subscribe promises are resolved but requested docs/queries are not yet
881
+ materialized in DataTree.
882
+
883
+ After `useBatch()` stops throwing in compat mode, immediate reads via
884
+ `useLocal(...).get(...)` for already requested batch entities should not produce
885
+ transient `undefined` caused by materialization races.
734
886
 
735
887
  ## Examples
736
888
 
@@ -758,6 +910,8 @@ const Component = observer(() => {
758
910
  ```js
759
911
  const Component = observer(() => {
760
912
  const [users, $users] = useQuery('users', { active: true })
913
+ const $query = useQuery$('users', { active: true })
914
+ const ids = $query.getIds()
761
915
  return (
762
916
  <>
763
917
  {users.map(u => <div key={u._id}>{u.name}</div>)}