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/orm/Query.js ADDED
@@ -0,0 +1,278 @@
1
+ import { raw } from '@nx-js/observer-util'
2
+ import { get as _get, set as _set, del as _del } from './dataTree.js'
3
+ import { SEGMENTS } from './Signal.js'
4
+ import getSignal from './getSignal.js'
5
+ import { getConnection, fetchOnly } from './connection.js'
6
+ import { docSubscriptions } from './Doc.js'
7
+
8
+ const ERROR_ON_EXCESSIVE_UNSUBSCRIBES = false
9
+ export const PARAMS = Symbol('query params')
10
+ export const HASH = Symbol('query hash')
11
+ export const IS_QUERY = Symbol('is query signal')
12
+ export const QUERIES = '$queries'
13
+
14
+ class Query {
15
+ subscribing
16
+ unsubscribing
17
+ subscribed
18
+ initialized
19
+ shareQuery
20
+
21
+ constructor (collection, params) {
22
+ this.collection = collection
23
+ this.params = params
24
+ this.hash = hashQuery([this.collection], this.params)
25
+ this.docSignals = new Set()
26
+ }
27
+
28
+ init () {
29
+ if (this.initialized) return
30
+ this.initialized = true
31
+ this._initData()
32
+ }
33
+
34
+ async subscribe () {
35
+ if (this.subscribed) throw Error('trying to subscribe while already subscribed')
36
+ this.subscribed = true
37
+ // if we are in the middle of unsubscribing, just wait for it to finish and then resubscribe
38
+ if (this.unsubscribing) {
39
+ try {
40
+ await this.unsubscribing
41
+ } catch (err) {
42
+ // if error happened during unsubscribing, it means that we are still subscribed
43
+ // so we don't need to do anything
44
+ return
45
+ }
46
+ }
47
+ if (this.subscribing) {
48
+ try {
49
+ await this.subscribing
50
+ // if we are already subscribing from the previous time, delegate logic to that
51
+ // and if it finished successfully, we are done.
52
+ return
53
+ } catch (err) {
54
+ // if error happened during subscribing, we'll just try subscribing again
55
+ // so we just ignore the error and proceed with subscribing
56
+ this.subscribed = true
57
+ }
58
+ }
59
+
60
+ if (!this.subscribed) return // cancel if we initiated unsubscribe while waiting
61
+
62
+ this.subscribing = (async () => {
63
+ try {
64
+ this.subscribing = this._subscribe()
65
+ await this.subscribing
66
+ this.init()
67
+ } catch (err) {
68
+ console.log('subscription error', [this.collection, this.params], err)
69
+ this.subscribed = undefined
70
+ throw err
71
+ } finally {
72
+ this.subscribing = undefined
73
+ }
74
+ })()
75
+ await this.subscribing
76
+ }
77
+
78
+ async _subscribe () {
79
+ await new Promise((resolve, reject) => {
80
+ const method = fetchOnly ? 'createFetchQuery' : 'createSubscribeQuery'
81
+ this.shareQuery = getConnection()[method](this.collection, this.params, {}, err => {
82
+ if (err) return reject(err)
83
+ resolve()
84
+ })
85
+ })
86
+ }
87
+
88
+ async unsubscribe () {
89
+ if (!this.subscribed) throw Error('trying to unsubscribe while not subscribed')
90
+ this.subscribed = undefined
91
+ // if we are still handling the subscription, just wait for it to finish and then unsubscribe
92
+ if (this.subscribing) {
93
+ try {
94
+ await this.subscribing
95
+ } catch (err) {
96
+ // if error happened during subscribing, it means that we are still unsubscribed
97
+ // so we don't need to do anything
98
+ return
99
+ }
100
+ }
101
+ // if we are already unsubscribing from the previous time, delegate logic to that
102
+ if (this.unsubscribing) {
103
+ try {
104
+ await this.unsubscribing
105
+ return
106
+ } catch (err) {
107
+ // if error happened during unsubscribing, we'll just try unsubscribing again
108
+ this.subscribed = undefined
109
+ }
110
+ }
111
+
112
+ if (this.subscribed) return // cancel if we initiated subscribe while waiting
113
+
114
+ this.unsubscribing = (async () => {
115
+ try {
116
+ await this._unsubscribe()
117
+ this.initialized = undefined
118
+ this._removeData()
119
+ } catch (err) {
120
+ console.log('error unsubscribing', [this.collection, this.params], err)
121
+ this.subscribed = true
122
+ throw err
123
+ } finally {
124
+ this.unsubscribing = undefined
125
+ }
126
+ })()
127
+ await this.unsubscribing
128
+ }
129
+
130
+ async _unsubscribe () {
131
+ if (!this.shareQuery) throw Error('this.shareQuery is not defined. This should never happen')
132
+ await new Promise((resolve, reject) => {
133
+ this.shareQuery.destroy(err => {
134
+ if (err) return reject(err)
135
+ resolve()
136
+ })
137
+ this.shareQuery = undefined
138
+ })
139
+ }
140
+
141
+ _initData () {
142
+ { // reference the fetched docs
143
+ const docs = this.shareQuery.results.map(doc => raw(doc.data))
144
+ _set([QUERIES, this.hash, 'docs'], docs)
145
+
146
+ const ids = this.shareQuery.results.map(doc => doc.id)
147
+ for (const docId of ids) {
148
+ const $doc = getSignal(undefined, [this.collection, docId])
149
+ docSubscriptions.init($doc)
150
+ this.docSignals.add($doc)
151
+ }
152
+ _set([QUERIES, this.hash, 'ids'], ids)
153
+ }
154
+
155
+ this.shareQuery.on('insert', (shareDocs, index) => {
156
+ const newDocs = shareDocs.map(doc => raw(doc.data))
157
+ _get([QUERIES, this.hash, 'docs']).splice(index, 0, ...newDocs)
158
+
159
+ const ids = shareDocs.map(doc => doc.id)
160
+ for (const docId of ids) {
161
+ const $doc = getSignal(undefined, [this.collection, docId])
162
+ docSubscriptions.init($doc)
163
+ this.docSignals.add($doc)
164
+ }
165
+ _get([QUERIES, this.hash, 'ids']).splice(index, 0, ...ids)
166
+ })
167
+ this.shareQuery.on('move', (shareDocs, from, to) => {
168
+ const docs = _get([QUERIES, this.hash, 'docs'])
169
+ docs.splice(from, shareDocs.length)
170
+ docs.splice(to, 0, ...shareDocs.map(doc => raw(doc.data)))
171
+
172
+ const ids = _get([QUERIES, this.hash, 'ids'])
173
+ ids.splice(from, shareDocs.length)
174
+ ids.splice(to, 0, ...shareDocs.map(doc => doc.id))
175
+ })
176
+ this.shareQuery.on('remove', (shareDocs, index) => {
177
+ const docs = _get([QUERIES, this.hash, 'docs'])
178
+ docs.splice(index, shareDocs.length)
179
+
180
+ const docIds = shareDocs.map(doc => doc.id)
181
+ for (const docId of docIds) {
182
+ const $doc = getSignal(undefined, [this.collection, docId])
183
+ this.docSignals.delete($doc)
184
+ }
185
+ const ids = _get([QUERIES, this.hash, 'ids'])
186
+ ids.splice(index, docIds.length)
187
+ })
188
+ }
189
+
190
+ _removeData () {
191
+ this.docSignals.clear()
192
+ _del([QUERIES, this.hash])
193
+ }
194
+ }
195
+
196
+ class QuerySubscriptions {
197
+ constructor () {
198
+ this.subCount = new Map()
199
+ this.queries = new Map()
200
+ this.fr = new FinalizationRegistry(({ segments, params }) => this.destroy(segments, params))
201
+ }
202
+
203
+ subscribe ($query) {
204
+ const segments = [...$query[SEGMENTS]]
205
+ const params = JSON.parse(JSON.stringify($query[PARAMS]))
206
+ const hash = hashQuery(segments, params)
207
+ let count = this.subCount.get(hash) || 0
208
+ count += 1
209
+ this.subCount.set(hash, count)
210
+ if (count > 1) return this.queries.get(hash).subscribing
211
+
212
+ this.fr.register($query, { segments, params }, $query)
213
+
214
+ let query = this.queries.get(hash)
215
+ if (!query) {
216
+ query = new Query(segments[0], params)
217
+ this.queries.set(hash, query)
218
+ }
219
+ return query.subscribe()
220
+ }
221
+
222
+ async unsubscribe ($query) {
223
+ const segments = [...$query[SEGMENTS]]
224
+ const params = JSON.parse(JSON.stringify($query[PARAMS]))
225
+ const hash = hashQuery(segments, params)
226
+ let count = this.subCount.get(hash) || 0
227
+ count -= 1
228
+ if (count < 0) {
229
+ if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($query)
230
+ return
231
+ }
232
+ if (count > 0) {
233
+ this.subCount.set(hash, count)
234
+ return
235
+ }
236
+ this.subCount.delete(hash)
237
+ this.fr.unregister($query)
238
+ const query = this.queries.get(hash)
239
+ await query.unsubscribe()
240
+ if (query.subscribed) return // if we subscribed again while waiting for unsubscribe, we don't delete the doc
241
+ this.queries.delete(hash)
242
+ }
243
+
244
+ async destroy (segments, params) {
245
+ const hash = hashQuery(segments, params)
246
+ const query = this.queries.get(hash)
247
+ if (!query) return
248
+ this.subCount.delete(hash)
249
+ await query.unsubscribe()
250
+ if (query.subscribed) return // if we subscribed again while waiting for unsubscribe, we don't delete the doc
251
+ this.queries.delete(hash)
252
+ }
253
+ }
254
+
255
+ export const querySubscriptions = new QuerySubscriptions()
256
+
257
+ export function hashQuery (segments, params) {
258
+ // TODO: probably makes sense to use fast-stable-json-stringify for this because of the params
259
+ return JSON.stringify({ query: [segments[0], params] })
260
+ }
261
+
262
+ export function getQuerySignal (segments, params, options) {
263
+ params = JSON.parse(JSON.stringify(params))
264
+ const hash = hashQuery(segments, params)
265
+
266
+ const $query = getSignal(undefined, segments, {
267
+ signalHash: hash,
268
+ ...options
269
+ })
270
+ $query[IS_QUERY] ??= true
271
+ $query[PARAMS] ??= params
272
+ $query[HASH] ??= hash
273
+ return $query
274
+ }
275
+
276
+ const ERRORS = {
277
+ notSubscribed: $doc => Error('trying to unsubscribe when not subscribed. Doc: ' + $doc.path())
278
+ }
@@ -0,0 +1,44 @@
1
+ import { observe, unobserve } from '@nx-js/observer-util'
2
+ import { SEGMENTS } from './Signal.js'
3
+ import { set as _set, del as _del } from './dataTree.js'
4
+ import { LOCAL } from './Value.js'
5
+
6
+ // this is `let` to be able to directly change it if needed in tests or in the app
7
+ export let DELETION_DELAY = 0 // eslint-disable-line prefer-const
8
+
9
+ class ReactionSubscriptions {
10
+ constructor () {
11
+ this.initialized = new Map()
12
+ this.fr = new FinalizationRegistry(([id, reaction]) => this.destroy(id, reaction))
13
+ }
14
+
15
+ init ($value, fn) {
16
+ const id = $value[SEGMENTS][1]
17
+ if (this.initialized.has(id)) return
18
+
19
+ this.initialized.set(id, true)
20
+ const reactionScheduler = reaction => runReaction(id, reaction)
21
+ const reaction = observe(fn, { lazy: true, scheduler: reactionScheduler })
22
+ this.fr.register($value, [id, reaction])
23
+ runReaction(id, reaction)
24
+ }
25
+
26
+ destroy (id, reaction) {
27
+ this.initialized.delete(id)
28
+ unobserve(reaction)
29
+ // don't delete data right away to prevent dependent reactions which are also going to be GC'ed
30
+ // from triggering unnecessarily
31
+ setTimeout(() => _del([LOCAL, id]), DELETION_DELAY)
32
+ }
33
+ }
34
+
35
+ export const reactionSubscriptions = new ReactionSubscriptions()
36
+
37
+ function runReaction (id, reaction) {
38
+ const newValue = reaction()
39
+ _set([LOCAL, id], newValue)
40
+ }
41
+
42
+ export function setDeletionDelay (delayInMs) {
43
+ DELETION_DELAY = delayInMs
44
+ }
package/orm/Root.js ADDED
@@ -0,0 +1,41 @@
1
+ import getSignal from './getSignal.js'
2
+
3
+ export const ROOT_FUNCTION = Symbol('root function')
4
+ // TODO: in future make a connection spawnable instead of a singleton
5
+ // export const CONNECTION = Symbol('sharedb connection, used by sub() function')
6
+ export const ROOT = Symbol('root signal')
7
+ export const ROOT_ID = Symbol('root signal id. Used for caching')
8
+
9
+ export const GLOBAL_ROOT_ID = '__global__'
10
+
11
+ // TODO: create a separate local root for private collections
12
+ export function getRootSignal ({
13
+ rootFunction,
14
+ // connection,
15
+ rootId = '_' + createRandomString(8),
16
+ ...options
17
+ }) {
18
+ const $root = getSignal(undefined, [], {
19
+ rootId,
20
+ ...options
21
+ })
22
+ $root[ROOT_FUNCTION] ??= rootFunction
23
+ // $root[CONNECTION] ??= connection
24
+ $root[ROOT_ID] ??= rootId
25
+ return $root
26
+ }
27
+
28
+ export function getRoot (signal) {
29
+ if (signal[ROOT]) return signal[ROOT]
30
+ else if (signal[ROOT_ID]) return signal
31
+ else return undefined
32
+ }
33
+
34
+ function createRandomString (length) {
35
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
36
+ let result = ''
37
+ for (let i = 0; i < length; i++) {
38
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
39
+ }
40
+ return result
41
+ }
package/orm/Signal.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Implementation of the BaseSignal class which is used as a base class for all signals
3
+ * and can be extended to create custom models for a particular path pattern of the data tree.
4
+ *
5
+ * All signals in the app should be created using getSignal() function which automatically
6
+ * determines the correct model for the given path pattern and wraps the signal object in a Proxy.
7
+ *
8
+ * Proxy is used for the following reasons:
9
+ * 1. To allow accessing child signals using dot syntax
10
+ * 2. To be able to call the top-level signal as a `$()` function
11
+ * 3. If extremely late bindings are enabled, to prevent name collisions when accessing fields
12
+ * in the raw data tree which have the same name as signal's methods
13
+ */
14
+ import { get as _get, set as _set, del as _del, setPublicDoc as _setPublicDoc } from './dataTree.js'
15
+ import getSignal, { rawSignal } from './getSignal.js'
16
+ import { IS_QUERY, HASH, QUERIES } from './Query.js'
17
+ import { ROOT_FUNCTION, getRoot } from './Root.js'
18
+ import uuid from '../utils/uuid.cjs'
19
+
20
+ export const SEGMENTS = Symbol('path segments targeting the particular node in the data tree')
21
+
22
+ export default class Signal extends Function {
23
+ constructor (segments) {
24
+ if (!Array.isArray(segments)) throw Error('Signal constructor expects an array of segments')
25
+ super()
26
+ this[SEGMENTS] = segments
27
+ }
28
+
29
+ path () {
30
+ if (arguments.length > 0) throw Error('Signal.path() does not accept any arguments')
31
+ return this[SEGMENTS].join('.')
32
+ }
33
+
34
+ id () {
35
+ return uuid()
36
+ }
37
+
38
+ get () {
39
+ if (arguments.length > 0) throw Error('Signal.get() does not accept any arguments')
40
+ if (this[IS_QUERY]) {
41
+ const hash = this[HASH]
42
+ return _get([QUERIES, hash, 'docs'])
43
+ }
44
+ return _get(this[SEGMENTS])
45
+ }
46
+
47
+ * [Symbol.iterator] () {
48
+ if (this[IS_QUERY]) {
49
+ const ids = _get([QUERIES, this[HASH], 'ids'])
50
+ for (const id of ids) yield getSignal(getRoot(this), [this[SEGMENTS][0], id])
51
+ } else {
52
+ const items = _get(this[SEGMENTS])
53
+ if (!Array.isArray(items)) return
54
+ for (let i = 0; i < items.length; i++) yield getSignal(getRoot(this), [...this[SEGMENTS], i])
55
+ }
56
+ }
57
+
58
+ map (...args) {
59
+ if (this[IS_QUERY]) {
60
+ const collection = this[SEGMENTS][0]
61
+ const hash = this[HASH]
62
+ const ids = _get([QUERIES, hash, 'ids'])
63
+ return ids.map(id => getSignal(getRoot(this), [collection, id])).map(...args)
64
+ }
65
+ const items = _get(this[SEGMENTS])
66
+ if (!Array.isArray(items)) return []
67
+ return Array(items.length)
68
+ .fill()
69
+ .map((_, index) => getSignal(getRoot(this), [...this[SEGMENTS], index]))
70
+ .map(...args)
71
+ }
72
+
73
+ async set (value) {
74
+ if (arguments.length > 1) throw Error('Signal.set() expects a single argument')
75
+ if (this[SEGMENTS].length === 0) throw Error('Can\'t set the root signal data')
76
+ if (isPublicCollection(this[SEGMENTS][0])) {
77
+ await _setPublicDoc(this[SEGMENTS], value)
78
+ } else {
79
+ _set(this[SEGMENTS], value)
80
+ }
81
+ }
82
+
83
+ // TODO: make it use an actual increment json0 operation on public collections
84
+ async increment (value) {
85
+ if (arguments.length > 1) throw Error('Signal.increment() expects a single argument')
86
+ if (value === undefined) value = 1
87
+ if (typeof value !== 'number') throw Error('Signal.increment() expects a number argument')
88
+ let currentValue = this.get()
89
+ if (currentValue === undefined) currentValue = 0
90
+ if (typeof currentValue !== 'number') throw Error('Signal.increment() tried to increment a non-number value')
91
+ await this.set(currentValue + value)
92
+ }
93
+
94
+ async add (value) {
95
+ if (arguments.length > 1) throw Error('Signal.add() expects a single argument')
96
+ let id
97
+ if (value.id) {
98
+ value = JSON.parse(JSON.stringify(value))
99
+ id = value.id
100
+ delete value.id
101
+ }
102
+ id ??= uuid()
103
+ await this[id].set(value)
104
+ }
105
+
106
+ async del () {
107
+ if (arguments.length > 0) throw Error('Signal.del() does not accept any arguments')
108
+ if (this[SEGMENTS].length === 0) throw Error('Can\'t delete the root signal data')
109
+ if (isPublicCollection(this[SEGMENTS][0])) {
110
+ if (this[SEGMENTS].length === 1) throw Error('Can\'t delete the whole collection')
111
+ await _setPublicDoc(this[SEGMENTS], undefined, true)
112
+ } else {
113
+ _del(this[SEGMENTS])
114
+ }
115
+ }
116
+
117
+ // clone () {}
118
+ // async assign () {}
119
+ // async push () {}
120
+ // async pop () {}
121
+ // async unshift () {}
122
+ // async shift () {}
123
+ // async splice () {}
124
+ // async move () {}
125
+ // async del () {}
126
+ }
127
+
128
+ // dot syntax returns a child signal only if no such method or property exists
129
+ export const regularBindings = {
130
+ apply (signal, thisArg, argumentsList) {
131
+ if (signal[SEGMENTS].length === 0) {
132
+ if (!signal[ROOT_FUNCTION]) throw Error(ERRORS.noRootFunction)
133
+ return signal[ROOT_FUNCTION].call(thisArg, signal, ...argumentsList)
134
+ }
135
+ throw Error('Signal can\'t be called as a function since extremely late bindings are disabled')
136
+ },
137
+ get (signal, key, receiver) {
138
+ if (key in signal) return Reflect.get(signal, key, receiver)
139
+ return Reflect.apply(extremelyLateBindings.get, this, arguments)
140
+ }
141
+ }
142
+
143
+ const QUERY_METHODS = ['map', 'get']
144
+
145
+ // dot syntax always returns a child signal even if such method or property exists.
146
+ // The method is only called when the signal is explicitly called as a function,
147
+ // in which case we get the original method from the raw (non-proxied) parent signal
148
+ export const extremelyLateBindings = {
149
+ apply (signal, thisArg, argumentsList) {
150
+ if (signal[SEGMENTS].length === 0) {
151
+ if (!signal[ROOT_FUNCTION]) throw Error(ERRORS.noRootFunction)
152
+ return signal[ROOT_FUNCTION].call(thisArg, signal, ...argumentsList)
153
+ }
154
+ const key = signal[SEGMENTS][signal[SEGMENTS].length - 1]
155
+ const $parent = getSignal(getRoot(signal), signal[SEGMENTS].slice(0, -1))
156
+ const rawParent = rawSignal($parent)
157
+ if (!(key in rawParent)) {
158
+ throw Error(`Method "${key}" does not exist on signal "${$parent[SEGMENTS].join('.')}"`)
159
+ }
160
+ return Reflect.apply(rawParent[key], $parent, argumentsList)
161
+ },
162
+ get (signal, key, receiver) {
163
+ if (typeof key === 'symbol') return Reflect.get(signal, key, receiver)
164
+ if (key === 'then') return undefined // handle checks for whether the symbol is a Promise
165
+ key = transformAlias(signal[SEGMENTS], key)
166
+ if (signal[IS_QUERY]) {
167
+ if (key === 'ids') return getSignal(getRoot(signal), [QUERIES, signal[HASH], 'ids'])
168
+ if (QUERY_METHODS.includes(key)) return Reflect.get(signal, key, receiver)
169
+ }
170
+ return getSignal(getRoot(signal), [...signal[SEGMENTS], key])
171
+ }
172
+ }
173
+
174
+ const transformAlias = (({
175
+ collectionsMapping = {
176
+ session: '_session',
177
+ page: '_page',
178
+ render: '$render',
179
+ system: '$system'
180
+ },
181
+ regex$ = /^\$/
182
+ } = {}) => (segments, key) => {
183
+ if (regex$.test(key)) key = key.slice(1)
184
+ if (segments.length === 0) key = collectionsMapping[key] || key
185
+ return key
186
+ })()
187
+
188
+ export function isPublicCollectionSignal ($signal) {
189
+ return $signal instanceof Signal && $signal[SEGMENTS].length === 1 && isPublicCollection($signal[SEGMENTS][0])
190
+ }
191
+
192
+ export function isPublicDocumentSignal ($signal) {
193
+ return $signal instanceof Signal && $signal[SEGMENTS].length === 2 && isPublicCollection($signal[SEGMENTS][0])
194
+ }
195
+
196
+ export function isPublicCollection (collectionName) {
197
+ if (!collectionName) return false
198
+ return !isPrivateCollection(collectionName)
199
+ }
200
+
201
+ export function isPrivateCollection (collectionName) {
202
+ if (!collectionName) return false
203
+ return /^[_$]/.test(collectionName)
204
+ }
205
+
206
+ const ERRORS = {
207
+ noRootFunction: `
208
+ Root signal does not have a root function set.
209
+ You must use getRootSignal({ rootId, rootFunction }) to create a root signal.
210
+ `
211
+ }
package/orm/Value.js ADDED
@@ -0,0 +1,27 @@
1
+ import { SEGMENTS } from './Signal.js'
2
+ import { set as _set, del as _del } from './dataTree.js'
3
+
4
+ export const LOCAL = '$local'
5
+
6
+ class ValueSubscriptions {
7
+ constructor () {
8
+ this.initialized = new Map()
9
+ this.fr = new FinalizationRegistry(id => this.destroy(id))
10
+ }
11
+
12
+ init ($value, value) {
13
+ const id = $value[SEGMENTS][1]
14
+ if (this.initialized.has(id)) return
15
+
16
+ _set([LOCAL, id], value)
17
+ this.initialized.set(id, true)
18
+ this.fr.register($value, id)
19
+ }
20
+
21
+ destroy (id) {
22
+ this.initialized.delete(id)
23
+ _del([LOCAL, id])
24
+ }
25
+ }
26
+
27
+ export const valueSubscriptions = new ValueSubscriptions()
@@ -0,0 +1,31 @@
1
+ export const MODELS = {}
2
+
3
+ export default function addModel (pattern, Model) {
4
+ if (typeof pattern !== 'string') throw Error('Model pattern must be a string, e.g. "users.*"')
5
+ if (/\s/.test(pattern)) throw Error('Model pattern can not have spaces')
6
+ if (typeof Model !== 'function') throw Error('Model must be a class')
7
+ pattern = pattern.replace(/\[[^\]]+\]/g, '*') // replace `[id]` with `*`
8
+ if (pattern !== '' && pattern.split('.').some(segment => segment === '')) {
9
+ throw Error('Model pattern can not have empty segments')
10
+ }
11
+ if (MODELS[pattern]) throw Error(`Model for pattern "${pattern}" already exists`)
12
+ MODELS[pattern] = Model
13
+ }
14
+
15
+ export function findModel (segments) {
16
+ // if segments is an empty array, treat it as a top-level signal.
17
+ // Top-level signal class is the one that has an empty string as a pattern.
18
+ if (segments.length === 0) segments = ['']
19
+ for (const pattern in MODELS) {
20
+ const patternSegments = pattern.split('.')
21
+ if (segments.length !== patternSegments.length) continue
22
+ let match = true
23
+ for (let i = 0; i < segments.length; i++) {
24
+ if (patternSegments[i] !== '*' && patternSegments[i] !== segments[i]) {
25
+ match = false
26
+ break
27
+ }
28
+ }
29
+ if (match) return MODELS[pattern]
30
+ }
31
+ }
@@ -0,0 +1,26 @@
1
+ export let connection
2
+ export let fetchOnly
3
+
4
+ export function setConnection (_connection) {
5
+ connection = _connection
6
+ }
7
+
8
+ export function getConnection () {
9
+ if (!connection) throw Error(ERRORS.notSet)
10
+ return connection
11
+ }
12
+
13
+ export function setFetchOnly (_fetchOnly) {
14
+ fetchOnly = _fetchOnly
15
+ }
16
+
17
+ const ERRORS = {
18
+ notSet: `
19
+ Connection is not set.
20
+ You must set the initialized ShareDB connection before using subscriptions.
21
+ You've probably forgotten to call connect() in your app:
22
+
23
+ import connect from '@startupjs/signals-orm/connect'
24
+ connect({ baseUrl: 'http://localhost:3000' })
25
+ `
26
+ }