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 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
@@ -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.init($doc)
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.init($doc)
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
@@ -0,0 +1,5 @@
1
+ import Signal from './Signal.js'
2
+ export { belongsTo, hasMany, hasOne } from './associations.js'
3
+
4
+ export const BaseModel = Signal
5
+ export default BaseModel
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.36",
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": "0f90958b39502634ec8fefc99d7e688f0ac4c4bc"
85
+ "gitHead": "fb010df766a588f54f91b1bc14dfd14a7c178d33"
85
86
  }