teamplay 0.4.0-alpha.84 → 0.4.0-alpha.86

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.
Files changed (3) hide show
  1. package/orm/Doc.js +454 -135
  2. package/orm/Query.js +500 -210
  3. package/package.json +2 -2
package/orm/Doc.js CHANGED
@@ -176,29 +176,162 @@ class Doc {
176
176
  export class DocSubscriptions {
177
177
  constructor (DocClass = Doc) {
178
178
  this.DocClass = DocClass
179
- this.subCount = new Map() // transportHash -> total ref count (owners + retained docs)
180
- this.ownerFetchCount = new Map() // ownerKey -> fetch intent count
181
- this.ownerSubscribeCount = new Map() // ownerKey -> subscribe intent count
182
- this.ownerMeta = new Map() // ownerKey -> { hash, segments, rootId }
183
- this.ownerKeysByHash = new Map() // transportHash -> Set(ownerKey)
184
- this.docs = new Map()
185
- this.pendingDestroyTimers = new Map()
186
- this.transportTasks = new Map()
179
+ this.ownerRecords = new Map() // ownerKey -> owner record
180
+ this.entries = new Map() // transportHash -> transport entry
187
181
  this.fr = new FinalizationRegistry(({ hash, ownerKey }) => this.destroyByOwnerKey(ownerKey, { hash, force: true }))
182
+ this.subCount = createReadonlyMapView({
183
+ get: hash => this.getTrackedCount(hash),
184
+ has: hash => this.getTrackedCount(hash) !== undefined,
185
+ size: () => this.getTrackedHashCountSize(),
186
+ keys: () => getTrackedHashes(this.entries)
187
+ })
188
+ this.ownerFetchCount = createReadonlyMapView({
189
+ get: ownerKey => {
190
+ const count = this.ownerRecords.get(ownerKey)?.fetchCount
191
+ return count > 0 ? count : undefined
192
+ },
193
+ has: ownerKey => !!this.ownerRecords.get(ownerKey)?.fetchCount,
194
+ size: () => countMapLike(this.ownerRecords, record => record.fetchCount > 0),
195
+ keys: () => filterMapKeys(this.ownerRecords, record => record.fetchCount > 0)
196
+ })
197
+ this.ownerSubscribeCount = createReadonlyMapView({
198
+ get: ownerKey => {
199
+ const count = this.ownerRecords.get(ownerKey)?.subscribeCount
200
+ return count > 0 ? count : undefined
201
+ },
202
+ has: ownerKey => !!this.ownerRecords.get(ownerKey)?.subscribeCount,
203
+ size: () => countMapLike(this.ownerRecords, record => record.subscribeCount > 0),
204
+ keys: () => filterMapKeys(this.ownerRecords, record => record.subscribeCount > 0)
205
+ })
206
+ this.ownerMeta = createReadonlyMapView({
207
+ get: ownerKey => this.getOwnerMeta(ownerKey),
208
+ has: ownerKey => this.ownerRecords.has(ownerKey),
209
+ size: () => this.ownerRecords.size,
210
+ keys: () => this.ownerRecords.keys()
211
+ })
212
+ this.ownerKeysByHash = createReadonlyMapView({
213
+ get: hash => this.getOwnerKeys(hash),
214
+ has: hash => !!this.getOwnerKeys(hash),
215
+ size: () => countMapLike(this.entries, entry => entry.owners.size > 0),
216
+ keys: () => filterMapKeys(this.entries, entry => entry.owners.size > 0)
217
+ })
218
+ this.docs = createReadonlyMapView({
219
+ get: hash => this.getRuntime(hash),
220
+ has: hash => this.hasRuntime(hash),
221
+ size: () => this.getRuntimeCount(),
222
+ keys: () => filterMapKeys(this.entries, entry => !!entry.runtime)
223
+ })
224
+ this.pendingDestroyTimers = createReadonlyMapView({
225
+ get: hash => this.entries.get(hash)?.pendingDestroy,
226
+ has: hash => !!this.entries.get(hash)?.pendingDestroy,
227
+ size: () => countMapLike(this.entries, entry => !!entry.pendingDestroy),
228
+ keys: () => filterMapKeys(this.entries, entry => !!entry.pendingDestroy)
229
+ })
230
+ }
231
+
232
+ getOrCreateOwnerRecord (ownerKey, meta) {
233
+ let record = this.ownerRecords.get(ownerKey)
234
+ if (!record) {
235
+ record = {
236
+ ownerKey,
237
+ rootId: meta.rootId,
238
+ hash: meta.hash,
239
+ segments: meta.segments ? [...meta.segments] : parseDocHash(meta.hash),
240
+ fetchCount: 0,
241
+ subscribeCount: 0
242
+ }
243
+ this.ownerRecords.set(ownerKey, record)
244
+ } else {
245
+ if (meta.rootId != null) record.rootId = meta.rootId
246
+ if (meta.hash != null) record.hash = meta.hash
247
+ if (meta.segments != null) record.segments = [...meta.segments]
248
+ }
249
+ return record
250
+ }
251
+
252
+ getOrCreateEntry (hash, segments) {
253
+ let entry = this.entries.get(hash)
254
+ if (!entry) {
255
+ entry = {
256
+ hash,
257
+ segments: segments ? [...segments] : parseDocHash(hash),
258
+ mode: 'idle',
259
+ targetMode: 'idle',
260
+ phase: 'stable',
261
+ runtime: null,
262
+ owners: new Set(),
263
+ retainCount: 0,
264
+ pendingDestroy: null,
265
+ reconcilePromise: null
266
+ }
267
+ this.entries.set(hash, entry)
268
+ } else if (segments && !entry.segments?.length) {
269
+ entry.segments = [...segments]
270
+ }
271
+ return entry
272
+ }
273
+
274
+ getEntry (hash) {
275
+ return this.entries.get(hash)
276
+ }
277
+
278
+ getEntryTotalCount (entry) {
279
+ if (!entry) return 0
280
+ let count = entry.retainCount
281
+ for (const ownerKey of entry.owners) {
282
+ count += this.getOwnerTotalCount(ownerKey)
283
+ }
284
+ return count
285
+ }
286
+
287
+ syncOwnerMirror () {}
288
+
289
+ clearOwnerMirror () {}
290
+
291
+ syncEntryMirror () {}
292
+
293
+ deleteEntryIfEmpty (hash) {
294
+ const entry = this.entries.get(hash)
295
+ if (!entry) return
296
+ if (entry.owners.size > 0) return
297
+ if (entry.retainCount > 0) return
298
+ if (entry.pendingDestroy) return
299
+ if (entry.runtime) return
300
+ if (entry.phase === 'transition') return
301
+ this.entries.delete(hash)
302
+ }
303
+
304
+ ensureRuntime (hash, segments) {
305
+ const entry = this.getOrCreateEntry(hash, segments)
306
+ if (!entry.runtime) {
307
+ const runtimeSegments = entry.segments?.length ? entry.segments : parseDocHash(hash)
308
+ entry.runtime = new this.DocClass(...runtimeSegments)
309
+ }
310
+ entry.runtime.init()
311
+ entry.mode = entry.runtime.activeTransportMode || entry.mode
312
+ this.syncEntryMirror(entry)
313
+ return entry.runtime
314
+ }
315
+
316
+ addOwnerToEntry (record) {
317
+ const entry = this.getOrCreateEntry(record.hash, record.segments)
318
+ entry.owners.add(record.ownerKey)
319
+ this.syncEntryMirror(entry)
320
+ return entry
321
+ }
322
+
323
+ removeOwnerFromEntry (record) {
324
+ const entry = this.entries.get(record.hash)
325
+ if (!entry) return
326
+ entry.owners.delete(record.ownerKey)
327
+ this.syncEntryMirror(entry)
188
328
  }
189
329
 
190
330
  init ($doc) {
191
331
  const segments = [...$doc[SEGMENTS]]
192
332
  const hash = hashDoc(segments)
193
- let doc = this.docs.get(hash)
194
- if (doc) {
195
- if (doc.initialized) return
196
- doc.init()
197
- } else {
198
- doc = new this.DocClass(...segments)
199
- this.docs.set(hash, doc)
200
- doc.init()
201
- }
333
+ this.getOrCreateEntry(hash, segments)
334
+ this.ensureRuntime(hash, segments)
202
335
  }
203
336
 
204
337
  subscribe ($doc, { intent = 'subscribe' } = {}) {
@@ -207,23 +340,22 @@ export class DocSubscriptions {
207
340
  const rootId = getOwningRootId($doc)
208
341
  const ownerKey = getDocOwnerKey(rootId, hash)
209
342
  const token = getDocFinalizationToken($doc)
210
- const previousCount = this.subCount.get(hash) || 0
343
+ const entry = this.getOrCreateEntry(hash, segments)
344
+ const previousCount = this.getEntryTotalCount(entry)
211
345
  this.cancelDestroy(hash)
212
- this.incrementOwnerIntent(ownerKey, intent)
213
- this.addOwnerMeta(ownerKey, hash, segments, rootId)
214
- this.subCount.set(hash, previousCount + 1)
346
+ const record = this.getOrCreateOwnerRecord(ownerKey, { hash, segments, rootId })
347
+ this.incrementOwnerIntent(record, intent)
348
+ this.addOwnerToEntry(record)
215
349
  if (rootId) {
216
350
  registerRootOwnedDirectDocSubscription(rootId, hash, segments, token)
217
351
  }
218
352
  this.fr.register($doc, { hash, ownerKey }, token)
219
-
220
- this.init($doc)
221
- const doc = this.docs.get(hash)
353
+ this.ensureRuntime(hash, segments)
354
+ const doc = entry.runtime
222
355
  if (
223
356
  previousCount > 0 &&
224
357
  doc &&
225
- !doc._subscribing &&
226
- !this.transportTasks.get(hash) &&
358
+ entry.phase === 'stable' &&
227
359
  this.getDesiredTransportMode(hash) === doc.activeTransportMode
228
360
  ) return
229
361
  return this.reconcileTransport(hash)
@@ -232,10 +364,11 @@ export class DocSubscriptions {
232
364
  retain ($doc) {
233
365
  const segments = [...$doc[SEGMENTS]]
234
366
  const hash = hashDoc(segments)
367
+ const entry = this.getOrCreateEntry(hash, segments)
235
368
  this.cancelDestroy(hash)
236
- const count = this.subCount.get(hash) || 0
237
- this.subCount.set(hash, count + 1)
238
- this.init($doc)
369
+ entry.retainCount += 1
370
+ this.ensureRuntime(hash, segments)
371
+ this.syncEntryMirror(entry)
239
372
  }
240
373
 
241
374
  async unsubscribe ($doc, { intent = 'subscribe' } = {}) {
@@ -244,23 +377,26 @@ export class DocSubscriptions {
244
377
  const rootId = getOwningRootId($doc)
245
378
  const ownerKey = getDocOwnerKey(rootId, hash)
246
379
  const token = getDocFinalizationToken($doc)
247
- const currentIntentCount = this.getOwnerIntentCount(ownerKey, intent)
380
+ const record = this.ownerRecords.get(ownerKey)
381
+ const currentIntentCount = this.getOwnerIntentCount(record, intent)
248
382
  if (currentIntentCount <= 0) {
249
383
  if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($doc)
250
384
  return
251
385
  }
252
- this.setOwnerIntentCount(ownerKey, intent, currentIntentCount - 1)
253
- const nextOwnerCount = this.getOwnerTotalCount(ownerKey)
254
- const count = Math.max((this.subCount.get(hash) || 0) - 1, 0)
255
- if (count > 0) this.subCount.set(hash, count)
256
- else this.subCount.set(hash, 0)
386
+ this.setOwnerIntentCount(record, intent, currentIntentCount - 1)
387
+ const nextOwnerCount = this.getOwnerTotalCount(record)
257
388
  if (rootId) {
258
389
  unregisterRootOwnedDirectDocSubscription(rootId, hash, token)
259
390
  }
391
+ const entry = this.getOrCreateEntry(hash, segments)
260
392
  if (nextOwnerCount === 0) {
261
393
  this.fr.unregister(token)
262
- this.removeOwnerMeta(ownerKey, hash)
394
+ if (record) {
395
+ this.removeOwnerFromEntry(record)
396
+ }
397
+ this.ownerRecords.delete(ownerKey)
263
398
  }
399
+ const count = this.getEntryTotalCount(entry)
264
400
  const destroyPromise = count === 0 ? this.scheduleDestroy(segments) : undefined
265
401
  await this.reconcileTransport(hash)
266
402
  if (count > 0) return
@@ -270,17 +406,17 @@ export class DocSubscriptions {
270
406
  async release ($doc) {
271
407
  const segments = [...$doc[SEGMENTS]]
272
408
  const hash = hashDoc(segments)
273
- let count = this.subCount.get(hash) || 0
274
- count -= 1
275
- if (count < 0) {
409
+ const entry = this.entries.get(hash)
410
+ if (!entry) {
276
411
  if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($doc)
277
412
  return
278
413
  }
279
- if (count > 0) {
280
- this.subCount.set(hash, count)
414
+ if (entry.retainCount <= 0) {
415
+ if (ERROR_ON_EXCESSIVE_UNSUBSCRIBES) throw ERRORS.notSubscribed($doc)
281
416
  return
282
417
  }
283
- this.subCount.set(hash, 0)
418
+ entry.retainCount -= 1
419
+ if ((this.getTrackedCount(hash) || 0) > 0) return
284
420
  await this.scheduleDestroy(segments)
285
421
  }
286
422
 
@@ -290,18 +426,12 @@ export class DocSubscriptions {
290
426
  }
291
427
 
292
428
  async clear () {
293
- const hashes = new Set([
294
- ...this.pendingDestroyTimers.keys(),
295
- ...this.docs.keys()
296
- ])
429
+ const hashes = new Set(this.entries.keys())
297
430
  for (const hash of hashes) {
298
431
  await this.destroyByHash(hash, { force: true })
299
432
  }
300
- this.subCount.clear()
301
- this.ownerFetchCount.clear()
302
- this.ownerSubscribeCount.clear()
303
- this.ownerMeta.clear()
304
- this.ownerKeysByHash.clear()
433
+ this.entries.clear()
434
+ this.ownerRecords.clear()
305
435
  }
306
436
 
307
437
  async releaseRootOwnedSubscriptions (rootId) {
@@ -317,7 +447,7 @@ export class DocSubscriptions {
317
447
  }
318
448
 
319
449
  async flushPendingDestroys () {
320
- const hashes = Array.from(this.pendingDestroyTimers.keys())
450
+ const hashes = Array.from(filterMapKeys(this.entries, entry => !!entry.pendingDestroy))
321
451
  for (const hash of hashes) {
322
452
  await this.destroyByHash(hash)
323
453
  }
@@ -330,18 +460,19 @@ export class DocSubscriptions {
330
460
  await this.destroyByHash(hash, options)
331
461
  return
332
462
  }
333
- const existing = this.pendingDestroyTimers.get(hash)
463
+ const entry = this.getOrCreateEntry(hash, segments)
464
+ const existing = entry.pendingDestroy
334
465
  if (existing) {
335
466
  if (options.force) existing.force = true
336
467
  return existing.promise
337
468
  }
338
- const entry = createPendingDestroyEntry()
339
- if (options.force) entry.force = true
340
- entry.timer = setTimeout(() => {
341
- this.destroyByHash(hash, { force: entry.force }).catch(ignoreDestroyError)
469
+ const pendingDestroy = createPendingDestroyEntry()
470
+ if (options.force) pendingDestroy.force = true
471
+ pendingDestroy.timer = setTimeout(() => {
472
+ this.destroyByHash(hash, { force: pendingDestroy.force }).catch(ignoreDestroyError)
342
473
  }, delay)
343
- this.pendingDestroyTimers.set(hash, entry)
344
- return entry.promise
474
+ entry.pendingDestroy = pendingDestroy
475
+ return pendingDestroy.promise
345
476
  }
346
477
 
347
478
  cancelDestroy (hash) {
@@ -351,36 +482,50 @@ export class DocSubscriptions {
351
482
  }
352
483
 
353
484
  async reconcileTransport (hash) {
354
- const previous = this.transportTasks.get(hash) || Promise.resolve()
355
- const next = previous
485
+ const entry = this.getOrCreateEntry(hash)
486
+ entry.targetMode = this.getDesiredTransportMode(hash)
487
+ if (entry.phase === 'transition' && entry.reconcilePromise) return entry.reconcilePromise
488
+ const next = Promise.resolve()
356
489
  .catch(ignoreDestroyError)
357
490
  .then(() => this.reconcileTransportNow(hash))
358
- this.transportTasks.set(hash, next)
491
+ entry.phase = 'transition'
492
+ entry.reconcilePromise = next
359
493
  try {
360
494
  await next
361
495
  } finally {
362
- if (this.transportTasks.get(hash) === next) this.transportTasks.delete(hash)
496
+ const currentEntry = this.entries.get(hash)
497
+ if (currentEntry?.reconcilePromise === next) {
498
+ currentEntry.reconcilePromise = null
499
+ currentEntry.phase = 'stable'
500
+ }
501
+ this.deleteEntryIfEmpty(hash)
363
502
  }
364
503
  }
365
504
 
366
505
  async reconcileTransportNow (hash) {
367
- const doc = this.docs.get(hash)
368
- if (!doc) return
506
+ const entry = this.getOrCreateEntry(hash)
369
507
  while (true) {
370
- const desiredMode = this.getDesiredTransportMode(hash)
371
- const currentMode = doc.activeTransportMode
508
+ let doc = entry.runtime
509
+ const desiredMode = entry.targetMode = this.getDesiredTransportMode(hash)
510
+ const currentMode = doc?.activeTransportMode ?? entry.mode
511
+ entry.mode = currentMode
372
512
  if (desiredMode === currentMode) return
373
513
  if (desiredMode === 'idle') {
374
- if (currentMode === 'idle') return
375
- await doc.unsubscribe()
514
+ if (doc && currentMode !== 'idle') {
515
+ await doc.unsubscribe()
516
+ }
517
+ entry.mode = 'idle'
376
518
  continue
377
519
  }
378
- if (currentMode !== 'idle') {
520
+ if (currentMode !== 'idle' && doc) {
379
521
  await doc.unsubscribe()
522
+ entry.mode = 'idle'
380
523
  continue
381
524
  }
382
- doc._subscribing = doc.subscribe({ mode: desiredMode }).then(() => { doc._subscribing = undefined })
383
- await doc._subscribing
525
+ doc = this.ensureRuntime(hash)
526
+ await doc.subscribe({ mode: desiredMode })
527
+ entry.runtime = doc
528
+ entry.mode = doc.activeTransportMode || desiredMode
384
529
  }
385
530
  }
386
531
 
@@ -397,33 +542,51 @@ export class DocSubscriptions {
397
542
  }
398
543
 
399
544
  try {
400
- const count = this.subCount.get(hash) || 0
545
+ const entry = this.entries.get(hash)
546
+ if (options.force && entry?.owners.size) {
547
+ this.removeAllOwnersFromEntry(hash)
548
+ }
549
+ const count = entry ? this.getEntryTotalCount(entry) : (this.getTrackedCount(hash) || 0)
401
550
  if (!options.force && count > 0) {
402
551
  settlePending()
403
552
  return
404
553
  }
405
- const doc = this.docs.get(hash)
554
+ const doc = entry?.runtime
406
555
  if (!doc) {
407
- this.subCount.delete(hash)
556
+ if (entry) {
557
+ entry.mode = 'idle'
558
+ entry.runtime = null
559
+ this.deleteEntryIfEmpty(hash)
560
+ }
408
561
  settlePending()
409
562
  return
410
563
  }
411
564
  await this.reconcileTransport(hash)
412
- if (!options.force && (this.subCount.get(hash) || 0) > 0) {
565
+ const nextEntry = this.entries.get(hash)
566
+ const nextCount = nextEntry ? this.getEntryTotalCount(nextEntry) : (this.getTrackedCount(hash) || 0)
567
+ if (!options.force && nextCount > 0) {
413
568
  settlePending()
414
569
  return
415
570
  }
416
- if (doc.activeTransportMode !== 'idle') {
417
- await doc.unsubscribe()
571
+ const activeDoc = nextEntry?.runtime || doc
572
+ if (activeDoc.activeTransportMode !== 'idle') {
573
+ await activeDoc.unsubscribe()
418
574
  }
419
- if (!options.force && (this.subCount.get(hash) || 0) > 0) {
575
+ const finalEntryBeforeDestroy = this.entries.get(hash)
576
+ const finalCountBeforeDestroy = finalEntryBeforeDestroy
577
+ ? this.getEntryTotalCount(finalEntryBeforeDestroy)
578
+ : (this.getTrackedCount(hash) || 0)
579
+ if (!options.force && finalCountBeforeDestroy > 0) {
420
580
  settlePending()
421
581
  return
422
582
  }
423
- if (typeof doc.hasPending === 'function' && doc.hasPending()) {
424
- if (typeof doc.whenNothingPending === 'function') {
425
- if (pendingDestroy) this.pendingDestroyTimers.set(hash, pendingDestroy)
426
- doc.whenNothingPending(() => {
583
+ if (typeof activeDoc.hasPending === 'function' && activeDoc.hasPending()) {
584
+ if (typeof activeDoc.whenNothingPending === 'function') {
585
+ if (pendingDestroy) {
586
+ const nextEntry = this.getOrCreateEntry(hash)
587
+ nextEntry.pendingDestroy = pendingDestroy
588
+ }
589
+ activeDoc.whenNothingPending(() => {
427
590
  const nextOptions = pendingDestroy ? { ...options, _pendingDestroy: pendingDestroy } : options
428
591
  this.destroyByHash(hash, nextOptions).catch(ignoreDestroyError)
429
592
  })
@@ -432,11 +595,14 @@ export class DocSubscriptions {
432
595
  }
433
596
  return
434
597
  }
435
- if (typeof doc.destroy === 'function') await doc.destroy()
436
- if (typeof doc.dispose === 'function') doc.dispose()
437
- this.docs.delete(hash)
438
- this.subCount.delete(hash)
439
- this.ownerKeysByHash.delete(hash)
598
+ if (typeof activeDoc.destroy === 'function') await activeDoc.destroy()
599
+ if (typeof activeDoc.dispose === 'function') activeDoc.dispose()
600
+ const finalEntry = this.entries.get(hash)
601
+ if (finalEntry) {
602
+ finalEntry.runtime = null
603
+ finalEntry.mode = 'idle'
604
+ this.deleteEntryIfEmpty(hash)
605
+ }
440
606
  settlePending()
441
607
  } catch (err) {
442
608
  settlePending(err)
@@ -445,65 +611,72 @@ export class DocSubscriptions {
445
611
  }
446
612
 
447
613
  takePendingDestroy (hash, expectedEntry) {
448
- const entry = this.pendingDestroyTimers.get(hash)
449
- if (!entry) return
450
- if (expectedEntry && entry !== expectedEntry) return
451
- clearTimeout(entry.timer)
452
- this.pendingDestroyTimers.delete(hash)
453
- return entry
614
+ const transportEntry = this.entries.get(hash)
615
+ const pendingDestroy = transportEntry?.pendingDestroy
616
+ if (!pendingDestroy) return
617
+ if (expectedEntry && pendingDestroy !== expectedEntry) return
618
+ clearTimeout(pendingDestroy.timer)
619
+ transportEntry.pendingDestroy = null
620
+ this.deleteEntryIfEmpty(hash)
621
+ return pendingDestroy
454
622
  }
455
623
 
456
- getOwnerIntentCount (ownerKey, intent) {
457
- const store = intent === 'fetch' ? this.ownerFetchCount : this.ownerSubscribeCount
458
- return store.get(ownerKey) || 0
624
+ getOwnerIntentCount (recordOrOwnerKey, intent) {
625
+ const record = typeof recordOrOwnerKey === 'string'
626
+ ? this.ownerRecords.get(recordOrOwnerKey)
627
+ : recordOrOwnerKey
628
+ if (!record) return 0
629
+ return intent === 'fetch' ? record.fetchCount : record.subscribeCount
459
630
  }
460
631
 
461
- setOwnerIntentCount (ownerKey, intent, count) {
462
- const store = intent === 'fetch' ? this.ownerFetchCount : this.ownerSubscribeCount
463
- if (count > 0) store.set(ownerKey, count)
464
- else store.delete(ownerKey)
632
+ setOwnerIntentCount (record, intent, count) {
633
+ if (!record) return
634
+ if (intent === 'fetch') record.fetchCount = Math.max(count, 0)
635
+ else record.subscribeCount = Math.max(count, 0)
636
+ this.syncOwnerMirror(record)
465
637
  }
466
638
 
467
- incrementOwnerIntent (ownerKey, intent) {
468
- this.setOwnerIntentCount(ownerKey, intent, this.getOwnerIntentCount(ownerKey, intent) + 1)
639
+ incrementOwnerIntent (record, intent) {
640
+ this.setOwnerIntentCount(record, intent, this.getOwnerIntentCount(record, intent) + 1)
469
641
  }
470
642
 
471
- getOwnerTotalCount (ownerKey) {
472
- return (this.ownerFetchCount.get(ownerKey) || 0) + (this.ownerSubscribeCount.get(ownerKey) || 0)
643
+ getOwnerTotalCount (recordOrOwnerKey) {
644
+ const record = typeof recordOrOwnerKey === 'string'
645
+ ? this.ownerRecords.get(recordOrOwnerKey)
646
+ : recordOrOwnerKey
647
+ if (!record) return 0
648
+ return record.fetchCount + record.subscribeCount
473
649
  }
474
650
 
475
651
  addOwnerMeta (ownerKey, hash, segments, rootId) {
476
- if (this.ownerMeta.has(ownerKey)) return
477
- this.ownerMeta.set(ownerKey, { hash, segments: [...segments], rootId })
478
- let ownerKeys = this.ownerKeysByHash.get(hash)
479
- if (!ownerKeys) {
480
- ownerKeys = new Set()
481
- this.ownerKeysByHash.set(hash, ownerKeys)
482
- }
483
- ownerKeys.add(ownerKey)
652
+ const record = this.getOrCreateOwnerRecord(ownerKey, { hash, segments, rootId })
653
+ this.addOwnerToEntry(record)
484
654
  }
485
655
 
486
656
  removeOwnerMeta (ownerKey, hash) {
487
- const meta = this.ownerMeta.get(ownerKey)
488
- const knownHash = hash ?? meta?.hash
489
- this.ownerMeta.delete(ownerKey)
490
- this.ownerFetchCount.delete(ownerKey)
491
- this.ownerSubscribeCount.delete(ownerKey)
657
+ const record = this.ownerRecords.get(ownerKey)
658
+ const knownHash = hash ?? record?.hash
659
+ if (record) {
660
+ this.removeOwnerFromEntry(record)
661
+ this.ownerRecords.delete(ownerKey)
662
+ }
492
663
  if (!knownHash) return
493
- const ownerKeys = this.ownerKeysByHash.get(knownHash)
664
+ const ownerKeys = this.entries.get(knownHash)?.owners
494
665
  if (!ownerKeys) return
495
666
  ownerKeys.delete(ownerKey)
496
- if (ownerKeys.size === 0) this.ownerKeysByHash.delete(knownHash)
667
+ this.deleteEntryIfEmpty(knownHash)
497
668
  }
498
669
 
499
670
  getDesiredTransportMode (hash) {
500
- const ownerKeys = this.ownerKeysByHash.get(hash)
671
+ const entry = this.entries.get(hash)
672
+ const ownerKeys = entry?.owners
501
673
  if (!ownerKeys || ownerKeys.size === 0) return 'idle'
502
674
  let hasFetchBackedOwner = false
503
675
  for (const ownerKey of ownerKeys) {
504
- const subscribeCount = this.ownerSubscribeCount.get(ownerKey) || 0
505
- const fetchCount = this.ownerFetchCount.get(ownerKey) || 0
506
- const rootId = this.ownerMeta.get(ownerKey)?.rootId
676
+ const record = this.ownerRecords.get(ownerKey)
677
+ const subscribeCount = record?.subscribeCount || 0
678
+ const fetchCount = record?.fetchCount || 0
679
+ const rootId = record?.rootId
507
680
  const subscribeMode = getRootTransportMode(rootId, 'subscribe')
508
681
  if (subscribeCount > 0 && subscribeMode === 'subscribe') return 'subscribe'
509
682
  if (fetchCount > 0 || (subscribeCount > 0 && subscribeMode === 'fetch')) {
@@ -513,21 +686,123 @@ export class DocSubscriptions {
513
686
  return hasFetchBackedOwner ? 'fetch' : 'idle'
514
687
  }
515
688
 
689
+ removeAllOwnersFromEntry (hash) {
690
+ const entry = this.entries.get(hash)
691
+ if (!entry) return
692
+ for (const ownerKey of Array.from(entry.owners)) {
693
+ const record = this.ownerRecords.get(ownerKey)
694
+ if (record) this.removeOwnerFromEntry(record)
695
+ else entry.owners.delete(ownerKey)
696
+ this.ownerRecords.delete(ownerKey)
697
+ }
698
+ }
699
+
700
+ async destroyTransportEntry (hash, runtime) {
701
+ const activeDoc = this.entries.get(hash)?.runtime || runtime
702
+ if (!activeDoc) {
703
+ const entry = this.entries.get(hash)
704
+ if (entry) {
705
+ entry.runtime = null
706
+ entry.mode = 'idle'
707
+ }
708
+ this.deleteEntryIfEmpty(hash)
709
+ return
710
+ }
711
+ if (activeDoc.activeTransportMode !== 'idle') {
712
+ await activeDoc.unsubscribe()
713
+ }
714
+ if (typeof activeDoc.hasPending === 'function' && activeDoc.hasPending()) {
715
+ if (typeof activeDoc.whenNothingPending === 'function') {
716
+ await new Promise(resolve => activeDoc.whenNothingPending(resolve))
717
+ }
718
+ }
719
+ if (typeof activeDoc.destroy === 'function') await activeDoc.destroy()
720
+ if (typeof activeDoc.dispose === 'function') activeDoc.dispose()
721
+ const finalEntry = this.entries.get(hash)
722
+ if (finalEntry && finalEntry.owners.size > 0) return
723
+ if (finalEntry) {
724
+ finalEntry.runtime = null
725
+ finalEntry.mode = 'idle'
726
+ }
727
+ this.deleteEntryIfEmpty(hash)
728
+ }
729
+
516
730
  async destroyByOwnerKey (ownerKey, options = {}) {
517
- const meta = this.ownerMeta.get(ownerKey)
518
- if (!meta) return
519
- const { hash, segments } = meta
520
- const ownerCount = this.getOwnerTotalCount(ownerKey)
731
+ const record = this.ownerRecords.get(ownerKey)
732
+ const hash = record?.hash ?? options.hash
733
+ if (!hash) return
734
+ const segments = record?.segments ?? parseDocHash(hash)
735
+ const ownerCount = this.getOwnerTotalCount(record || ownerKey)
521
736
  if (!options.force && ownerCount > 0) return
522
737
 
523
- const currentCount = this.subCount.get(hash) || 0
524
- const nextCount = Math.max(currentCount - ownerCount, 0)
525
- if (nextCount > 0) this.subCount.set(hash, nextCount)
526
- else this.subCount.set(hash, 0)
527
- this.removeOwnerMeta(ownerKey, hash)
738
+ const entry = this.entries.get(hash)
739
+ if (record) {
740
+ this.removeOwnerFromEntry(record)
741
+ this.ownerRecords.delete(ownerKey)
742
+ } else if (entry?.owners.has(ownerKey)) {
743
+ entry.owners.delete(ownerKey)
744
+ }
745
+
746
+ if (!entry && !this.getRuntime(hash)) {
747
+ return
748
+ }
749
+
528
750
  await this.reconcileTransport(hash)
529
- if (nextCount > 0) return
530
- await this.scheduleDestroy(segments, { force: !!options.force })
751
+ const nextEntry = this.entries.get(hash)
752
+ const nextCount = nextEntry ? this.getEntryTotalCount(nextEntry) : (this.getTrackedCount(hash) || 0)
753
+ if (nextCount > 0) {
754
+ this.deleteEntryIfEmpty(hash)
755
+ return
756
+ }
757
+ if (options.force) {
758
+ await this.destroyTransportEntry(hash, nextEntry?.runtime || entry?.runtime)
759
+ return
760
+ }
761
+ await this.scheduleDestroy(segments, { force: false })
762
+ }
763
+
764
+ getRuntime (hash) {
765
+ return this.entries.get(hash)?.runtime
766
+ }
767
+
768
+ hasRuntime (hash) {
769
+ return !!this.getRuntime(hash)
770
+ }
771
+
772
+ getRuntimeCount () {
773
+ return countMapLike(this.entries, entry => !!entry.runtime)
774
+ }
775
+
776
+ getTrackedCount (hash) {
777
+ const entry = this.entries.get(hash)
778
+ if (entry) {
779
+ const total = this.getEntryTotalCount(entry)
780
+ if (total > 0 || entry.pendingDestroy) return total
781
+ }
782
+ return undefined
783
+ }
784
+
785
+ getTrackedHashCountSize () {
786
+ return countMapLike(this.entries, entry => {
787
+ const total = this.getEntryTotalCount(entry)
788
+ return total > 0 || !!entry.pendingDestroy
789
+ })
790
+ }
791
+
792
+ getOwnerMeta (ownerKey) {
793
+ const record = this.ownerRecords.get(ownerKey)
794
+ if (!record) return undefined
795
+ return {
796
+ hash: record.hash,
797
+ segments: [...record.segments],
798
+ rootId: record.rootId
799
+ }
800
+ }
801
+
802
+ getOwnerKeys (hash) {
803
+ const owners = this.entries.get(hash)?.owners
804
+ if (!owners?.size) return undefined
805
+ return new Set(owners)
531
806
  }
532
807
  }
533
808
 
@@ -537,6 +812,10 @@ function hashDoc (segments) {
537
812
  return JSON.stringify(segments)
538
813
  }
539
814
 
815
+ function parseDocHash (hash) {
816
+ return JSON.parse(hash)
817
+ }
818
+
540
819
  function getDocOwnerKey (rootId, hash) {
541
820
  return JSON.stringify({ owner: [rootId, hash] })
542
821
  }
@@ -611,3 +890,43 @@ function has (obj, key) {
611
890
  const ERRORS = {
612
891
  notSubscribed: $doc => Error('trying to unsubscribe when not subscribed. Doc: ' + $doc.path())
613
892
  }
893
+
894
+ function createReadonlyMapView ({ get, has, size, keys }) {
895
+ return {
896
+ get,
897
+ has,
898
+ get size () {
899
+ return size()
900
+ },
901
+ * keys () {
902
+ yield * keys()
903
+ },
904
+ * values () {
905
+ for (const key of keys()) yield get(key)
906
+ },
907
+ * entries () {
908
+ for (const key of keys()) yield [key, get(key)]
909
+ },
910
+ [Symbol.iterator] () {
911
+ return this.entries()
912
+ }
913
+ }
914
+ }
915
+
916
+ function countMapLike (iterableMap, predicate) {
917
+ let count = 0
918
+ for (const value of iterableMap.values()) {
919
+ if (predicate(value)) count++
920
+ }
921
+ return count
922
+ }
923
+
924
+ function * filterMapKeys (iterableMap, predicate) {
925
+ for (const [key, value] of iterableMap.entries()) {
926
+ if (predicate(value)) yield key
927
+ }
928
+ }
929
+
930
+ function * getTrackedHashes (entries) {
931
+ yield * entries.keys()
932
+ }