teamplay 0.0.1

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.
@@ -0,0 +1,196 @@
1
+ import { observable, raw } from '@nx-js/observer-util'
2
+ import jsonDiff from 'json0-ot-diff'
3
+ import diffMatchPatch from 'diff-match-patch'
4
+ import { getConnection } from './connection.js'
5
+
6
+ const ALLOW_PARTIAL_DOC_CREATION = false
7
+
8
+ export const dataTreeRaw = {}
9
+ const dataTree = observable(dataTreeRaw)
10
+
11
+ export function get (segments, tree = dataTree) {
12
+ let dataNode = tree
13
+ for (const segment of segments) {
14
+ if (dataNode == null) return dataNode
15
+ dataNode = dataNode[segment]
16
+ }
17
+ return dataNode
18
+ }
19
+
20
+ export function getRaw (segments) {
21
+ return get(segments, dataTreeRaw)
22
+ }
23
+
24
+ export function set (segments, value, tree = dataTree) {
25
+ let dataNode = tree
26
+ let dataNodeRaw = raw(dataTree)
27
+ for (let i = 0; i < segments.length - 1; i++) {
28
+ const segment = segments[i]
29
+ if (dataNode[segment] == null) {
30
+ // if next segment is a number, it means that we are in the array
31
+ if (typeof segments[i + 1] === 'number') dataNode[segment] = []
32
+ else dataNode[segment] = {}
33
+ }
34
+ dataNode = dataNode[segment]
35
+ dataNodeRaw = dataNodeRaw[segment]
36
+ }
37
+ const key = segments[segments.length - 1]
38
+ // handle adding out of bounds empty element to the array
39
+ if (value == null && Array.isArray(dataNodeRaw) && key >= dataNodeRaw.length) {
40
+ // inject new undefined elements to the end of the array
41
+ dataNode.splice(dataNodeRaw.length, key - dataNodeRaw.length + 1,
42
+ ...Array(key - dataNodeRaw.length + 1).fill(undefined))
43
+ return
44
+ }
45
+ // handle when the value didn't change
46
+ if (value === dataNodeRaw[key]) return
47
+ // handle setting undefined value
48
+ if (value == null) {
49
+ if (Array.isArray(dataNodeRaw)) {
50
+ // if parent is an array -- we set array element to undefined
51
+ // IMPORTANT: JSON serialization will replace `undefined` with `null`
52
+ // so if the data will go to the server, it will be serialized as `null`.
53
+ // And when it comes back from the server it will be still `null`.
54
+ // This can lead to confusion since when you set `undefined` the value
55
+ // might end up becoming `null` for seemingly no reason (like in this case).
56
+ dataNode[key] = undefined
57
+ } else {
58
+ // if parent is an object -- we completely delete the property.
59
+ // Deleting the property is better for the JSON serialization
60
+ // since JSON does not have `undefined` values and replaces them with `null`.
61
+ delete dataNode[key]
62
+ }
63
+ return
64
+ }
65
+ // just set the new value
66
+ dataNode[key] = value
67
+ }
68
+
69
+ export function del (segments, tree = dataTree) {
70
+ let dataNode = tree
71
+ for (let i = 0; i < segments.length - 1; i++) {
72
+ const segment = segments[i]
73
+ if (dataNode[segment] == null) return
74
+ dataNode = dataNode[segment]
75
+ }
76
+ if (Array.isArray(dataNode)) {
77
+ // remove the element from the array
78
+ dataNode.splice(segments[segments.length - 1], 1)
79
+ } else {
80
+ // remove the property from the object
81
+ delete dataNode[segments[segments.length - 1]]
82
+ }
83
+ }
84
+
85
+ export async function setPublicDoc (segments, value, deleteValue = false) {
86
+ if (segments.length === 0) throw Error(ERRORS.publicDoc(segments))
87
+ if (segments.length === 1) {
88
+ // set multiple documents at the same time
89
+ if (typeof value !== 'object') throw Error(ERRORS.notObjectCollection(segments, value))
90
+ for (const docId in value) {
91
+ await setPublicDoc([segments[0], docId], value[docId])
92
+ }
93
+ }
94
+ const [collection, docId] = segments
95
+ if (typeof docId === 'number') throw Error(ERRORS.publicDocIdNumber(segments))
96
+ if (docId === 'undefined') throw Error(ERRORS.publicDocIdUndefined(segments))
97
+ if (!(collection && docId)) throw Error(ERRORS.publicDoc(segments))
98
+ const doc = getConnection().get(collection, docId)
99
+ if (!doc.data && deleteValue) throw Error(ERRORS.deleteNonExistentDoc(segments))
100
+ // make sure that the value is not observable to not trigger extra reads. And clone it
101
+ value = raw(value)
102
+ if (value == null) {
103
+ value = undefined
104
+ } else {
105
+ value = JSON.parse(JSON.stringify(value))
106
+ }
107
+ if (segments.length === 2 && !doc.data) {
108
+ // > create a new doc. Full doc data is provided
109
+ if (typeof value !== 'object') throw Error(ERRORS.notObject(segments, value))
110
+ const newDoc = value
111
+ return new Promise((resolve, reject) => {
112
+ doc.create(newDoc, err => err ? reject(err) : resolve())
113
+ })
114
+ } else if (!doc.data) {
115
+ // >> create a new doc. Partial doc data is provided (subpath)
116
+ // NOTE: We throw an error when trying to set a subpath on a non-existing doc
117
+ // to prevent potential mistakes. In future we might allow it though.
118
+ if (!ALLOW_PARTIAL_DOC_CREATION) throw Error(ERRORS.partialDocCreation(segments, value))
119
+ const newDoc = {}
120
+ set(segments.slice(2), value, newDoc)
121
+ return new Promise((resolve, reject) => {
122
+ doc.create(newDoc, err => err ? reject(err) : resolve())
123
+ })
124
+ } else if (segments.length === 2 && (deleteValue || value == null)) {
125
+ // > delete doc
126
+ return new Promise((resolve, reject) => {
127
+ doc.del(err => err ? reject(err) : resolve())
128
+ })
129
+ } else if (segments.length === 2) {
130
+ // > modify existing doc. Full doc modification
131
+ if (typeof value !== 'object') throw Error(ERRORS.notObject(segments, value))
132
+ const oldDoc = getRaw([collection, docId])
133
+ const diff = jsonDiff(oldDoc, value, diffMatchPatch)
134
+ return new Promise((resolve, reject) => {
135
+ doc.submitOp(diff, err => err ? reject(err) : resolve())
136
+ })
137
+ } else {
138
+ // > modify existing doc. Partial doc modification
139
+ const oldDoc = getRaw([collection, docId])
140
+ const newDoc = JSON.parse(JSON.stringify(oldDoc))
141
+ if (deleteValue) {
142
+ del(segments.slice(2), newDoc)
143
+ } else {
144
+ set(segments.slice(2), value, newDoc)
145
+ }
146
+ const diff = jsonDiff(oldDoc, newDoc, diffMatchPatch)
147
+ return new Promise((resolve, reject) => {
148
+ doc.submitOp(diff, err => err ? reject(err) : resolve())
149
+ })
150
+ }
151
+ }
152
+
153
+ export default dataTree
154
+
155
+ const ERRORS = {
156
+ publicDoc: segments => `Public doc should have collection and docId. Got: ${segments}`,
157
+ nonExistingDoc: segments => `
158
+ Trying to modify a non-existing doc ${segments}.
159
+ Make sure you have subscribed to the doc before modifying it OR creating it.
160
+ `,
161
+ notObject: (segments, value) => `
162
+ Trying to set a non-object value to a public doc ${segments}.
163
+ Value: ${value}
164
+ `,
165
+ notObjectCollection: (segments, value) => `
166
+ Trying to set multiple documents for the collection but the value passed is not an object.
167
+ Path: ${segments}
168
+ Value: ${value}
169
+ `,
170
+ publicDocIdNumber: segments => `
171
+ Public doc id must be a string. Got a number: ${segments}
172
+ `,
173
+ deleteNonExistentDoc: segments => `
174
+ Trying to delete data from a non-existing doc ${segments}.
175
+ Make sure that the document exists and you are subscribed to it
176
+ before trying to delete anything from it or the doc itself.
177
+ `,
178
+ publicDocIdUndefined: segments => `
179
+ Trying to modify a public document with the id 'undefined'.
180
+ It's most likely a bug in your code and the variable you are using to store
181
+ the document id is not initialized correctly.
182
+ Got path: ${segments}
183
+ `,
184
+ partialDocCreation: (segments, value) => `
185
+ Can't set a value to a subpath of a document which doesn't exist.
186
+
187
+ You have probably forgotten to subscribe to the document.
188
+ You MUST subscribe to an existing document with 'sub()' before trying to modify it.
189
+
190
+ If instead you want to create a new document, you must provide the full data for it
191
+ and set it for the $.collection.docId signal.
192
+
193
+ Path: ${segments}
194
+ Value: ${value}
195
+ `
196
+ }
@@ -0,0 +1,100 @@
1
+ import Cache from './Cache.js'
2
+ import Signal, { regularBindings, extremelyLateBindings, isPublicCollection, isPrivateCollection } from './Signal.js'
3
+ import { findModel } from './addModel.js'
4
+ import { LOCAL } from './$.js'
5
+ import { ROOT, ROOT_ID, GLOBAL_ROOT_ID } from './Root.js'
6
+ import { QUERIES } from './Query.js'
7
+
8
+ const PROXIES_CACHE = new Cache()
9
+ const PROXY_TO_SIGNAL = new WeakMap()
10
+
11
+ // extremely late bindings let you use fields in your raw data which have the same name as signal's methods
12
+ const USE_EXTREMELY_LATE_BINDINGS = true
13
+
14
+ // get proxy-wrapped signal from cache or create a new one
15
+ // TODO: move Private, Public, Local signals out of this file, same as Query has its own signal
16
+ export default function getSignal ($root, segments = [], {
17
+ useExtremelyLateBindings = USE_EXTREMELY_LATE_BINDINGS,
18
+ rootId,
19
+ signalHash,
20
+ proxyHandlers = getDefaultProxyHandlers({ useExtremelyLateBindings })
21
+ } = {}) {
22
+ if (!($root instanceof Signal)) {
23
+ if (segments.length === 0 && !rootId) throw Error(ERRORS.rootIdRequired)
24
+ if (segments.length >= 1 && isPrivateCollection(segments[0])) {
25
+ if (segments[0] === QUERIES) {
26
+ // TODO: this is a hack to temporarily let the queries work.
27
+ // '$queries' collection is always added to the global (singleton) root signal.
28
+ // In future it should also be part of the particular root signal.
29
+ $root = getSignal(undefined, [], { rootId: GLOBAL_ROOT_ID })
30
+ } else {
31
+ throw Error(ERRORS.rootSignalRequired)
32
+ }
33
+ }
34
+ }
35
+ signalHash ??= hashSegments(segments, $root?.[ROOT_ID] || rootId)
36
+ let proxy = PROXIES_CACHE.get(signalHash)
37
+ if (proxy) return proxy
38
+
39
+ const SignalClass = getSignalClass(segments)
40
+ const signal = new SignalClass(segments)
41
+ proxy = new Proxy(signal, proxyHandlers)
42
+ if (segments.length >= 1) {
43
+ if (isPrivateCollection(segments[0])) {
44
+ proxy[ROOT] = $root
45
+ } else {
46
+ // TODO: this is probably a hack, currently public collection signals don't need a root signal
47
+ // but without it calling the methods of root signal like $.get() doesn't work
48
+ proxy[ROOT] = $root || getSignal(undefined, [], { rootId: GLOBAL_ROOT_ID })
49
+ }
50
+ }
51
+ PROXY_TO_SIGNAL.set(proxy, signal)
52
+ const dependencies = []
53
+
54
+ // if the signal is a child of the local value created through the $() function,
55
+ // we need to add the parent signal ('$local.id') to the dependencies so that it doesn't get garbage collected
56
+ // before the child signal ('$local.id.firstName') is garbage collected.
57
+ // Same goes for public collections -- we need to keep the document signal alive while its child signals are alive
58
+ if (segments.length > 2) {
59
+ if (segments[0] === LOCAL) {
60
+ dependencies.push(getSignal($root, segments.slice(0, 2)))
61
+ } else if (isPublicCollection(segments[0])) {
62
+ dependencies.push(getSignal(undefined, segments.slice(0, 2)))
63
+ }
64
+ }
65
+
66
+ PROXIES_CACHE.set(signalHash, proxy, dependencies)
67
+ return proxy
68
+ }
69
+
70
+ function getDefaultProxyHandlers ({ useExtremelyLateBindings } = {}) {
71
+ return useExtremelyLateBindings ? extremelyLateBindings : regularBindings
72
+ }
73
+
74
+ function hashSegments (segments, rootId) {
75
+ if (segments.length === 0) {
76
+ if (!rootId) throw Error(ERRORS.rootIdRequired)
77
+ return JSON.stringify({ root: rootId })
78
+ } else if (isPrivateCollection(segments[0])) {
79
+ if (!rootId) throw Error(ERRORS.privateCollectionRootIdRequired(segments))
80
+ return JSON.stringify({ private: [rootId, segments] })
81
+ } else {
82
+ return JSON.stringify(segments)
83
+ }
84
+ }
85
+
86
+ export function getSignalClass (segments) {
87
+ return findModel(segments) ?? Signal
88
+ }
89
+
90
+ export function rawSignal (proxy) {
91
+ return PROXY_TO_SIGNAL.get(proxy)
92
+ }
93
+
94
+ export { PROXIES_CACHE as __DEBUG_SIGNALS_CACHE__ }
95
+
96
+ const ERRORS = {
97
+ rootIdRequired: 'Root signal must have a rootId specified',
98
+ privateCollectionRootIdRequired: segments => `Private collection signal must have a rootId specified. Segments: ${segments}`,
99
+ rootSignalRequired: 'First argument of getSignal() for private collections must be a Root Signal'
100
+ }
package/orm/sub.js ADDED
@@ -0,0 +1,32 @@
1
+ import Signal, { SEGMENTS, isPublicCollectionSignal, isPublicDocumentSignal } from './Signal.js'
2
+ import { docSubscriptions } from './Doc.js'
3
+ import { querySubscriptions, getQuerySignal } from './Query.js'
4
+
5
+ export default function sub ($signal, params) {
6
+ if (isPublicDocumentSignal($signal)) {
7
+ return doc$($signal)
8
+ } else if (isPublicCollectionSignal($signal)) {
9
+ return query$($signal, params)
10
+ } else if (typeof $signal === 'function' && !($signal instanceof Signal)) {
11
+ return api$($signal, params)
12
+ } else {
13
+ throw Error('Invalid args passed for sub()')
14
+ }
15
+ }
16
+
17
+ function doc$ ($doc) {
18
+ const promise = docSubscriptions.subscribe($doc)
19
+ if (!promise) return $doc
20
+ return new Promise(resolve => promise.then(() => resolve($doc)))
21
+ }
22
+
23
+ function query$ ($collection, params) {
24
+ const $query = getQuerySignal($collection[SEGMENTS], params)
25
+ const promise = querySubscriptions.subscribe($query)
26
+ if (!promise) return $query
27
+ return new Promise(resolve => promise.then(() => resolve($query)))
28
+ }
29
+
30
+ function api$ (fn, args) {
31
+ throw Error('sub() for async functions is not implemented yet')
32
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "teamplay",
3
+ "version": "0.0.1",
4
+ "description": "Deep signals ORM for React with real-time collaboration",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./connect": "./connect/index.js",
10
+ "./server": "./server.js",
11
+ "./connect-test": "./connect/test.js"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "test": "npm run test-server && npm run test-client",
18
+ "test-server": "node --expose-gc -r ./test/_init.cjs --test",
19
+ "test-server-only": "node --expose-gc -r ./test/_init.cjs --test --test-only",
20
+ "test-client": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
21
+ },
22
+ "dependencies": {
23
+ "@nx-js/observer-util": "^4.1.3",
24
+ "@startupjs/cache": "^0.56.0-alpha.0",
25
+ "@startupjs/channel": "^0.56.0-alpha.0",
26
+ "@startupjs/debug": "^0.56.0-alpha.0",
27
+ "@startupjs/utils": "^0.56.0-alpha.64",
28
+ "diff-match-patch": "^1.0.5",
29
+ "json0-ot-diff": "^1.1.2",
30
+ "lodash": "^4.17.20",
31
+ "sharedb": "^5.0.0",
32
+ "uuid": "^9.0.1"
33
+ },
34
+ "devDependencies": {
35
+ "@jest/globals": "^29.7.0",
36
+ "@testing-library/react": "^15.0.7",
37
+ "jest": "^29.7.0",
38
+ "jest-environment-jsdom": "^29.7.0",
39
+ "sharedb-mingo-memory": "^3.0.1"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "*",
43
+ "sharedb-mingo-memory": "*"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "sharedb-mingo-memory": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "jest": {
51
+ "transform": {},
52
+ "testEnvironment": "jsdom",
53
+ "testRegex": "test_client/.*\\.jsx?$",
54
+ "testPathIgnorePatterns": [
55
+ "node_modules",
56
+ "<rootDir>/test_client/helpers"
57
+ ]
58
+ },
59
+ "license": "MIT"
60
+ }
@@ -0,0 +1,93 @@
1
+ // TODO: rewrite to use useSyncExternalStore like in mobx. This will also help with handling Suspense abandonment better
2
+ // to cleanup the observer() reaction when the component is unmounted or was abandoned and unmounts will never trigger.
3
+ // ref: https://github.com/mobxjs/mobx/blob/94bc4997c14152ff5aefcaac64d982d5c21ba51a/packages/mobx-react-lite/src/useObserver.ts
4
+ import { createElement as el, forwardRef as _forwardRef, useCallback, useState, useMemo } from 'react'
5
+ import _throttle from 'lodash/throttle.js'
6
+ import { observe, unobserve } from '@nx-js/observer-util'
7
+ import { pipeComponentMeta, useCache, useUnmount } from './helpers.js'
8
+ import trapRender from './trapRender.js'
9
+
10
+ const DEFAULT_THROTTLE_TIMEOUT = 100
11
+
12
+ export default function convertToObserver (BaseComponent, options = {}) {
13
+ options = { ...DEFAULT_OPTIONS, ...options }
14
+ const { forwardRef } = options
15
+ // MAGIC. This fixes hot-reloading. TODO: figure out WHY it fixes it
16
+ const random = Math.random()
17
+
18
+ // memo; we are not intested in deep updates
19
+ // in props; we assume that if deep objects are changed,
20
+ // this is in observables, which would have been tracked anyway
21
+ let Component = (...args) => {
22
+ // forceUpdate 2.0
23
+ const forceUpdate = useForceUpdate(options.throttle)
24
+ const cache = useCache(options.cache != null ? options.cache : true)
25
+
26
+ // wrap the BaseComponent into an observe decorator once.
27
+ // This way it will track any observable changes and will trigger rerender
28
+ const reactionRef = useMemo(() => ({}), [])
29
+ const observedRender = useMemo(() => {
30
+ const blockUpdate = { value: false }
31
+ const update = () => {
32
+ // TODO: Decide whether the check for unmount is needed here
33
+ // Force update unless update is blocked. It's important to block
34
+ // updates caused by rendering
35
+ // (when the sync rendening is in progress)
36
+ if (!blockUpdate.value) forceUpdate()
37
+ }
38
+ const trappedRender = trapRender({ render: BaseComponent, blockUpdate, cache, reactionRef })
39
+ return observe(trappedRender, {
40
+ scheduler: update,
41
+ lazy: true
42
+ })
43
+ }, [random])
44
+
45
+ if (reactionRef.current !== observedRender) reactionRef.current = observedRender
46
+
47
+ // clean up observer on unmount
48
+ useUnmount(() => {
49
+ // TODO: this does not execute the same amount of times as observe() does,
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
+ }
56
+ })
57
+
58
+ return observedRender(...args)
59
+ }
60
+
61
+ if (forwardRef) Component = _forwardRef(Component)
62
+ pipeComponentMeta(BaseComponent, Component)
63
+
64
+ Component.__observerOptions = options
65
+
66
+ return Component
67
+ }
68
+
69
+ const DEFAULT_OPTIONS = {
70
+ forwardRef: false,
71
+ suspenseProps: {
72
+ fallback: el(NullComponent, null, null)
73
+ }
74
+ }
75
+
76
+ function NullComponent () {
77
+ return null
78
+ }
79
+
80
+ function useForceUpdate (throttle) {
81
+ const [, setTick] = useState()
82
+ if (throttle) {
83
+ const timeout = typeof (throttle) === 'number' ? +throttle : DEFAULT_THROTTLE_TIMEOUT
84
+ // eslint-disable-next-line react-hooks/rules-of-hooks
85
+ return useCallback(
86
+ _throttle(() => {
87
+ setTick(Math.random())
88
+ }, timeout)
89
+ , [])
90
+ } else {
91
+ return () => setTick(Math.random())
92
+ }
93
+ }
@@ -0,0 +1,32 @@
1
+ export default new class ExecutionContextTracker {
2
+ #contextId
3
+ #hooksCounter
4
+
5
+ isActive () {
6
+ return this.#contextId !== undefined
7
+ }
8
+
9
+ getComponentId () {
10
+ return this.#contextId
11
+ }
12
+
13
+ newHookId () {
14
+ this.incrementHooksCounter()
15
+ const id = `_${this.#contextId}_${this.#hooksCounter}`
16
+ return id
17
+ }
18
+
19
+ incrementHooksCounter () {
20
+ if (!this.#contextId) return
21
+ this.#hooksCounter++
22
+ }
23
+
24
+ _start (contextId) {
25
+ this.#contextId = contextId
26
+ this.#hooksCounter = -1
27
+ }
28
+
29
+ _clear () {
30
+ this.#contextId = undefined
31
+ }
32
+ }()
@@ -0,0 +1,35 @@
1
+ import { useMemo, useContext, createContext } from 'react'
2
+ import { CACHE_ACTIVE, getDummyCache } from '@startupjs/cache'
3
+ import useIsomorphicLayoutEffect from '@startupjs/utils/useIsomorphicLayoutEffect'
4
+
5
+ export const ComponentMetaContext = createContext({})
6
+
7
+ export function pipeComponentDisplayName (SourceComponent, TargetComponent, suffix = '', defaultName = 'StartupjsWrapper') {
8
+ const displayName = SourceComponent.displayName || SourceComponent.name
9
+
10
+ if (!TargetComponent.displayName) {
11
+ TargetComponent.displayName = displayName ? (displayName + suffix) : defaultName
12
+ }
13
+ }
14
+
15
+ export function pipeComponentMeta (SourceComponent, TargetComponent, suffix = '', defaultName = 'StartupjsWrapper') {
16
+ pipeComponentDisplayName(SourceComponent, TargetComponent, suffix, defaultName)
17
+
18
+ if (!TargetComponent.propTypes && SourceComponent.propTypes) {
19
+ TargetComponent.propTypes = SourceComponent.propTypes
20
+ }
21
+ if (!TargetComponent.defaultProps && SourceComponent.defaultProps) {
22
+ TargetComponent.defaultProps = SourceComponent.defaultProps
23
+ }
24
+ return TargetComponent
25
+ }
26
+
27
+ export function useCache (active) {
28
+ if (!CACHE_ACTIVE.value || !active) return useMemo(getDummyCache, []) // eslint-disable-line react-hooks/rules-of-hooks
29
+ const { cache } = useContext(ComponentMetaContext) // eslint-disable-line react-hooks/rules-of-hooks
30
+ return cache
31
+ }
32
+
33
+ export function useUnmount (fn) {
34
+ useIsomorphicLayoutEffect(() => fn, [])
35
+ }
@@ -0,0 +1,9 @@
1
+ import convertToObserver from './convertToObserver.js'
2
+ import wrapIntoSuspense from './wrapIntoSuspense.js'
3
+
4
+ function observer (Component, options) {
5
+ return wrapIntoSuspense(convertToObserver(Component, options))
6
+ }
7
+ observer.__wrapObserverMeta = wrapIntoSuspense
8
+ observer.__makeObserver = convertToObserver
9
+ export default observer
@@ -0,0 +1,45 @@
1
+ // trap render function (functional component) to block observer updates and activate cache
2
+ // during synchronous rendering
3
+ import { useId } from 'react'
4
+ import { unobserve } from '@nx-js/observer-util'
5
+ import executionContextTracker from './executionContextTracker.js'
6
+
7
+ export default function trapRender ({ render, blockUpdate, cache, reactionRef }) {
8
+ return (...args) => {
9
+ const id = useId()
10
+ executionContextTracker._start(id)
11
+ blockUpdate.value = true
12
+ cache.activate()
13
+ try {
14
+ // destroyer.reset() // TODO: this one is for any destructuring logic which might be needed
15
+ // promiseBatcher.reset() // TODO: this is to support useBatch* hooks
16
+ const res = render(...args)
17
+ // if (promiseBatcher.isActive()) {
18
+ // throw Error('[react-sharedb] useBatch* hooks were used without a closing useBatch() call.')
19
+ // }
20
+ blockUpdate.value = false // TODO: might want to just put it into finally block
21
+ return res
22
+ } catch (err) {
23
+ // TODO: this might only be needed only if promise is thrown
24
+ // (check if useUnmount in convertToObserver is called if a regular error is thrown)
25
+ if (reactionRef.current) {
26
+ unobserve(reactionRef.current)
27
+ reactionRef.current = undefined
28
+ }
29
+
30
+ if (!err.then) throw err
31
+ // If the Promise was thrown, we catch it before Suspense does.
32
+ // And we run destructors for each hook previous to the one
33
+ // which did throw this Promise.
34
+ // We have to manually do it since the unmount logic is not working
35
+ // for components which were terminated by Suspense as a result of
36
+ // a promise being thrown.
37
+ // const destroy = destroyer.getDestructor()
38
+ // throw err.then(destroy)
39
+ throw err
40
+ } finally {
41
+ cache.deactivate()
42
+ executionContextTracker._clear()
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,9 @@
1
+ import $ from '../orm/$.js'
2
+ import executionContextTracker from './executionContextTracker.js'
3
+
4
+ // universal versions of $() which work as a plain function or as a react hook
5
+ export default function universal$ ($root, value) {
6
+ let id
7
+ if (executionContextTracker.isActive()) id = executionContextTracker.newHookId()
8
+ return $($root, value, id)
9
+ }
@@ -0,0 +1,9 @@
1
+ import sub from '../orm/sub.js'
2
+ import executionContextTracker from './executionContextTracker.js'
3
+
4
+ // universal versions of sub() which work as a plain function or as a react hook
5
+ export default function universalSub (...args) {
6
+ const promiseOrSignal = sub(...args)
7
+ if (executionContextTracker.isActive() && promiseOrSignal.then) throw promiseOrSignal
8
+ return promiseOrSignal
9
+ }