teamplay 0.1.10 → 0.1.12
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/index.js +1 -1
- package/orm/Doc.js +5 -2
- package/orm/Query.js +8 -3
- package/orm/Signal.js +8 -0
- package/orm/connection.js +5 -0
- package/package.json +8 -8
- package/react/convertToObserver.js +88 -55
- package/react/helpers.js +12 -8
- package/react/trapRender.js +6 -12
- package/react/universal$.js +11 -3
- package/react/universalSub.js +8 -1
- package/react/wrapIntoSuspense.js +27 -41
- package/server.js +3 -1
package/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export const $ = _getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universa
|
|
|
15
15
|
export default $
|
|
16
16
|
export { default as sub } from './react/universalSub.js'
|
|
17
17
|
export { default as observer } from './react/observer.js'
|
|
18
|
-
export { connection, setConnection, getConnection, fetchOnly, setFetchOnly } from './orm/connection.js'
|
|
18
|
+
export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
|
|
19
19
|
export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
|
|
20
20
|
export { aggregation, aggregationHeader } from '@teamplay/utils/aggregation'
|
|
21
21
|
|
package/orm/Doc.js
CHANGED
|
@@ -80,7 +80,9 @@ class Doc {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
async unsubscribe () {
|
|
83
|
-
if (!this.subscribed)
|
|
83
|
+
if (!this.subscribed) {
|
|
84
|
+
throw Error('trying to unsubscribe while not subscribed. Doc: ' + [this.collection, this.docId])
|
|
85
|
+
}
|
|
84
86
|
this.subscribed = undefined
|
|
85
87
|
// if we are still handling the subscription, just wait for it to finish and then unsubscribe
|
|
86
88
|
if (this.subscribing) {
|
|
@@ -216,7 +218,8 @@ class DocSubscriptions {
|
|
|
216
218
|
if (!doc) return
|
|
217
219
|
this.subCount.delete(hash)
|
|
218
220
|
this.initialized.delete(hash)
|
|
219
|
-
|
|
221
|
+
// If the document was initialized as part of query and wasn't directly subscribed to, we should not unsubscribe from it.
|
|
222
|
+
if (doc.subscribed) await doc.unsubscribe()
|
|
220
223
|
if (doc.subscribed) return // if we subscribed again while waiting for unsubscribe, we don't delete the doc
|
|
221
224
|
this.docs.delete(hash)
|
|
222
225
|
}
|
package/orm/Query.js
CHANGED
|
@@ -87,7 +87,9 @@ class Query {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async unsubscribe () {
|
|
90
|
-
if (!this.subscribed)
|
|
90
|
+
if (!this.subscribed) {
|
|
91
|
+
throw Error('trying to unsubscribe while not subscribed. Query: ' + [this.collection, this.params])
|
|
92
|
+
}
|
|
91
93
|
this.subscribed = undefined
|
|
92
94
|
// if we are still handling the subscription, just wait for it to finish and then unsubscribe
|
|
93
95
|
if (this.subscribing) {
|
|
@@ -227,7 +229,7 @@ class QuerySubscriptions {
|
|
|
227
229
|
let count = this.subCount.get(hash) || 0
|
|
228
230
|
count -= 1
|
|
229
231
|
if (count < 0) {
|
|
230
|
-
if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($query)
|
|
232
|
+
if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw Error(ERRORS.notSubscribed($query))
|
|
231
233
|
return
|
|
232
234
|
}
|
|
233
235
|
if (count > 0) {
|
|
@@ -275,5 +277,8 @@ export function getQuerySignal (segments, params, options) {
|
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
const ERRORS = {
|
|
278
|
-
notSubscribed: $
|
|
280
|
+
notSubscribed: $query => `
|
|
281
|
+
trying to unsubscribe when not subscribed. Query:
|
|
282
|
+
${[$query[SEGMENTS], $query[PARAMS]]}
|
|
283
|
+
`
|
|
279
284
|
}
|
package/orm/Signal.js
CHANGED
|
@@ -16,6 +16,7 @@ import { get as _get, set as _set, del as _del, setPublicDoc as _setPublicDoc }
|
|
|
16
16
|
import getSignal, { rawSignal } from './getSignal.js'
|
|
17
17
|
import { IS_QUERY, HASH, QUERIES } from './Query.js'
|
|
18
18
|
import { ROOT_FUNCTION, getRoot } from './Root.js'
|
|
19
|
+
import { publicOnly } from './connection.js'
|
|
19
20
|
|
|
20
21
|
export const SEGMENTS = Symbol('path segments targeting the particular node in the data tree')
|
|
21
22
|
|
|
@@ -82,6 +83,7 @@ export default class Signal extends Function {
|
|
|
82
83
|
if (isPublicCollection(this[SEGMENTS][0])) {
|
|
83
84
|
await _setPublicDoc(this[SEGMENTS], value)
|
|
84
85
|
} else {
|
|
86
|
+
if (publicOnly) throw Error(ERRORS.publicOnly)
|
|
85
87
|
_set(this[SEGMENTS], value)
|
|
86
88
|
}
|
|
87
89
|
}
|
|
@@ -98,6 +100,7 @@ export default class Signal extends Function {
|
|
|
98
100
|
// TODO: implement a json0 operation for pop
|
|
99
101
|
async pop () {
|
|
100
102
|
if (arguments.length > 0) throw Error('Signal.pop() does not accept any arguments')
|
|
103
|
+
if (this[SEGMENTS].length < 2) throw Error('Can\'t pop from a collection or root signal')
|
|
101
104
|
if (this[IS_QUERY]) throw Error('Signal.pop() can\'t be used on a query signal')
|
|
102
105
|
const array = this.get()
|
|
103
106
|
if (!Array.isArray(array) || array.length === 0) return
|
|
@@ -144,6 +147,7 @@ export default class Signal extends Function {
|
|
|
144
147
|
if (this[SEGMENTS].length === 1) throw Error('Can\'t delete the whole collection')
|
|
145
148
|
await _setPublicDoc(this[SEGMENTS], undefined, true)
|
|
146
149
|
} else {
|
|
150
|
+
if (publicOnly) throw Error(ERRORS.publicOnly)
|
|
147
151
|
_del(this[SEGMENTS])
|
|
148
152
|
}
|
|
149
153
|
}
|
|
@@ -241,5 +245,9 @@ const ERRORS = {
|
|
|
241
245
|
noRootFunction: `
|
|
242
246
|
Root signal does not have a root function set.
|
|
243
247
|
You must use getRootSignal({ rootId, rootFunction }) to create a root signal.
|
|
248
|
+
`,
|
|
249
|
+
publicOnly: `
|
|
250
|
+
Can't modify private collections data when 'publicOnly' is enabled.
|
|
251
|
+
On the server you can only work with public collections.
|
|
244
252
|
`
|
|
245
253
|
}
|
package/orm/connection.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export let connection
|
|
2
2
|
export let fetchOnly
|
|
3
|
+
export let publicOnly
|
|
3
4
|
|
|
4
5
|
export function setConnection (_connection) {
|
|
5
6
|
connection = _connection
|
|
@@ -14,6 +15,10 @@ export function setFetchOnly (_fetchOnly) {
|
|
|
14
15
|
fetchOnly = _fetchOnly
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function setPublicOnly (_publicOnly) {
|
|
19
|
+
publicOnly = _publicOnly
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
const ERRORS = {
|
|
18
23
|
notSet: `
|
|
19
24
|
Connection is not set.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@nx-js/observer-util": "^4.1.3",
|
|
26
|
-
"@teamplay/backend": "^0.1.
|
|
27
|
-
"@teamplay/cache": "^0.1.
|
|
28
|
-
"@teamplay/channel": "^0.1.
|
|
29
|
-
"@teamplay/debug": "^0.1.
|
|
30
|
-
"@teamplay/schema": "^0.1.
|
|
31
|
-
"@teamplay/utils": "^0.1.
|
|
26
|
+
"@teamplay/backend": "^0.1.12",
|
|
27
|
+
"@teamplay/cache": "^0.1.12",
|
|
28
|
+
"@teamplay/channel": "^0.1.12",
|
|
29
|
+
"@teamplay/debug": "^0.1.12",
|
|
30
|
+
"@teamplay/schema": "^0.1.12",
|
|
31
|
+
"@teamplay/utils": "^0.1.12",
|
|
32
32
|
"diff-match-patch": "^1.0.5",
|
|
33
33
|
"events": "^3.3.0",
|
|
34
34
|
"json0-ot-diff": "^1.1.2",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"license": "MIT",
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "63672ca0f00e682d687585088094a59746a42015"
|
|
67
67
|
}
|
|
@@ -1,93 +1,126 @@
|
|
|
1
1
|
// TODO: rewrite to use useSyncExternalStore like in mobx. This will also help with handling Suspense abandonment better
|
|
2
2
|
// to cleanup the observer() reaction when the component is unmounted or was abandoned and unmounts will never trigger.
|
|
3
3
|
// ref: https://github.com/mobxjs/mobx/blob/94bc4997c14152ff5aefcaac64d982d5c21ba51a/packages/mobx-react-lite/src/useObserver.ts
|
|
4
|
-
import {
|
|
5
|
-
import _throttle from 'lodash/throttle.js'
|
|
4
|
+
import { forwardRef as _forwardRef, useRef, useSyncExternalStore } from 'react'
|
|
6
5
|
import { observe, unobserve } from '@nx-js/observer-util'
|
|
7
|
-
import
|
|
6
|
+
import _throttle from 'lodash/throttle.js'
|
|
7
|
+
import { createCaches, getDummyCache } from '@teamplay/cache'
|
|
8
|
+
import { __increment, __decrement } from '@teamplay/debug'
|
|
9
|
+
import executionContextTracker from './executionContextTracker.js'
|
|
10
|
+
import { pipeComponentMeta, useUnmount, useId } from './helpers.js'
|
|
8
11
|
import trapRender from './trapRender.js'
|
|
9
12
|
|
|
10
13
|
const DEFAULT_THROTTLE_TIMEOUT = 100
|
|
11
14
|
|
|
12
|
-
export default function convertToObserver (BaseComponent,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
export default function convertToObserver (BaseComponent, {
|
|
16
|
+
forwardRef,
|
|
17
|
+
cache: enableCache = true,
|
|
18
|
+
throttle,
|
|
19
|
+
...options
|
|
20
|
+
} = {}) {
|
|
21
|
+
throttle = normalizeThrottle(throttle)
|
|
15
22
|
// MAGIC. This fixes hot-reloading. TODO: figure out WHY it fixes it
|
|
16
|
-
const random = Math.random()
|
|
23
|
+
// const random = Math.random()
|
|
17
24
|
|
|
18
25
|
// memo; we are not intested in deep updates
|
|
19
26
|
// in props; we assume that if deep objects are changed,
|
|
20
27
|
// this is in observables, which would have been tracked anyway
|
|
21
28
|
let Component = (...args) => {
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
29
|
+
const [cache, destroyCache] = useCreateCacheRef(enableCache)
|
|
30
|
+
const componentId = useId()
|
|
31
|
+
|
|
32
|
+
const admRef = useRef()
|
|
33
|
+
if (!admRef.current) {
|
|
34
|
+
const adm = {
|
|
35
|
+
stateVersion: Symbol(), // eslint-disable-line symbol-description
|
|
36
|
+
onStoreChange: undefined,
|
|
37
|
+
subscribe (onStoreChange) {
|
|
38
|
+
adm.onStoreChange = () => {
|
|
39
|
+
adm.stateVersion = Symbol() // eslint-disable-line symbol-description
|
|
40
|
+
onStoreChange()
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
adm.onStoreChange = undefined
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
getSnapshot () {
|
|
47
|
+
return adm.stateVersion
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
admRef.current = adm
|
|
51
|
+
}
|
|
52
|
+
const adm = admRef.current
|
|
53
|
+
|
|
54
|
+
useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot)
|
|
25
55
|
|
|
26
56
|
// wrap the BaseComponent into an observe decorator once.
|
|
27
57
|
// This way it will track any observable changes and will trigger rerender
|
|
28
|
-
const reactionRef =
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
const reactionRef = useRef()
|
|
59
|
+
const destroyRef = useRef()
|
|
60
|
+
if (!reactionRef.current) {
|
|
61
|
+
let update = () => {
|
|
62
|
+
// It's important to block updates caused by rendering itself
|
|
63
|
+
// (when the sync rendering is in progress).
|
|
64
|
+
if (!executionContextTracker.isActive()) adm.onStoreChange?.()
|
|
65
|
+
}
|
|
66
|
+
if (throttle) update = _throttle(update, throttle)
|
|
67
|
+
destroyRef.current = (where) => {
|
|
68
|
+
if (!reactionRef.current) throw Error(`NO REACTION REF - ${where}`)
|
|
69
|
+
unobserve(reactionRef.current)
|
|
70
|
+
reactionRef.current = undefined
|
|
71
|
+
destroyRef.current = undefined
|
|
72
|
+
destroyCache(where)
|
|
37
73
|
}
|
|
38
|
-
const trappedRender = trapRender({
|
|
39
|
-
|
|
74
|
+
const trappedRender = trapRender({
|
|
75
|
+
render: BaseComponent,
|
|
76
|
+
cache,
|
|
77
|
+
destroy: destroyRef.current,
|
|
78
|
+
componentId
|
|
79
|
+
})
|
|
80
|
+
reactionRef.current = observe(trappedRender, {
|
|
40
81
|
scheduler: update,
|
|
41
82
|
lazy: true
|
|
42
83
|
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (reactionRef.current !== observedRender) reactionRef.current = observedRender
|
|
84
|
+
}
|
|
46
85
|
|
|
47
86
|
// clean up observer on unmount
|
|
48
87
|
useUnmount(() => {
|
|
49
|
-
|
|
50
|
-
// probably because of throw's of the async hooks.
|
|
51
|
-
// So there probably are memory leaks here. Research this.
|
|
52
|
-
if (observedRender.current) {
|
|
53
|
-
unobserve(observedRender.current)
|
|
54
|
-
observedRender.current = undefined
|
|
55
|
-
}
|
|
88
|
+
destroyRef.current('useUnmount()')
|
|
56
89
|
})
|
|
57
90
|
|
|
58
|
-
return
|
|
91
|
+
return reactionRef.current(...args)
|
|
59
92
|
}
|
|
60
93
|
|
|
61
94
|
if (forwardRef) Component = _forwardRef(Component)
|
|
62
95
|
pipeComponentMeta(BaseComponent, Component)
|
|
63
96
|
|
|
64
|
-
Component
|
|
65
|
-
|
|
66
|
-
return Component
|
|
97
|
+
return { Component, forwardRef, ...options }
|
|
67
98
|
}
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
100
|
+
function normalizeThrottle (throttle) {
|
|
101
|
+
if (typeof throttle === 'boolean') {
|
|
102
|
+
if (throttle) return DEFAULT_THROTTLE_TIMEOUT
|
|
103
|
+
else return undefined
|
|
73
104
|
}
|
|
105
|
+
if (typeof throttle === 'number') return throttle
|
|
106
|
+
if (throttle == null) return undefined
|
|
107
|
+
throw Error('observer(): throttle can be either boolean or number (milliseconds)')
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} else {
|
|
91
|
-
return () => setTick(Math.random())
|
|
110
|
+
function useCreateCacheRef (enableCache) {
|
|
111
|
+
const cacheRef = useRef()
|
|
112
|
+
const destroyCacheRef = useRef()
|
|
113
|
+
if (!cacheRef.current) {
|
|
114
|
+
__increment('ObserverWrapper.cache')
|
|
115
|
+
const _createCaches = enableCache ? createCaches : getDummyCache
|
|
116
|
+
cacheRef.current = _createCaches(['styles', 'model'])
|
|
117
|
+
destroyCacheRef.current = (where) => {
|
|
118
|
+
if (!cacheRef.current) throw Error(`NO CACHE REF - ${where}`)
|
|
119
|
+
__decrement('ObserverWrapper.cache')
|
|
120
|
+
cacheRef.current.clear()
|
|
121
|
+
cacheRef.current = undefined
|
|
122
|
+
destroyCacheRef.current = undefined
|
|
123
|
+
}
|
|
92
124
|
}
|
|
125
|
+
return [cacheRef.current, destroyCacheRef.current]
|
|
93
126
|
}
|
package/react/helpers.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CACHE_ACTIVE, getDummyCache } from '@teamplay/cache'
|
|
3
|
-
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect.js'
|
|
1
|
+
import { useContext, createContext, useRef, useEffect } from 'react'
|
|
4
2
|
|
|
5
3
|
export const ComponentMetaContext = createContext({})
|
|
6
4
|
|
|
@@ -24,12 +22,18 @@ export function pipeComponentMeta (SourceComponent, TargetComponent, suffix = ''
|
|
|
24
22
|
return TargetComponent
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return cache
|
|
25
|
+
export function useId () {
|
|
26
|
+
const { componentId } = useContext(ComponentMetaContext)
|
|
27
|
+
return componentId
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
export function useUnmount (fn) {
|
|
34
|
-
|
|
31
|
+
const fnRef = useRef(fn)
|
|
32
|
+
fnRef.current = fn
|
|
33
|
+
useEffect(
|
|
34
|
+
() => () => {
|
|
35
|
+
fnRef.current()
|
|
36
|
+
},
|
|
37
|
+
[]
|
|
38
|
+
)
|
|
35
39
|
}
|
package/react/trapRender.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// trap render function (functional component) to block observer updates and activate cache
|
|
2
2
|
// during synchronous rendering
|
|
3
|
-
import { useId } from 'react'
|
|
4
|
-
import { unobserve } from '@nx-js/observer-util'
|
|
5
3
|
import executionContextTracker from './executionContextTracker.js'
|
|
6
4
|
|
|
7
|
-
export default function trapRender ({ render,
|
|
5
|
+
export default function trapRender ({ render, cache, destroy, componentId }) {
|
|
8
6
|
return (...args) => {
|
|
9
|
-
|
|
10
|
-
executionContextTracker._start(id)
|
|
11
|
-
blockUpdate.value = true
|
|
7
|
+
executionContextTracker._start(componentId)
|
|
12
8
|
cache.activate()
|
|
9
|
+
let destroyed
|
|
13
10
|
try {
|
|
14
11
|
// destroyer.reset() // TODO: this one is for any destructuring logic which might be needed
|
|
15
12
|
// promiseBatcher.reset() // TODO: this is to support useBatch* hooks
|
|
@@ -17,15 +14,12 @@ export default function trapRender ({ render, blockUpdate, cache, reactionRef })
|
|
|
17
14
|
// if (promiseBatcher.isActive()) {
|
|
18
15
|
// throw Error('[react-sharedb] useBatch* hooks were used without a closing useBatch() call.')
|
|
19
16
|
// }
|
|
20
|
-
blockUpdate.value = false // TODO: might want to just put it into finally block
|
|
21
17
|
return res
|
|
22
18
|
} catch (err) {
|
|
23
19
|
// TODO: this might only be needed only if promise is thrown
|
|
24
20
|
// (check if useUnmount in convertToObserver is called if a regular error is thrown)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
reactionRef.current = undefined
|
|
28
|
-
}
|
|
21
|
+
destroy('trapRender.js')
|
|
22
|
+
destroyed = true
|
|
29
23
|
|
|
30
24
|
if (!err.then) throw err
|
|
31
25
|
// If the Promise was thrown, we catch it before Suspense does.
|
|
@@ -38,7 +32,7 @@ export default function trapRender ({ render, blockUpdate, cache, reactionRef })
|
|
|
38
32
|
// throw err.then(destroy)
|
|
39
33
|
throw err
|
|
40
34
|
} finally {
|
|
41
|
-
cache.deactivate()
|
|
35
|
+
if (!destroyed) cache.deactivate()
|
|
42
36
|
executionContextTracker._clear()
|
|
43
37
|
}
|
|
44
38
|
}
|
package/react/universal$.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
1
2
|
import $ from '../orm/$.js'
|
|
2
3
|
import executionContextTracker from './executionContextTracker.js'
|
|
3
4
|
|
|
4
5
|
// universal versions of $() which work as a plain function or as a react hook
|
|
5
6
|
export default function universal$ ($root, value) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
if (executionContextTracker.isActive()) {
|
|
8
|
+
// within react component
|
|
9
|
+
const id = executionContextTracker.newHookId()
|
|
10
|
+
const $signal = $($root, value, id)
|
|
11
|
+
// save signal into ref to make sure it's not garbage collected while component exists
|
|
12
|
+
useRef($signal) // eslint-disable-line react-hooks/rules-of-hooks
|
|
13
|
+
return $signal
|
|
14
|
+
} else {
|
|
15
|
+
return $($root, value)
|
|
16
|
+
}
|
|
9
17
|
}
|
package/react/universalSub.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
1
2
|
import sub from '../orm/sub.js'
|
|
2
3
|
import executionContextTracker from './executionContextTracker.js'
|
|
3
4
|
|
|
4
5
|
// universal versions of sub() which work as a plain function or as a react hook
|
|
5
6
|
export default function universalSub (...args) {
|
|
6
7
|
const promiseOrSignal = sub(...args)
|
|
7
|
-
if (executionContextTracker.isActive()
|
|
8
|
+
if (executionContextTracker.isActive()) {
|
|
9
|
+
// within react component
|
|
10
|
+
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
11
|
+
if (promiseOrSignal.then) throw promiseOrSignal
|
|
12
|
+
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
13
|
+
useRef(promiseOrSignal) // eslint-disable-line react-hooks/rules-of-hooks
|
|
14
|
+
}
|
|
8
15
|
return promiseOrSignal
|
|
9
16
|
}
|
|
@@ -1,50 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
) {
|
|
9
|
-
|
|
10
|
-
if (!(suspenseProps && suspenseProps.fallback)) {
|
|
11
|
-
throw Error(
|
|
12
|
-
'[observer()] You must pass at least ' +
|
|
13
|
-
'a fallback parameter to suspenseProps'
|
|
14
|
-
)
|
|
15
|
-
}
|
|
1
|
+
import { forwardRef as _forwardRef, memo, createElement as el, Suspense, useId, useRef } from 'react'
|
|
2
|
+
import { pipeComponentMeta, pipeComponentDisplayName, ComponentMetaContext } from './helpers.js'
|
|
3
|
+
|
|
4
|
+
export default function wrapIntoSuspense ({
|
|
5
|
+
Component,
|
|
6
|
+
forwardRef,
|
|
7
|
+
suspenseProps = DEFAULT_SUSPENSE_PROPS
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (!suspenseProps?.fallback) throw Error(ERRORS.noFallback)
|
|
16
10
|
|
|
17
11
|
let SuspenseWrapper = (props, ref) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// const [componentMeta] = React.useState({
|
|
25
|
-
// componentId: $root.id(),
|
|
26
|
-
// createdAt: Date.now(),
|
|
27
|
-
// cache
|
|
28
|
-
// })
|
|
29
|
-
const componentMeta = useMemo(function () {
|
|
30
|
-
return {
|
|
31
|
-
// componentId: $root.id(), // TODO: implement creating a unique component guid here (if it's needed anymore)
|
|
32
|
-
createdAt: Date.now(),
|
|
33
|
-
cache
|
|
12
|
+
const componentId = useId()
|
|
13
|
+
const componentMetaRef = useRef()
|
|
14
|
+
if (!componentMetaRef.current) {
|
|
15
|
+
componentMetaRef.current = {
|
|
16
|
+
componentId,
|
|
17
|
+
createdAt: Date.now()
|
|
34
18
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
useUnmount(() => {
|
|
38
|
-
__decrement('ObserverWrapper.cache')
|
|
39
|
-
cache.clear()
|
|
40
|
-
})
|
|
19
|
+
}
|
|
41
20
|
|
|
42
21
|
if (forwardRef) props = { ...props, ref }
|
|
43
22
|
|
|
44
23
|
return (
|
|
45
|
-
el(ComponentMetaContext.Provider, { value:
|
|
24
|
+
el(ComponentMetaContext.Provider, { value: componentMetaRef.current },
|
|
46
25
|
el(Suspense, suspenseProps,
|
|
47
|
-
el(
|
|
26
|
+
el(Component, props)
|
|
48
27
|
)
|
|
49
28
|
)
|
|
50
29
|
)
|
|
@@ -52,12 +31,19 @@ export default function wrapIntoSuspense (
|
|
|
52
31
|
|
|
53
32
|
// pipe only displayName because forwardRef render function
|
|
54
33
|
// do not support propTypes or defaultProps
|
|
55
|
-
pipeComponentDisplayName(
|
|
34
|
+
pipeComponentDisplayName(Component, SuspenseWrapper, 'StartupjsObserverWrapper')
|
|
56
35
|
|
|
57
36
|
if (forwardRef) SuspenseWrapper = _forwardRef(SuspenseWrapper)
|
|
58
37
|
SuspenseWrapper = memo(SuspenseWrapper)
|
|
59
38
|
|
|
60
|
-
pipeComponentMeta(
|
|
39
|
+
pipeComponentMeta(Component, SuspenseWrapper)
|
|
61
40
|
|
|
62
41
|
return SuspenseWrapper
|
|
63
42
|
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_SUSPENSE_PROPS = { fallback: el(NullComponent, null, null) }
|
|
45
|
+
function NullComponent () { return null }
|
|
46
|
+
|
|
47
|
+
const ERRORS = {
|
|
48
|
+
noFallback: '[observer()] You must pass at least a fallback parameter to suspenseProps'
|
|
49
|
+
}
|
package/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createChannel from '@teamplay/channel/server'
|
|
2
|
-
import { connection, setConnection, setFetchOnly } from './orm/connection.js'
|
|
2
|
+
import { connection, setConnection, setFetchOnly, setPublicOnly } from './orm/connection.js'
|
|
3
3
|
|
|
4
4
|
export { default as ShareDB } from 'sharedb'
|
|
5
5
|
export {
|
|
@@ -9,11 +9,13 @@ export {
|
|
|
9
9
|
|
|
10
10
|
export function initConnection (backend, {
|
|
11
11
|
fetchOnly = true,
|
|
12
|
+
publicOnly = true,
|
|
12
13
|
...options
|
|
13
14
|
} = {}) {
|
|
14
15
|
if (!backend) throw Error('backend is required')
|
|
15
16
|
if (connection) throw Error('Connection already exists')
|
|
16
17
|
setConnection(backend.connect())
|
|
17
18
|
setFetchOnly(fetchOnly)
|
|
19
|
+
setPublicOnly(publicOnly)
|
|
18
20
|
return createChannel(backend, options)
|
|
19
21
|
}
|