teamplay 0.4.0-alpha.36 → 0.4.0-alpha.38
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 +12 -0
- package/orm/Compat/README.md +5 -0
- package/orm/Doc.js +26 -0
- package/orm/Query.js +6 -2
- package/orm/SignalBase.js +17 -0
- package/orm/associations.js +99 -0
- package/orm/index.d.ts +6 -0
- package/orm/index.js +5 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -19,6 +19,18 @@ Features:
|
|
|
19
19
|
|
|
20
20
|
For installation and documentation see [teamplay.dev](https://teamplay.dev)
|
|
21
21
|
|
|
22
|
+
## ORM Compat Helpers
|
|
23
|
+
|
|
24
|
+
For legacy Racer-style model mixins (for example versioning libraries which call
|
|
25
|
+
`getAssociations()`), use ORM compat helpers from the `teamplay/orm` subpath:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import BaseModel, { hasMany, hasOne, belongsTo } from 'teamplay/orm'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
These helpers attach class-level associations and expose them through
|
|
32
|
+
`$doc.getAssociations()` on model signals.
|
|
33
|
+
|
|
22
34
|
## License
|
|
23
35
|
|
|
24
36
|
MIT
|
package/orm/Compat/README.md
CHANGED
|
@@ -389,6 +389,11 @@ console.log(getSubscriptionGcDelay()) // 500
|
|
|
389
389
|
When refCount drops to `0`, unsubscribe/destroy is scheduled after this delay.
|
|
390
390
|
If a new subscribe arrives before timeout, pending destroy is cancelled and the same doc/query instance is reused.
|
|
391
391
|
|
|
392
|
+
Compat queries also retain lifecycle ownership of docs they materialize into DataTree.
|
|
393
|
+
This means a doc that arrived through `useQuery` / `useBatchQuery` will stay available
|
|
394
|
+
for immediate `useLocal` / `useModel` reads while that query remains subscribed, even if
|
|
395
|
+
some unrelated `useDoc` subscriber for the same `collection.id` unmounts.
|
|
396
|
+
|
|
392
397
|
### set(value) and set(path, value)
|
|
393
398
|
|
|
394
399
|
`SignalCompat` accepts both:
|
package/orm/Doc.js
CHANGED
|
@@ -149,6 +149,15 @@ export class DocSubscriptions {
|
|
|
149
149
|
return doc._subscribing
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
retain ($doc) {
|
|
153
|
+
const segments = [...$doc[SEGMENTS]]
|
|
154
|
+
const hash = hashDoc(segments)
|
|
155
|
+
this.cancelDestroy(hash)
|
|
156
|
+
const count = this.subCount.get(hash) || 0
|
|
157
|
+
this.subCount.set(hash, count + 1)
|
|
158
|
+
this.init($doc)
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
async unsubscribe ($doc) {
|
|
153
162
|
const segments = [...$doc[SEGMENTS]]
|
|
154
163
|
const hash = hashDoc(segments)
|
|
@@ -167,6 +176,23 @@ export class DocSubscriptions {
|
|
|
167
176
|
await this.scheduleDestroy(segments)
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
async release ($doc) {
|
|
180
|
+
const segments = [...$doc[SEGMENTS]]
|
|
181
|
+
const hash = hashDoc(segments)
|
|
182
|
+
let count = this.subCount.get(hash) || 0
|
|
183
|
+
count -= 1
|
|
184
|
+
if (count < 0) {
|
|
185
|
+
if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($doc)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (count > 0) {
|
|
189
|
+
this.subCount.set(hash, count)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
this.subCount.set(hash, 0)
|
|
193
|
+
await this.scheduleDestroy(segments)
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
async destroy (segments) {
|
|
171
197
|
const hash = hashDoc(segments)
|
|
172
198
|
await this.destroyByHash(hash, { force: true })
|
package/orm/Query.js
CHANGED
|
@@ -89,7 +89,7 @@ export class Query {
|
|
|
89
89
|
const ids = this.shareQuery.results.map(doc => doc.id)
|
|
90
90
|
for (const docId of ids) {
|
|
91
91
|
const $doc = getSignal(undefined, [this.collectionName, docId])
|
|
92
|
-
docSubscriptions.
|
|
92
|
+
docSubscriptions.retain($doc)
|
|
93
93
|
this.docSignals.add($doc)
|
|
94
94
|
}
|
|
95
95
|
_set([QUERIES, this.hash, 'ids'], ids)
|
|
@@ -112,7 +112,7 @@ export class Query {
|
|
|
112
112
|
const ids = shareDocs.map(doc => doc.id)
|
|
113
113
|
for (const docId of ids) {
|
|
114
114
|
const $doc = getSignal(undefined, [this.collectionName, docId])
|
|
115
|
-
docSubscriptions.
|
|
115
|
+
docSubscriptions.retain($doc)
|
|
116
116
|
this.docSignals.add($doc)
|
|
117
117
|
}
|
|
118
118
|
_get([QUERIES, this.hash, 'ids']).splice(index, 0, ...ids)
|
|
@@ -172,6 +172,7 @@ export class Query {
|
|
|
172
172
|
const docIds = shareDocs.map(doc => doc.id)
|
|
173
173
|
for (const docId of docIds) {
|
|
174
174
|
const $doc = getSignal(undefined, [this.collectionName, docId])
|
|
175
|
+
docSubscriptions.release($doc).catch(ignoreDestroyError)
|
|
175
176
|
this.docSignals.delete($doc)
|
|
176
177
|
}
|
|
177
178
|
const ids = _get([QUERIES, this.hash, 'ids'])
|
|
@@ -202,6 +203,9 @@ export class Query {
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
_removeData () {
|
|
206
|
+
for (const $doc of this.docSignals) {
|
|
207
|
+
docSubscriptions.release($doc).catch(ignoreDestroyError)
|
|
208
|
+
}
|
|
205
209
|
this.docSignals.clear()
|
|
206
210
|
_del([QUERIES, this.hash])
|
|
207
211
|
}
|
package/orm/SignalBase.js
CHANGED
|
@@ -60,6 +60,18 @@ export const DEFAULT_GETTERS = ['path', 'id', 'get', 'peek', 'getId', 'map', 're
|
|
|
60
60
|
export class Signal extends Function {
|
|
61
61
|
static ID_FIELDS = DEFAULT_ID_FIELDS
|
|
62
62
|
static [GETTERS] = DEFAULT_GETTERS
|
|
63
|
+
static associations = []
|
|
64
|
+
|
|
65
|
+
static addAssociation (association) {
|
|
66
|
+
if (!association || typeof association !== 'object') {
|
|
67
|
+
throw Error('Signal.addAssociation() expects an association object')
|
|
68
|
+
}
|
|
69
|
+
const inherited = this.associations || []
|
|
70
|
+
const own = Object.prototype.hasOwnProperty.call(this, 'associations')
|
|
71
|
+
? this.associations
|
|
72
|
+
: inherited.slice()
|
|
73
|
+
this.associations = own.concat(association)
|
|
74
|
+
}
|
|
63
75
|
|
|
64
76
|
constructor (segments) {
|
|
65
77
|
if (!Array.isArray(segments)) throw Error('Signal constructor expects an array of segments')
|
|
@@ -184,6 +196,11 @@ export class Signal extends Function {
|
|
|
184
196
|
return this[SEGMENTS][0]
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
getAssociations () {
|
|
200
|
+
const $raw = rawSignal(this) || this
|
|
201
|
+
return $raw.constructor.associations || []
|
|
202
|
+
}
|
|
203
|
+
|
|
187
204
|
* [Symbol.iterator] () {
|
|
188
205
|
if (this[IS_QUERY]) {
|
|
189
206
|
const ids = _get([QUERIES, this[HASH], 'ids'])
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function getCollectionName (OrmEntity, options = {}, helperName = 'association') {
|
|
2
|
+
if (options.key) return undefined
|
|
3
|
+
const collection = OrmEntity?.collection
|
|
4
|
+
if (typeof collection === 'string' && collection) return collection
|
|
5
|
+
throw new Error(
|
|
6
|
+
`teamplay/${helperName}: Associated model must define static "collection" ` +
|
|
7
|
+
'or pass options.key explicitly'
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toSingular (name) {
|
|
12
|
+
if (typeof name !== 'string' || !name) return name
|
|
13
|
+
if (name.endsWith('ies') && name.length > 3) return name.slice(0, -3) + 'y'
|
|
14
|
+
if (name.endsWith('sses') && name.length > 4) return name.slice(0, -2) // classes -> class
|
|
15
|
+
if (name.endsWith('ses') && name.length > 3) return name.slice(0, -2) // houses -> house
|
|
16
|
+
if (name.endsWith('s') && !name.endsWith('ss') && name.length > 1) return name.slice(0, -1)
|
|
17
|
+
return name
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function belongsTo (AssociatedOrmEntity, options = {}) {
|
|
21
|
+
return function decorateBelongsTo (OrmEntity) {
|
|
22
|
+
const key = options.key || (toSingular(
|
|
23
|
+
getCollectionName(AssociatedOrmEntity, options, 'belongsTo')
|
|
24
|
+
) + 'Id')
|
|
25
|
+
|
|
26
|
+
OrmEntity.addAssociation(
|
|
27
|
+
Object.assign({
|
|
28
|
+
type: 'belongsTo',
|
|
29
|
+
orm: AssociatedOrmEntity,
|
|
30
|
+
key
|
|
31
|
+
}, options)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
AssociatedOrmEntity.addAssociation(
|
|
35
|
+
Object.assign({
|
|
36
|
+
type: 'oppositeBelongsTo',
|
|
37
|
+
orm: OrmEntity,
|
|
38
|
+
key,
|
|
39
|
+
opposite: true
|
|
40
|
+
}, options)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return OrmEntity
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasMany (AssociatedOrmEntity, options = {}) {
|
|
48
|
+
return function decorateHasMany (OrmEntity) {
|
|
49
|
+
const key = options.key || (toSingular(
|
|
50
|
+
getCollectionName(AssociatedOrmEntity, options, 'hasMany')
|
|
51
|
+
) + 'Ids')
|
|
52
|
+
|
|
53
|
+
OrmEntity.addAssociation(
|
|
54
|
+
Object.assign({
|
|
55
|
+
type: 'hasMany',
|
|
56
|
+
orm: AssociatedOrmEntity,
|
|
57
|
+
key
|
|
58
|
+
}, options)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
AssociatedOrmEntity.addAssociation(
|
|
62
|
+
Object.assign({
|
|
63
|
+
type: 'oppositeHasMany',
|
|
64
|
+
orm: OrmEntity,
|
|
65
|
+
key,
|
|
66
|
+
opposite: true
|
|
67
|
+
}, options)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return OrmEntity
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function hasOne (AssociatedOrmEntity, options = {}) {
|
|
75
|
+
return function decorateHasOne (OrmEntity) {
|
|
76
|
+
const key = options.key || (toSingular(
|
|
77
|
+
getCollectionName(AssociatedOrmEntity, options, 'hasOne')
|
|
78
|
+
) + 'Id')
|
|
79
|
+
|
|
80
|
+
OrmEntity.addAssociation(
|
|
81
|
+
Object.assign({
|
|
82
|
+
type: 'hasOne',
|
|
83
|
+
orm: AssociatedOrmEntity,
|
|
84
|
+
key
|
|
85
|
+
}, options)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
AssociatedOrmEntity.addAssociation(
|
|
89
|
+
Object.assign({
|
|
90
|
+
type: 'oppositeHasOne',
|
|
91
|
+
orm: OrmEntity,
|
|
92
|
+
key,
|
|
93
|
+
opposite: true
|
|
94
|
+
}, options)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return OrmEntity
|
|
98
|
+
}
|
|
99
|
+
}
|
package/orm/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const BaseModel: any
|
|
2
|
+
export default BaseModel
|
|
3
|
+
|
|
4
|
+
export function belongsTo (AssociatedOrmEntity: any, options?: Record<string, any>): (OrmEntity: any) => any
|
|
5
|
+
export function hasMany (AssociatedOrmEntity: any, options?: Record<string, any>): (OrmEntity: any) => any
|
|
6
|
+
export function hasOne (AssociatedOrmEntity: any, options?: Record<string, any>): (OrmEntity: any) => any
|
package/orm/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.38",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
|
+
"./orm": "./orm/index.js",
|
|
9
10
|
"./connect": "./connect/index.js",
|
|
10
11
|
"./server": "./server.js",
|
|
11
12
|
"./connect-test": "./connect/test.js",
|
|
@@ -81,5 +82,5 @@
|
|
|
81
82
|
]
|
|
82
83
|
},
|
|
83
84
|
"license": "MIT",
|
|
84
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "fb010df766a588f54f91b1bc14dfd14a7c178d33"
|
|
85
86
|
}
|