teamplay 0.4.0-alpha.21 → 0.4.0-alpha.23

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.
@@ -174,6 +174,46 @@ $alias.get() === $user.get() // false
174
174
  - No event emissions specific to refs.
175
175
  - No support for racer-style ref meta/options beyond the basic signature.
176
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` / `null` result clears target path via normal `set` semantics.
203
+ - Returns target signal.
204
+
205
+ ### stop(targetPath)
206
+
207
+ Stops a computation created with `start(targetPath, ...)`.
208
+ No-op if there is no active computation for the path.
209
+ Source of truth is root API (`$root.stop(...)`), but non-root calls are supported as sugar:
210
+ - `$scope.stop('a.b')` → `$root.stop('<scopePath>.a.b')`
211
+ - `$scope.stop()` → `$root.stop('<scopePath>')`
212
+
213
+ ```js
214
+ $root.stop('_virtual.lesson')
215
+ ```
216
+
177
217
  ### query(collection, query, options?)
178
218
 
179
219
  Creates a query signal **without** subscribing. Supports shorthand params:
@@ -97,7 +97,8 @@ export function useBatchDoc (collection, id, options) {
97
97
 
98
98
  export function useBatchDoc$ (collection, id, _options) {
99
99
  const $doc = getDocSignal(collection, id, 'useBatchDoc')
100
- return useSub($doc, undefined, { async: false, batch: true })
100
+ const options = _options ? { ..._options, ...BATCH_SUB_OPTIONS } : BATCH_SUB_OPTIONS
101
+ return useSub($doc, undefined, options)
101
102
  }
102
103
 
103
104
  export function useAsyncDoc$ (collection, id, options) {
@@ -140,7 +141,8 @@ export function useAsyncQuery (collection, query, options) {
140
141
 
141
142
  export function useBatchQuery$ (collection, query, _options) {
142
143
  const $collection = getCollectionSignal(collection, query, 'useBatchQuery')
143
- return useSub($collection, normalizeQuery(query, 'useBatchQuery'), { async: false, batch: true })
144
+ const options = _options ? { ..._options, ...BATCH_SUB_OPTIONS } : BATCH_SUB_OPTIONS
145
+ return useSub($collection, normalizeQuery(query, 'useBatchQuery'), options)
144
146
  }
145
147
 
146
148
  export function useBatchQuery (collection, query, options) {
@@ -306,3 +308,11 @@ function normalizeQuery (query, hookName) {
306
308
  }
307
309
  return query
308
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
+ })
@@ -0,0 +1,85 @@
1
+ import { observe, unobserve } from '@nx-js/observer-util'
2
+ import { getRoot } from '../Root.js'
3
+
4
+ const START_REACTIONS = Symbol('compat start reactions')
5
+
6
+ export function compatStartOnRoot ($root, targetPath, ...depsAndGetter) {
7
+ if (!isRootSignal($root)) throw Error('Signal.start() is only available on root signal')
8
+ if (typeof targetPath !== 'string') throw Error('Signal.start() expects targetPath to be a string')
9
+ if (depsAndGetter.length < 1) {
10
+ throw Error('Signal.start() expects targetPath, dependencies, and a getter function')
11
+ }
12
+ const getter = depsAndGetter[depsAndGetter.length - 1]
13
+ if (typeof getter !== 'function') {
14
+ throw Error('Signal.start() expects the last argument to be a getter function')
15
+ }
16
+ const deps = depsAndGetter.slice(0, -1)
17
+ const targetSegments = parsePathSegments(targetPath)
18
+ const $target = resolveSignal($root, targetSegments)
19
+ const targetKey = $target.path()
20
+
21
+ const store = getStartStore($root)
22
+ const existing = store.get(targetKey)
23
+ if (existing) existing.stop()
24
+
25
+ const reaction = observe(() => {
26
+ const resolvedDeps = deps.map(dep => resolveStartDep(dep, $root))
27
+ const nextValue = getter(...resolvedDeps)
28
+ const maybePromise = $target.set(nextValue)
29
+ if (maybePromise?.catch) maybePromise.catch(ignorePromiseRejection)
30
+ })
31
+ store.set(targetKey, { stop: () => unobserve(reaction) })
32
+ return $target
33
+ }
34
+
35
+ export function compatStopOnRoot ($root, targetPath) {
36
+ if (!isRootSignal($root)) throw Error('Signal.stop() is only available on root signal')
37
+ if (typeof targetPath !== 'string') throw Error('Signal.stop() expects targetPath to be a string')
38
+ const targetSegments = parsePathSegments(targetPath)
39
+ const $target = resolveSignal($root, targetSegments)
40
+ const targetKey = $target.path()
41
+ const store = getStartStore($root)
42
+ const existing = store.get(targetKey)
43
+ if (!existing) return
44
+ existing.stop()
45
+ store.delete(targetKey)
46
+ }
47
+
48
+ export function joinScopePath (scopePath, relativePath) {
49
+ if (typeof scopePath !== 'string') scopePath = ''
50
+ const segments = []
51
+ if (scopePath) segments.push(...parsePathSegments(scopePath))
52
+ if (relativePath) segments.push(...parsePathSegments(relativePath))
53
+ return segments.join('.')
54
+ }
55
+
56
+ function getStartStore ($root) {
57
+ $root[START_REACTIONS] ??= new Map()
58
+ return $root[START_REACTIONS]
59
+ }
60
+
61
+ function resolveStartDep (dep, $root) {
62
+ if (isSignalLike(dep)) return dep.get()
63
+ if (typeof dep === 'string') return resolveSignal($root, parsePathSegments(dep)).get()
64
+ return dep
65
+ }
66
+
67
+ function isSignalLike (value) {
68
+ return value && typeof value.path === 'function' && typeof value.get === 'function'
69
+ }
70
+
71
+ function parsePathSegments (path) {
72
+ return path.split('.').filter(Boolean)
73
+ }
74
+
75
+ function resolveSignal ($base, segments) {
76
+ let $cursor = $base
77
+ for (const segment of segments) $cursor = $cursor[segment]
78
+ return $cursor
79
+ }
80
+
81
+ function isRootSignal ($signal) {
82
+ return getRoot($signal) === $signal
83
+ }
84
+
85
+ function ignorePromiseRejection () {}
package/orm/SignalBase.js CHANGED
@@ -48,6 +48,7 @@ import { publicOnly } from './connection.js'
48
48
  import { DEFAULT_ID_FIELDS, getIdFieldsForSegments, isIdFieldPath, normalizeIdFields } from './idFields.js'
49
49
  import { isCompatEnv } from './compatEnv.js'
50
50
  import { resolveRefSegmentsSafe, resolveRefSignalSafe } from './Compat/refFallback.js'
51
+ import { compatStartOnRoot, compatStopOnRoot, joinScopePath } from './Compat/startStopCompat.js'
51
52
 
52
53
  export const SEGMENTS = Symbol('path segments targeting the particular node in the data tree')
53
54
  export const ARRAY_METHOD = Symbol('run array method on the signal')
@@ -522,6 +523,22 @@ export const extremelyLateBindings = {
522
523
  }
523
524
  }
524
525
  }
526
+
527
+ if (key === 'start') {
528
+ const [relativePath, ...depsAndGetter] = argumentsList
529
+ if (typeof relativePath !== 'string') throw Error('Signal.start() expects targetPath to be a string')
530
+ const absolutePath = joinScopePath($parent.path(), relativePath)
531
+ return compatStartOnRoot(getRoot($parent) || $parent, absolutePath, ...depsAndGetter)
532
+ }
533
+ if (key === 'stop') {
534
+ if (argumentsList.length > 1) throw Error('Signal.stop() expects zero or one argument')
535
+ const relativePath = argumentsList.length === 0 ? '' : argumentsList[0]
536
+ if (relativePath != null && typeof relativePath !== 'string') {
537
+ throw Error('Signal.stop() expects targetPath to be a string')
538
+ }
539
+ const absolutePath = joinScopePath($parent.path(), relativePath || '')
540
+ return compatStopOnRoot(getRoot($parent) || $parent, absolutePath)
541
+ }
525
542
  }
526
543
 
527
544
  throw Error(ERRORS.noSignalKey($parent, key))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.21",
3
+ "version": "0.4.0-alpha.23",
4
4
  "description": "Full-stack signals ORM with multiplayer",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -81,5 +81,5 @@
81
81
  ]
82
82
  },
83
83
  "license": "MIT",
84
- "gitHead": "5b440dc6e2e114f2cfaac81d4757b710f153ff5d"
84
+ "gitHead": "bcf9b46727a5fe9ee3836e582bf25b933fc7d479"
85
85
  }