teamplay 0.4.0-alpha.7 → 0.4.0-alpha.9
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/SignalCompat.js +3 -1
- package/orm/getSignal.js +3 -0
- package/package.json +9 -8
- package/orm/Compat/REF.md +0 -315
|
@@ -168,7 +168,9 @@ class SignalCompat extends Signal {
|
|
|
168
168
|
if (isRootCollectionCall) {
|
|
169
169
|
if (arguments.length !== 2) throw Error('Signal.add() expects (collection, object)')
|
|
170
170
|
if (!value || typeof value !== 'object') throw Error('Signal.add() expects an object argument')
|
|
171
|
-
|
|
171
|
+
const $root = getRoot(this) || this
|
|
172
|
+
const $collection = resolveSignal($root, [collectionOrValue])
|
|
173
|
+
return $collection.add(value)
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
if (arguments.length > 1) throw Error('Signal.add() expects a single argument')
|
package/orm/getSignal.js
CHANGED
|
@@ -49,6 +49,9 @@ export default function getSignal ($root, segments = [], {
|
|
|
49
49
|
// but without it calling the methods of root signal like $.get() doesn't work
|
|
50
50
|
proxy[ROOT] = $root || getSignal(undefined, [], { rootId: GLOBAL_ROOT_ID })
|
|
51
51
|
}
|
|
52
|
+
signal[ROOT] = proxy[ROOT]
|
|
53
|
+
} else {
|
|
54
|
+
signal[ROOT] = proxy
|
|
52
55
|
}
|
|
53
56
|
PROXY_TO_SIGNAL.set(proxy, signal)
|
|
54
57
|
const dependencies = []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.9",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@nx-js/observer-util": "^4.1.3",
|
|
36
36
|
"@startupjs/sharedb-mingo-memory": "^4.0.0-2",
|
|
37
|
-
"@teamplay/backend": "^0.4.0-alpha.
|
|
38
|
-
"@teamplay/cache": "^0.4.0-alpha.
|
|
39
|
-
"@teamplay/channel": "^0.4.0-alpha.
|
|
40
|
-
"@teamplay/debug": "^0.4.0-alpha.
|
|
41
|
-
"@teamplay/schema": "^0.4.0-alpha.
|
|
42
|
-
"@teamplay/utils": "^0.4.0-alpha.
|
|
37
|
+
"@teamplay/backend": "^0.4.0-alpha.9",
|
|
38
|
+
"@teamplay/cache": "^0.4.0-alpha.9",
|
|
39
|
+
"@teamplay/channel": "^0.4.0-alpha.9",
|
|
40
|
+
"@teamplay/debug": "^0.4.0-alpha.9",
|
|
41
|
+
"@teamplay/schema": "^0.4.0-alpha.9",
|
|
42
|
+
"@teamplay/utils": "^0.4.0-alpha.9",
|
|
43
43
|
"diff-match-patch": "^1.0.5",
|
|
44
44
|
"events": "^3.3.0",
|
|
45
45
|
"json0-ot-diff": "^1.1.2",
|
|
@@ -80,5 +80,6 @@
|
|
|
80
80
|
"<rootDir>/test_client/helpers"
|
|
81
81
|
]
|
|
82
82
|
},
|
|
83
|
-
"license": "MIT"
|
|
83
|
+
"license": "MIT",
|
|
84
|
+
"gitHead": "df4107d9fda0cdf2474c0dffd1c4f3c9527c8168"
|
|
84
85
|
}
|
package/orm/Compat/REF.md
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
# SignalCompat `ref` / `removeRef` — Compatibility Draft
|
|
2
|
-
|
|
3
|
-
This document captures a **draft** implementation of StartupJS/Racer-style `ref` behavior for Teamplay’s `SignalCompat`.
|
|
4
|
-
|
|
5
|
-
It is **not active in code** right now. The goal is to discuss and decide whether we want to bring it back, and in what form.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1) Why we need this
|
|
10
|
-
|
|
11
|
-
In LMS there are a few **real usages** of model refs (not React DOM refs):
|
|
12
|
-
|
|
13
|
-
- `components/Media/index.js`
|
|
14
|
-
```js
|
|
15
|
-
if ($fullscreen) $localFullscreen.ref($fullscreen)
|
|
16
|
-
```
|
|
17
|
-
- `main/components/FilterV2/index.js`
|
|
18
|
-
```js
|
|
19
|
-
if (!isMultiSelect) $localValue.ref($value)
|
|
20
|
-
```
|
|
21
|
-
- `main/Layout/Tutoring/index.js` and `v5/apps/main/Layout/Tutoring/index.js`
|
|
22
|
-
```js
|
|
23
|
-
$session.ref('tutoringSession', $tutoringSession)
|
|
24
|
-
$session.removeRef('tutoringSession')
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
These use the **Racer model ref**, which effectively makes one path behave like another path (alias). Teamplay doesn’t have this concept, so we explored a minimal compat layer.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 2) Target API (minimal subset)
|
|
32
|
-
|
|
33
|
-
We only target what LMS actually uses:
|
|
34
|
-
|
|
35
|
-
### `ref(target)`
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
$local.ref($.users.user1)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
This means `$local` mirrors `$users.user1` and mutating `$local` mutates `$users.user1`.
|
|
42
|
-
|
|
43
|
-
### `ref(subpath, target)`
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
$session.ref('tutoringSession', $tutoringSession)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
This means `$session.tutoringSession` acts as an alias to `$tutoringSession`.
|
|
50
|
-
|
|
51
|
-
### `removeRef(path?)`
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
$local.removeRef()
|
|
55
|
-
$session.removeRef('tutoringSession')
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Stops syncing.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 3) Semantics vs Racer
|
|
63
|
-
|
|
64
|
-
Racer refs are deep and complicated (they respond to all model events, including array insert/remove/move, etc).
|
|
65
|
-
|
|
66
|
-
This draft **only covers**:
|
|
67
|
-
- Signal-level aliasing (one signal proxies another).
|
|
68
|
-
- No `refList`, `refExtra`, `refMap`.
|
|
69
|
-
- No automatic path-patching for list inserts/moves.
|
|
70
|
-
|
|
71
|
-
It should be enough for current LMS usages.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## 4) Draft Implementation Strategy
|
|
76
|
-
|
|
77
|
-
### 4.1 Keep a ref store on root
|
|
78
|
-
|
|
79
|
-
We store refs on root signal:
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
const REFS = Symbol('compat refs')
|
|
83
|
-
$root[REFS] = new Map()
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Each entry is keyed by `fromPath` and stores `{ stop }` cleanup.
|
|
87
|
-
|
|
88
|
-
### 4.2 One-way reactive sync (target → alias)
|
|
89
|
-
|
|
90
|
-
We use `@nx-js/observer-util` `observe()` to track target changes and push them into alias:
|
|
91
|
-
|
|
92
|
-
```js
|
|
93
|
-
const toReaction = observe(() => {
|
|
94
|
-
const value = $to.get()
|
|
95
|
-
trackDeep(value)
|
|
96
|
-
setDiffDeepBypassRef($from, deepCopy(value))
|
|
97
|
-
}, { lazy: true })
|
|
98
|
-
|
|
99
|
-
toReaction()
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Why deep copy?
|
|
103
|
-
- Without it, `setDiffDeep` can re-use same object references and skip updates.
|
|
104
|
-
- Deep copy ensures the diffing path detects change.
|
|
105
|
-
|
|
106
|
-
### 4.3 Forward all mutations from alias → target
|
|
107
|
-
|
|
108
|
-
To avoid two reactions and feedback loops, we forward all mutator calls:
|
|
109
|
-
|
|
110
|
-
- `set`, `setNull`, `setDiffDeep`, `setEach`
|
|
111
|
-
- `del`
|
|
112
|
-
- `increment`
|
|
113
|
-
- `push`, `unshift`, `insert`, `pop`, `shift`, `remove`, `move`
|
|
114
|
-
- `stringInsert`, `stringRemove`
|
|
115
|
-
- `assign`
|
|
116
|
-
|
|
117
|
-
Forwarding uses a hidden `REF_TARGET` symbol on the alias signal.
|
|
118
|
-
|
|
119
|
-
### 4.4 Mutator forward mechanism
|
|
120
|
-
|
|
121
|
-
On each mutator:
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
const forwarded = forwardRef(this, 'set', arguments)
|
|
125
|
-
if (forwarded) return forwarded
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
`forwardRef()` resolves to a target signal if present and applies the same method there.
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## 5) Draft Code (for later restoration)
|
|
133
|
-
|
|
134
|
-
Below is the exact code we removed from `SignalCompat.js`. It can be re-applied as-is.
|
|
135
|
-
|
|
136
|
-
### 5.1 Imports (add back)
|
|
137
|
-
|
|
138
|
-
```js
|
|
139
|
-
import { raw, observe, unobserve } from '@nx-js/observer-util'
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### 5.2 Symbols and helpers (add near other helpers)
|
|
143
|
-
|
|
144
|
-
```js
|
|
145
|
-
const REFS = Symbol('compat refs')
|
|
146
|
-
const REF_TARGET = Symbol('compat ref target')
|
|
147
|
-
|
|
148
|
-
function getRefStore ($signal) {
|
|
149
|
-
const $root = getRoot($signal) || $signal
|
|
150
|
-
$root[REFS] ??= new Map()
|
|
151
|
-
return $root[REFS]
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function createRefLink ($from, $to) {
|
|
155
|
-
const toReaction = observe(() => {
|
|
156
|
-
const value = $to.get()
|
|
157
|
-
trackDeep(value)
|
|
158
|
-
setDiffDeepBypassRef($from, deepCopy(value))
|
|
159
|
-
}, { lazy: true })
|
|
160
|
-
|
|
161
|
-
// Prime sync and start tracking.
|
|
162
|
-
toReaction()
|
|
163
|
-
return () => {
|
|
164
|
-
unobserve(toReaction)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function trackDeep (value, seen = new Set()) {
|
|
169
|
-
if (!value || typeof value !== 'object') return
|
|
170
|
-
if (seen.has(value)) return
|
|
171
|
-
seen.add(value)
|
|
172
|
-
if (Array.isArray(value)) {
|
|
173
|
-
for (const item of value) trackDeep(item, seen)
|
|
174
|
-
} else {
|
|
175
|
-
for (const key in value) {
|
|
176
|
-
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
177
|
-
trackDeep(value[key], seen)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function resolveRefSignal ($signal) {
|
|
184
|
-
let current = $signal
|
|
185
|
-
const seen = new Set()
|
|
186
|
-
while (current && current[REF_TARGET]) {
|
|
187
|
-
if (seen.has(current)) break
|
|
188
|
-
seen.add(current)
|
|
189
|
-
current = current[REF_TARGET]
|
|
190
|
-
}
|
|
191
|
-
return current
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function forwardRef ($signal, methodName, args) {
|
|
195
|
-
const $target = resolveRefSignal($signal)
|
|
196
|
-
if ($target === $signal) return null
|
|
197
|
-
return SignalCompat.prototype[methodName].apply($target, args)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function setDiffDeepBypassRef ($signal, value) {
|
|
201
|
-
return Signal.prototype.set.call($signal, value)
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### 5.3 `ref()` / `removeRef()` methods (add to `SignalCompat`)
|
|
206
|
-
|
|
207
|
-
```js
|
|
208
|
-
ref (path, target, options) {
|
|
209
|
-
if (arguments.length > 3) throw Error('Signal.ref() expects one to three arguments')
|
|
210
|
-
let $from = this
|
|
211
|
-
let $to
|
|
212
|
-
if (arguments.length === 1) {
|
|
213
|
-
$to = resolveRefTarget(this, path, 'Signal.ref()')
|
|
214
|
-
} else if (arguments.length === 2) {
|
|
215
|
-
if (isSignalLike(target) || typeof target === 'string') {
|
|
216
|
-
const segments = parseAtSubpath(path, 1, 'Signal.ref()')
|
|
217
|
-
$from = resolveSignal(this, segments)
|
|
218
|
-
$to = resolveRefTarget(this, target, 'Signal.ref()')
|
|
219
|
-
} else {
|
|
220
|
-
$to = resolveRefTarget(this, path, 'Signal.ref()')
|
|
221
|
-
options = target
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
const segments = parseAtSubpath(path, 1, 'Signal.ref()')
|
|
225
|
-
$from = resolveSignal(this, segments)
|
|
226
|
-
$to = resolveRefTarget(this, target, 'Signal.ref()')
|
|
227
|
-
}
|
|
228
|
-
if (!$to) throw Error('Signal.ref() expects a target path or signal')
|
|
229
|
-
if ($from === $to) return $from
|
|
230
|
-
const store = getRefStore($from)
|
|
231
|
-
const fromPath = $from.path()
|
|
232
|
-
const existing = store.get(fromPath)
|
|
233
|
-
if (existing) existing.stop()
|
|
234
|
-
const stop = createRefLink($from, $to, options)
|
|
235
|
-
store.set(fromPath, { stop })
|
|
236
|
-
$from[REF_TARGET] = $to
|
|
237
|
-
return $from
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
removeRef (path) {
|
|
241
|
-
if (arguments.length > 1) throw Error('Signal.removeRef() expects a single argument')
|
|
242
|
-
let $from = this
|
|
243
|
-
if (arguments.length === 1) {
|
|
244
|
-
const segments = parseAtSubpath(path, 1, 'Signal.removeRef()')
|
|
245
|
-
$from = resolveSignal(this, segments)
|
|
246
|
-
}
|
|
247
|
-
const store = getRefStore($from)
|
|
248
|
-
const fromPath = $from.path()
|
|
249
|
-
const existing = store.get(fromPath)
|
|
250
|
-
if (existing) {
|
|
251
|
-
existing.stop()
|
|
252
|
-
store.delete(fromPath)
|
|
253
|
-
}
|
|
254
|
-
if ($from[REF_TARGET]) delete $from[REF_TARGET]
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### 5.4 Forwarding mutations (add to each mutator)
|
|
259
|
-
|
|
260
|
-
Example for `set()`:
|
|
261
|
-
|
|
262
|
-
```js
|
|
263
|
-
async set (path, value) {
|
|
264
|
-
const forwarded = forwardRef(this, 'set', arguments)
|
|
265
|
-
if (forwarded) return forwarded
|
|
266
|
-
// ...existing body
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Same pattern for:
|
|
271
|
-
- `setNull`, `setDiffDeep`, `setEach`
|
|
272
|
-
- `del`
|
|
273
|
-
- `increment`
|
|
274
|
-
- `push`, `unshift`, `insert`, `pop`, `shift`, `remove`, `move`
|
|
275
|
-
- `stringInsert`, `stringRemove`
|
|
276
|
-
- `assign`
|
|
277
|
-
|
|
278
|
-
### 5.5 Supporting helpers (only needed with ref)
|
|
279
|
-
|
|
280
|
-
```js
|
|
281
|
-
function isSignalLike (value) {
|
|
282
|
-
return value && typeof value.path === 'function' && typeof value.get === 'function'
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function resolveRefTarget ($signal, target, methodName) {
|
|
286
|
-
if (isSignalLike(target)) return target
|
|
287
|
-
if (typeof target === 'string') {
|
|
288
|
-
const segments = parseAtSubpath(target, 1, methodName)
|
|
289
|
-
const $root = getRoot($signal) || $signal
|
|
290
|
-
return resolveSignal($root, segments)
|
|
291
|
-
}
|
|
292
|
-
return undefined
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 6) Draft tests (removed)
|
|
299
|
-
|
|
300
|
-
We also had tests in `packages/teamplay/test/signalCompat.js`. They can be restored if needed:
|
|
301
|
-
|
|
302
|
-
- `syncs values both ways for direct signals`
|
|
303
|
-
- `supports subpath refs from root`
|
|
304
|
-
- `removeRef stops syncing`
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## 7) Risks and limitations
|
|
309
|
-
|
|
310
|
-
- This is **not a full racer ref** implementation.
|
|
311
|
-
- No support for `refList`, `refExtra`, `refMap`.
|
|
312
|
-
- No array index patching when list changes.
|
|
313
|
-
- Might not handle exotic cases with cyclic refs.
|
|
314
|
-
|
|
315
|
-
That said, it’s deliberately scoped to known LMS usage patterns and should be “good enough” for those.
|