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.
- package/README.md +95 -0
- package/connect/index.js +12 -0
- package/connect/sharedbConnection.cjs +3 -0
- package/connect/test.js +12 -0
- package/index.js +44 -0
- package/orm/$.js +38 -0
- package/orm/Cache.js +29 -0
- package/orm/Doc.js +232 -0
- package/orm/Query.js +278 -0
- package/orm/Reaction.js +44 -0
- package/orm/Root.js +41 -0
- package/orm/Signal.js +211 -0
- package/orm/Value.js +27 -0
- package/orm/addModel.js +31 -0
- package/orm/connection.js +26 -0
- package/orm/dataTree.js +196 -0
- package/orm/getSignal.js +100 -0
- package/orm/sub.js +32 -0
- package/package.json +60 -0
- package/react/convertToObserver.js +93 -0
- package/react/executionContextTracker.js +32 -0
- package/react/helpers.js +35 -0
- package/react/observer.js +9 -0
- package/react/trapRender.js +45 -0
- package/react/universal$.js +9 -0
- package/react/universalSub.js +9 -0
- package/react/wrapIntoSuspense.js +63 -0
- package/schema/GUID_PATTERN.js +1 -0
- package/schema/associations.js +51 -0
- package/schema/pickFormFields.js +104 -0
- package/server.js +12 -0
- package/utils/uuid.cjs +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# TeamPlay
|
|
2
|
+
|
|
3
|
+
> Deep signals ORM for React with real-time collaboration
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
|
|
7
|
+
- signals __*__
|
|
8
|
+
- multiplayer __**__
|
|
9
|
+
- ORM
|
|
10
|
+
- auto-sync data from client to DB and vice-versa __***__
|
|
11
|
+
- query DB directly from client __***__
|
|
12
|
+
|
|
13
|
+
> __*__ deep signals -- with support for objects and arrays\
|
|
14
|
+
> __**__ concurrent changes to the same data are auto-merged using [OT](https://en.wikipedia.org/wiki/Operational_transformation)\
|
|
15
|
+
> __***__ similar to Firebase but with your own MongoDB-compatible database
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Client-only mode
|
|
20
|
+
|
|
21
|
+
If you just want a client-only mode without any data being synced to the server, then you don't need to setup anything and can jump directly to [Usage](#usage).
|
|
22
|
+
|
|
23
|
+
### Synchronization of data with server
|
|
24
|
+
|
|
25
|
+
Enable the connection on client somewhere early in your client app:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import connect from 'teamplay/connect'
|
|
29
|
+
connect()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
And on the server, manually create a [ShareDB's backend](https://share.github.io/sharedb/api/backend) and create a connection handler for WebSockets:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { initConnection } from 'teamplay/server'
|
|
36
|
+
const { upgrade } = initConnection(backend) // ShareDB's Backend instance
|
|
37
|
+
server.on('upgrade', upgrade) // Node's 'http' server instance
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
TBD
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
### Simplest example with server synchronization
|
|
47
|
+
|
|
48
|
+
On the client we `connect()` to the server, and we have to wrap each React component into `observer()`:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// client.js
|
|
52
|
+
import connect from 'teamplay/connect'
|
|
53
|
+
import { observer, $, sub } from 'teamplay'
|
|
54
|
+
import { createRoot } from 'react-dom/client'
|
|
55
|
+
import { createElement as el } from 'react'
|
|
56
|
+
|
|
57
|
+
connect()
|
|
58
|
+
|
|
59
|
+
const App = observer(({ userId }) => {
|
|
60
|
+
const $user = sub($.users[userId])
|
|
61
|
+
if (!$user.get()) throw $user.set({ points: 0 })
|
|
62
|
+
const { $points } = $user
|
|
63
|
+
const onClick = () => $points.set($points.get() + 1)
|
|
64
|
+
return el('button', { onClick }, 'Points: ' + $points.get())
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const container = document.body.appendChild(document.createElement('div'))
|
|
68
|
+
createRoot(container).render(
|
|
69
|
+
el(App, { userId: '_1' })
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On the server we create the ShareDB backend and initialize the WebSocket connections handler:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// server.js
|
|
77
|
+
import http from 'http'
|
|
78
|
+
import { ShareDB, initConnection } from 'teamplay/server'
|
|
79
|
+
|
|
80
|
+
const server = http.createServer() // you can pass expressApp here if needed
|
|
81
|
+
const backend = new ShareDB()
|
|
82
|
+
const { upgrade } = initConnection(backend)
|
|
83
|
+
|
|
84
|
+
server.on('upgrade', upgrade)
|
|
85
|
+
|
|
86
|
+
server.listen(3000)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
ShareDB is a re-export of [`sharedb`](https://github.com/share/sharedb) library, check its docs for more info.
|
|
90
|
+
- for persistency and queries support pass [`sharedb-mongo`](https://github.com/share/sharedb-mongo) (which uses MongoDB) as `{ db }`
|
|
91
|
+
- when deploying to a cluster with multiple instances you also have to provide `{ pubsub }` like [`sharedb-redis-pubsub`](https://github.com/share/sharedb-redis-pubsub) (which uses Redis)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
package/connect/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Socket from '@startupjs/channel'
|
|
2
|
+
import Connection from './sharedbConnection.cjs'
|
|
3
|
+
import { connection, setConnection } from '../orm/connection.js'
|
|
4
|
+
|
|
5
|
+
export default function connect ({
|
|
6
|
+
baseUrl,
|
|
7
|
+
...options
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (connection) return
|
|
10
|
+
const socket = new Socket({ baseUrl, ...options })
|
|
11
|
+
setConnection(new Connection(socket))
|
|
12
|
+
}
|
package/connect/test.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// mock of client connection to sharedb to use inside tests.
|
|
2
|
+
// This just creates a sharedb server with in-memory database
|
|
3
|
+
// and creates a server connection to it.
|
|
4
|
+
import ShareBackend from 'sharedb'
|
|
5
|
+
import ShareDbMingo from 'sharedb-mingo-memory'
|
|
6
|
+
import { connection, setConnection } from '../orm/connection.js'
|
|
7
|
+
|
|
8
|
+
export default function connect () {
|
|
9
|
+
if (connection) return
|
|
10
|
+
const backend = new ShareBackend({ db: new ShareDbMingo() })
|
|
11
|
+
setConnection(backend.connect())
|
|
12
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// NOTE:
|
|
2
|
+
// $() and sub() are currently set to be universal ones which work in both
|
|
3
|
+
// plain JS and React environments. In React they are tied to the observer() HOC.
|
|
4
|
+
// This is done to simplify the API.
|
|
5
|
+
// In future, we might want to separate the plain JS and React APIs
|
|
6
|
+
import { getRootSignal as _getRootSignal, GLOBAL_ROOT_ID } from './orm/Root.js'
|
|
7
|
+
import universal$ from './react/universal$.js'
|
|
8
|
+
|
|
9
|
+
export { default as Signal, SEGMENTS } from './orm/Signal.js'
|
|
10
|
+
export { __DEBUG_SIGNALS_CACHE__, rawSignal, getSignalClass } from './orm/getSignal.js'
|
|
11
|
+
export { default as addModel } from './orm/addModel.js'
|
|
12
|
+
export { default as signal } from './orm/getSignal.js'
|
|
13
|
+
export { GLOBAL_ROOT_ID } from './orm/Root.js'
|
|
14
|
+
export const $ = _getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
|
|
15
|
+
export default $
|
|
16
|
+
export { default as sub } from './react/universalSub.js'
|
|
17
|
+
export { default as observer } from './react/observer.js'
|
|
18
|
+
export { connection, setConnection, getConnection, fetchOnly, setFetchOnly } from './orm/connection.js'
|
|
19
|
+
export * from './schema/associations.js'
|
|
20
|
+
export { default as GUID_PATTERN } from './schema/GUID_PATTERN.js'
|
|
21
|
+
export { default as pickFormFields } from './schema/pickFormFields.js'
|
|
22
|
+
|
|
23
|
+
export function getRootSignal (options) {
|
|
24
|
+
return _getRootSignal({
|
|
25
|
+
rootFunction: universal$,
|
|
26
|
+
...options
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// the following are react-specific hook alternatives to $() and sub() functions.
|
|
31
|
+
// In future we might want to expose them, but at the current time they are not needed
|
|
32
|
+
// and instead just the regular $() and sub() functions are used since they are universal
|
|
33
|
+
//
|
|
34
|
+
// export function use$ (value) {
|
|
35
|
+
// // TODO: maybe replace all non-letter/digit characters with underscores
|
|
36
|
+
// const id = useId() // eslint-disable-line react-hooks/rules-of-hooks
|
|
37
|
+
// return $(value, id)
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
// export function useSub (...args) {
|
|
41
|
+
// const promiseOrSignal = sub(...args)
|
|
42
|
+
// if (promiseOrSignal.then) throw promiseOrSignal
|
|
43
|
+
// return promiseOrSignal
|
|
44
|
+
// }
|
package/orm/$.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// this is just the $() function implementation.
|
|
2
|
+
// The actual $ exported from this package is a Proxy targeting the dataTree root,
|
|
3
|
+
// and this function is an implementation of the `apply` handler for that Proxy.
|
|
4
|
+
import getSignal from './getSignal.js'
|
|
5
|
+
import Signal from './Signal.js'
|
|
6
|
+
import { LOCAL, valueSubscriptions } from './Value.js'
|
|
7
|
+
import { reactionSubscriptions } from './Reaction.js'
|
|
8
|
+
|
|
9
|
+
export { LOCAL } from './Value.js'
|
|
10
|
+
|
|
11
|
+
let counter = 0
|
|
12
|
+
|
|
13
|
+
function newIncrementalId () {
|
|
14
|
+
const id = `_${counter}`
|
|
15
|
+
counter += 1
|
|
16
|
+
return id
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function $ ($root, value, id) {
|
|
20
|
+
if (!($root instanceof Signal)) throw Error('First argument of $() should be a Root Signal')
|
|
21
|
+
if (typeof value === 'function') {
|
|
22
|
+
return reaction$($root, value, id)
|
|
23
|
+
} else {
|
|
24
|
+
return value$($root, value, id)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function value$ ($root, value, id = newIncrementalId()) {
|
|
29
|
+
const $value = getSignal($root, [LOCAL, id])
|
|
30
|
+
valueSubscriptions.init($value, value)
|
|
31
|
+
return $value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function reaction$ ($root, fn, id = newIncrementalId()) {
|
|
35
|
+
const $value = getSignal($root, [LOCAL, id])
|
|
36
|
+
reactionSubscriptions.init($value, fn)
|
|
37
|
+
return $value
|
|
38
|
+
}
|
package/orm/Cache.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default class Cache {
|
|
2
|
+
constructor () {
|
|
3
|
+
this.cache = new Map()
|
|
4
|
+
this.fr = new FinalizationRegistry(([key]) => this.delete(key))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get (key) {
|
|
8
|
+
return this.cache.get(key)?.deref()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} key
|
|
13
|
+
* @param {*} value
|
|
14
|
+
* @param {Array} inputs - extra inputs to register with the finalization registry
|
|
15
|
+
* to hold strong references to them until the value is garbage collected
|
|
16
|
+
*/
|
|
17
|
+
set (key, value, inputs = []) {
|
|
18
|
+
this.cache.set(key, new WeakRef(value))
|
|
19
|
+
this.fr.register(value, [key, ...inputs])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
delete (key) {
|
|
23
|
+
this.cache.delete(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get size () {
|
|
27
|
+
return this.cache.size
|
|
28
|
+
}
|
|
29
|
+
}
|
package/orm/Doc.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { isObservable, observable } from '@nx-js/observer-util'
|
|
2
|
+
import { set as _set, del as _del } from './dataTree.js'
|
|
3
|
+
import { SEGMENTS } from './Signal.js'
|
|
4
|
+
import { getConnection, fetchOnly } from './connection.js'
|
|
5
|
+
|
|
6
|
+
const ERROR_ON_EXCESSIVE_UNSUBSCRIBES = false
|
|
7
|
+
|
|
8
|
+
class Doc {
|
|
9
|
+
subscribing
|
|
10
|
+
unsubscribing
|
|
11
|
+
subscribed
|
|
12
|
+
initialized
|
|
13
|
+
|
|
14
|
+
constructor (collection, docId) {
|
|
15
|
+
this.collection = collection
|
|
16
|
+
this.docId = docId
|
|
17
|
+
this.init()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
init () {
|
|
21
|
+
if (this.initialized) return
|
|
22
|
+
this.initialized = true
|
|
23
|
+
this._initData()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async subscribe () {
|
|
27
|
+
if (this.subscribed) throw Error('trying to subscribe while already subscribed')
|
|
28
|
+
this.subscribed = true
|
|
29
|
+
// if we are in the middle of unsubscribing, just wait for it to finish and then resubscribe
|
|
30
|
+
if (this.unsubscribing) {
|
|
31
|
+
try {
|
|
32
|
+
await this.unsubscribing
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// if error happened during unsubscribing, it means that we are still subscribed
|
|
35
|
+
// so we don't need to do anything
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (this.subscribing) {
|
|
40
|
+
try {
|
|
41
|
+
await this.subscribing
|
|
42
|
+
// if we are already subscribing from the previous time, delegate logic to that
|
|
43
|
+
// and if it finished successfully, we are done.
|
|
44
|
+
return
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// if error happened during subscribing, we'll just try subscribing again
|
|
47
|
+
// so we just ignore the error and proceed with subscribing
|
|
48
|
+
this.subscribed = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!this.subscribed) return // cancel if we initiated unsubscribe while waiting
|
|
53
|
+
|
|
54
|
+
this.subscribing = (async () => {
|
|
55
|
+
try {
|
|
56
|
+
this.subscribing = this._subscribe()
|
|
57
|
+
await this.subscribing
|
|
58
|
+
this.init()
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.log('subscription error', [this.collection, this.docId], err)
|
|
61
|
+
this.subscribed = undefined
|
|
62
|
+
throw err
|
|
63
|
+
} finally {
|
|
64
|
+
this.subscribing = undefined
|
|
65
|
+
}
|
|
66
|
+
})()
|
|
67
|
+
await this.subscribing
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _subscribe () {
|
|
71
|
+
const doc = getConnection().get(this.collection, this.docId)
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
const method = fetchOnly ? 'fetch' : 'subscribe'
|
|
74
|
+
doc[method](err => {
|
|
75
|
+
if (err) return reject(err)
|
|
76
|
+
resolve()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async unsubscribe () {
|
|
82
|
+
if (!this.subscribed) throw Error('trying to unsubscribe while not subscribed')
|
|
83
|
+
this.subscribed = undefined
|
|
84
|
+
// if we are still handling the subscription, just wait for it to finish and then unsubscribe
|
|
85
|
+
if (this.subscribing) {
|
|
86
|
+
try {
|
|
87
|
+
await this.subscribing
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// if error happened during subscribing, it means that we are still unsubscribed
|
|
90
|
+
// so we don't need to do anything
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// if we are already unsubscribing from the previous time, delegate logic to that
|
|
95
|
+
if (this.unsubscribing) {
|
|
96
|
+
try {
|
|
97
|
+
await this.unsubscribing
|
|
98
|
+
return
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// if error happened during unsubscribing, we'll just try unsubscribing again
|
|
101
|
+
this.subscribed = undefined
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.subscribed) return // cancel if we initiated subscribe while waiting
|
|
106
|
+
|
|
107
|
+
this.unsubscribing = (async () => {
|
|
108
|
+
try {
|
|
109
|
+
await this._unsubscribe()
|
|
110
|
+
this.initialized = undefined
|
|
111
|
+
this._removeData()
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log('error unsubscribing', [this.collection, this.docId], err)
|
|
114
|
+
this.subscribed = true
|
|
115
|
+
throw err
|
|
116
|
+
} finally {
|
|
117
|
+
this.unsubscribing = undefined
|
|
118
|
+
}
|
|
119
|
+
})()
|
|
120
|
+
await this.unsubscribing
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async _unsubscribe () {
|
|
124
|
+
const doc = getConnection().get(this.collection, this.docId)
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
doc.destroy(err => {
|
|
127
|
+
if (err) return reject(err)
|
|
128
|
+
resolve()
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_initData () {
|
|
134
|
+
const doc = getConnection().get(this.collection, this.docId)
|
|
135
|
+
// TODO: JSON does not have `undefined`, so we'll be receiving `null`.
|
|
136
|
+
// Handle this by converting all `null` to `undefined` in the doc's data tree.
|
|
137
|
+
// To do this we'll probably need to in the `op` event update the data tree
|
|
138
|
+
// and have a clone of the doc in our local data tree.
|
|
139
|
+
this._refData()
|
|
140
|
+
doc.on('load', () => this._refData())
|
|
141
|
+
doc.on('create', () => this._refData())
|
|
142
|
+
doc.on('del', () => _del([this.collection, this.docId]))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_refData () {
|
|
146
|
+
const doc = getConnection().get(this.collection, this.docId)
|
|
147
|
+
if (isObservable(doc.data)) return
|
|
148
|
+
if (doc.data == null) return
|
|
149
|
+
_set([this.collection, this.docId], doc.data)
|
|
150
|
+
doc.data = observable(doc.data)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_removeData () {
|
|
154
|
+
_del([this.collection, this.docId])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class DocSubscriptions {
|
|
159
|
+
constructor () {
|
|
160
|
+
this.subCount = new Map()
|
|
161
|
+
this.docs = new Map()
|
|
162
|
+
this.initialized = new Map()
|
|
163
|
+
this.fr = new FinalizationRegistry(segments => this.destroy(segments))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
init ($doc) {
|
|
167
|
+
const segments = [...$doc[SEGMENTS]]
|
|
168
|
+
const hash = hashDoc(segments)
|
|
169
|
+
if (this.initialized.has(hash)) return
|
|
170
|
+
this.initialized.set(hash, true)
|
|
171
|
+
|
|
172
|
+
this.fr.register($doc, segments, $doc)
|
|
173
|
+
|
|
174
|
+
let doc = this.docs.get(hash)
|
|
175
|
+
if (!doc) {
|
|
176
|
+
doc = new Doc(...segments)
|
|
177
|
+
this.docs.set(hash, doc)
|
|
178
|
+
}
|
|
179
|
+
doc.init()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
subscribe ($doc) {
|
|
183
|
+
const segments = [...$doc[SEGMENTS]]
|
|
184
|
+
const hash = hashDoc(segments)
|
|
185
|
+
let count = this.subCount.get(hash) || 0
|
|
186
|
+
count += 1
|
|
187
|
+
this.subCount.set(hash, count)
|
|
188
|
+
if (count > 1) return this.docs.get(hash).subscribing
|
|
189
|
+
|
|
190
|
+
this.init($doc)
|
|
191
|
+
const doc = this.docs.get(hash)
|
|
192
|
+
return doc.subscribe()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async unsubscribe ($doc) {
|
|
196
|
+
const segments = [...$doc[SEGMENTS]]
|
|
197
|
+
const hash = hashDoc(segments)
|
|
198
|
+
let count = this.subCount.get(hash) || 0
|
|
199
|
+
count -= 1
|
|
200
|
+
if (count < 0) {
|
|
201
|
+
if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($doc)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if (count > 0) {
|
|
205
|
+
this.subCount.set(hash, count)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
this.fr.unregister($doc)
|
|
209
|
+
this.destroy(segments)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async destroy (segments) {
|
|
213
|
+
const hash = hashDoc(segments)
|
|
214
|
+
const doc = this.docs.get(hash)
|
|
215
|
+
if (!doc) return
|
|
216
|
+
this.subCount.delete(hash)
|
|
217
|
+
this.initialized.delete(hash)
|
|
218
|
+
await doc.unsubscribe()
|
|
219
|
+
if (doc.subscribed) return // if we subscribed again while waiting for unsubscribe, we don't delete the doc
|
|
220
|
+
this.docs.delete(hash)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const docSubscriptions = new DocSubscriptions()
|
|
225
|
+
|
|
226
|
+
function hashDoc (segments) {
|
|
227
|
+
return JSON.stringify(segments)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const ERRORS = {
|
|
231
|
+
notSubscribed: $doc => Error('trying to unsubscribe when not subscribed. Doc: ' + $doc.path())
|
|
232
|
+
}
|