dsh-all-usage 1.1.8 → 1.1.10
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/CHANGELOG.md +28 -0
- package/README.md +13 -39
- package/lib/aggregation.js +50 -1
- package/lib/client.js +1 -1
- package/lib/ledger.js +20 -1
- package/lib/plugin.js +22 -2
- package/lib/session-sync.js +105 -62
- package/package.json +1 -1
package/lib/ledger.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { calculateCost, normalizeCostSnapshot } from './pricing.js'
|
|
2
2
|
import { billingInstantOf, contextTimeKey, extractUsageEvent, normalizeUsageValues as usageValues, pickPricingTime, touchContextTimes, upsertUsageSample as upsertUsageSampleState, usageStepKey } from './usage-core.js'
|
|
3
3
|
|
|
4
|
+
// Usage recorded for workspaces that left the registry is summed into one
|
|
5
|
+
// bucket. The ledger's promise is that flushed usage survives deletion, and a
|
|
6
|
+
// row per dead workspace would grow the dashboard without adding information.
|
|
7
|
+
const RETIRED_WORKSPACE_ID = 'retired:deleted'
|
|
8
|
+
|
|
9
|
+
// Re-attribute a record (and its turns/usage items) to another workspace id.
|
|
10
|
+
// Only the in-memory copy is retargeted: the persisted row keeps its original
|
|
11
|
+
// workspace id and cwd, so the mapping stays reversible.
|
|
12
|
+
function retargetLedgerRecord(record, workspaceId = RETIRED_WORKSPACE_ID) {
|
|
13
|
+
if (record === null || record === undefined) return record
|
|
14
|
+
return {
|
|
15
|
+
...record,
|
|
16
|
+
workspaceId,
|
|
17
|
+
turns: Array.isArray(record.turns) ? record.turns.map((turn) => ({ ...turn, workspaceId })) : record.turns,
|
|
18
|
+
usage: Array.isArray(record.usage) ? record.usage.map((item) => ({ ...item, workspaceId })) : record.usage,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
const LEDGER_VERSION = 3
|
|
5
23
|
const PREVIOUS_LEDGER_VERSION = 2
|
|
6
24
|
const LEGACY_LEDGER_VERSION = 1
|
|
@@ -495,6 +513,7 @@ export function createLedger(host) {
|
|
|
495
513
|
nextLedgerRevision,
|
|
496
514
|
ledgerEventKey,
|
|
497
515
|
sessionEvents,
|
|
516
|
+
retargetLedgerRecord,
|
|
498
517
|
buildLedgerRecord,
|
|
499
518
|
normalizeLedgerRecord,
|
|
500
519
|
prepareLedgerRecord,
|
|
@@ -509,4 +528,4 @@ export function createLedger(host) {
|
|
|
509
528
|
}
|
|
510
529
|
}
|
|
511
530
|
|
|
512
|
-
export { LEDGER_VERSION, PREVIOUS_LEDGER_VERSION, LEGACY_LEDGER_VERSION, LEDGER_SHARD_COUNT, ledgerShardIndex }
|
|
531
|
+
export { LEDGER_VERSION, PREVIOUS_LEDGER_VERSION, LEGACY_LEDGER_VERSION, LEDGER_SHARD_COUNT, RETIRED_WORKSPACE_ID, ledgerShardIndex, retargetLedgerRecord }
|
package/lib/plugin.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto'
|
|
|
2
2
|
import { createAggregation } from './aggregation.js'
|
|
3
3
|
import { createBalance } from './balance.js'
|
|
4
4
|
import { registerRoutes } from './http.js'
|
|
5
|
-
import { createLedger } from './ledger.js'
|
|
5
|
+
import { RETIRED_WORKSPACE_ID, createLedger } from './ledger.js'
|
|
6
6
|
import { createPricingRuntime } from './pricing-runtime.js'
|
|
7
7
|
import { createSessionSync } from './session-sync.js'
|
|
8
8
|
import { createCostAccumulator, createEmptyPricingState } from './pricing.js'
|
|
@@ -20,6 +20,10 @@ function apply(ctx) {
|
|
|
20
20
|
}
|
|
21
21
|
const state = {
|
|
22
22
|
wsMeta: new Map(),
|
|
23
|
+
// True once recorded usage from a vanished workspace was moved into the
|
|
24
|
+
// shared "deleted" bucket; the bucket row is only advertised when it holds
|
|
25
|
+
// something.
|
|
26
|
+
retiredBucketUsed: false,
|
|
23
27
|
pathIndex: new Map(),
|
|
24
28
|
memberOf: new Map(),
|
|
25
29
|
byDay: new Map(),
|
|
@@ -66,6 +70,14 @@ function apply(ctx) {
|
|
|
66
70
|
sessionsSkippedByRevision: 0,
|
|
67
71
|
sessionsRestoredFromLedger: 0,
|
|
68
72
|
sessionsFailed: 0,
|
|
73
|
+
// Cumulative registry-probe diagnostics. They deliberately survive a
|
|
74
|
+
// baseline reset so the status endpoint can show whether the
|
|
75
|
+
// `domain/changed` fast path fires at all on this host.
|
|
76
|
+
workspaceProbeEvents: 0,
|
|
77
|
+
workspaceSyncRuns: 0,
|
|
78
|
+
workspaceSyncAdded: 0,
|
|
79
|
+
workspaceSyncRemoved: 0,
|
|
80
|
+
workspaceSyncDeferred: 0,
|
|
69
81
|
},
|
|
70
82
|
ledgerRecords: new Map(),
|
|
71
83
|
queryCache: new Map(),
|
|
@@ -101,6 +113,8 @@ function apply(ctx) {
|
|
|
101
113
|
reconcileInFlight: false,
|
|
102
114
|
reconcileTimer: null,
|
|
103
115
|
workspaceSyncTimer: null,
|
|
116
|
+
workspaceSyncDeferred: false,
|
|
117
|
+
workspaceProbeTimer: null,
|
|
104
118
|
workspaceSyncInFlight: false,
|
|
105
119
|
workspaceSyncPending: false,
|
|
106
120
|
}
|
|
@@ -168,6 +182,11 @@ function apply(ctx) {
|
|
|
168
182
|
sessionsSkippedByRevision: state.sync.sessionsSkippedByRevision,
|
|
169
183
|
sessionsRestoredFromLedger: state.sync.sessionsRestoredFromLedger,
|
|
170
184
|
sessionsFailed: state.sync.sessionsFailed,
|
|
185
|
+
workspaceProbeEvents: state.sync.workspaceProbeEvents,
|
|
186
|
+
workspaceSyncRuns: state.sync.workspaceSyncRuns,
|
|
187
|
+
workspaceSyncAdded: state.sync.workspaceSyncAdded,
|
|
188
|
+
workspaceSyncRemoved: state.sync.workspaceSyncRemoved,
|
|
189
|
+
workspaceSyncDeferred: state.sync.workspaceSyncDeferred,
|
|
171
190
|
}
|
|
172
191
|
}
|
|
173
192
|
Object.assign(host, { commitPendingStats, markStatsChanged, resetSyncState, beginSync, noteSyncError, syncSnapshot })
|
|
@@ -223,7 +242,7 @@ function apply(ctx) {
|
|
|
223
242
|
if (typeof wsId !== 'string' || wsId.length === 0 || wsId.length > 256) return { ok: false, message: 'invalid-workspace', aliases: Object.assign({}, state.aliases) }
|
|
224
243
|
if (typeof raw !== 'string') return { ok: false, message: 'invalid-alias', aliases: Object.assign({}, state.aliases) }
|
|
225
244
|
const alias = raw.trim().slice(0, 80)
|
|
226
|
-
if (!state.wsMeta.has(wsId)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, state.aliases) }
|
|
245
|
+
if (!state.wsMeta.has(wsId) && !(state.retiredBucketUsed && wsId === RETIRED_WORKSPACE_ID)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, state.aliases) }
|
|
227
246
|
const previous = typeof state.aliases[wsId] === 'string' ? state.aliases[wsId] : ''
|
|
228
247
|
if (alias === previous) return { ok: true, aliases: Object.assign({}, state.aliases) }
|
|
229
248
|
if (alias === '') delete state.aliases[wsId]
|
|
@@ -273,6 +292,7 @@ function apply(ctx) {
|
|
|
273
292
|
state.pricingReady = host.pricing.loadPricing().then(() => { host.pricing.schedulePricingSync() })
|
|
274
293
|
void host.sessionSync.runBaseline()
|
|
275
294
|
host.sessionSync.scheduleReconcileTimer()
|
|
295
|
+
host.sessionSync.scheduleWorkspaceProbeTimer()
|
|
276
296
|
state.aliasesReady = loadAliases()
|
|
277
297
|
}
|
|
278
298
|
|
package/lib/session-sync.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { RETIRED_WORKSPACE_ID, retargetLedgerRecord } from './ledger.js'
|
|
1
2
|
import { contextTimeKey, extractUsageEvent, pickPricingTime, touchContextTimes, workspacePathKey } from './usage-core.js'
|
|
2
3
|
|
|
3
4
|
const RECONCILE_INTERVAL_MS = 120000
|
|
4
5
|
const RECONCILE_HINT_DELAY_MS = 3000
|
|
5
6
|
const WORKSPACE_SYNC_DEBOUNCE_MS = 300
|
|
7
|
+
// The registry event is a fast path, not a guarantee: a change that lands while a
|
|
8
|
+
// baseline runs used to be dropped outright, and a missed event left the plugin
|
|
9
|
+
// stale until the next restart. The (in-memory) registry list is polled as well.
|
|
10
|
+
const WORKSPACE_PROBE_INTERVAL_MS = 30000
|
|
6
11
|
|
|
7
12
|
export function createSessionSync(host) {
|
|
8
13
|
const { ctx, state } = host
|
|
@@ -153,6 +158,25 @@ export function createSessionSync(host) {
|
|
|
153
158
|
if (typeof cwd !== 'string' || cwd === '') return undefined
|
|
154
159
|
return state.pathIndex.get(workspacePathKey(cwd))
|
|
155
160
|
}
|
|
161
|
+
// DSH 0.1.5 replaced \`sessionPersistence.listSnapshots()\` with \`list()\`; both
|
|
162
|
+
// return per-session \`{ header, revision }\` rows. Without a revision the
|
|
163
|
+
// ledger-reuse fast path never fires, so every baseline re-reads every log.
|
|
164
|
+
async function loadSessionRevisions() {
|
|
165
|
+
const source = sessionPersistence
|
|
166
|
+
if (source === undefined || source === null) return null
|
|
167
|
+
const read = typeof source.list === 'function'
|
|
168
|
+
? () => source.list()
|
|
169
|
+
: (typeof source.listSnapshots === 'function' ? () => source.listSnapshots() : null)
|
|
170
|
+
if (read === null) return null
|
|
171
|
+
const rows = await read()
|
|
172
|
+
if (!Array.isArray(rows)) return null
|
|
173
|
+
const revisions = new Map()
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
176
|
+
if (rid !== undefined && row && typeof row.revision === 'string') revisions.set(rid, row.revision)
|
|
177
|
+
}
|
|
178
|
+
return revisions
|
|
179
|
+
}
|
|
156
180
|
function wsForLiveSession(session, sid) {
|
|
157
181
|
let wsId = state.memberOf.get(sid)
|
|
158
182
|
if (wsId !== undefined) return wsId
|
|
@@ -405,24 +429,18 @@ export function createSessionSync(host) {
|
|
|
405
429
|
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
406
430
|
// v1.0.8: cheap per-session change signal (header line + stat, no full-log read)
|
|
407
431
|
let snapshots = null
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
snapshots = new Map()
|
|
415
|
-
for (const row of rows) {
|
|
416
|
-
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
417
|
-
if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
} catch (err) {
|
|
421
|
-
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
422
|
-
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
423
|
-
state.sync.persistenceSnapshotsAvailable = false
|
|
424
|
-
markStatsChanged('scan')
|
|
432
|
+
try {
|
|
433
|
+
const revisions = await loadSessionRevisions()
|
|
434
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
435
|
+
if (revisions !== null) {
|
|
436
|
+
state.sync.persistenceSnapshotsAvailable = true
|
|
437
|
+
snapshots = revisions
|
|
425
438
|
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
441
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
442
|
+
state.sync.persistenceSnapshotsAvailable = false
|
|
443
|
+
markStatsChanged('scan')
|
|
426
444
|
}
|
|
427
445
|
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
428
446
|
if (setupFailed || !Array.isArray(records)) {
|
|
@@ -445,15 +463,19 @@ export function createSessionSync(host) {
|
|
|
445
463
|
}
|
|
446
464
|
for (const [sid, record] of state.ledgerRecords) {
|
|
447
465
|
if (!listedSessionIds.has(sid)) {
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
// synthetic rows predate sourceCwd persistence and cannot be validated,
|
|
452
|
-
// so they are fail-closed rather than resurrected after a restart.
|
|
453
|
-
if (record.sourceCwd !== undefined && workspaceForCwd(record.sourceCwd) === undefined) continue
|
|
466
|
+
// Recorded usage survives deletion. A session whose workspace, cwd or
|
|
467
|
+
// log is gone still counts from its ledger row; only legacy synthetic
|
|
468
|
+
// rows, which never belonged to a registered workspace, stay excluded.
|
|
454
469
|
if (typeof record.workspaceId === 'string' && record.workspaceId.startsWith('unregistered:')) continue
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
// Usage from a workspace that is no longer registered is summed into the
|
|
471
|
+
// shared deleted bucket, so history survives without a row per dead
|
|
472
|
+
// workspace. The stored row keeps its original workspace id and cwd.
|
|
473
|
+
const retired = typeof record.workspaceId !== 'string' || !state.wsMeta.has(record.workspaceId)
|
|
474
|
+
const carriesUsage = record.turns.length > 0 || record.usage.length > 0
|
|
475
|
+
// An empty row must not advertise a bucket that holds nothing.
|
|
476
|
+
if (retired && carriesUsage) state.retiredBucketUsed = true
|
|
477
|
+
applyLedgerRecord(retired ? retargetLedgerRecord(record) : record, retired ? 'ledger-retired' : 'ledger-recovery')
|
|
478
|
+
if (carriesUsage) state.sync.sessionsRestoredFromLedger += 1
|
|
457
479
|
}
|
|
458
480
|
}
|
|
459
481
|
if (state.sync.sessionsRestoredFromLedger > 0) markStatsChanged('scan')
|
|
@@ -499,6 +521,10 @@ export function createSessionSync(host) {
|
|
|
499
521
|
state.knownSessionIds.clear()
|
|
500
522
|
for (const sid of listedSessionIds) state.knownSessionIds.add(sid)
|
|
501
523
|
state.scan.done = true
|
|
524
|
+
if (state.workspaceSyncDeferred) {
|
|
525
|
+
state.workspaceSyncDeferred = false
|
|
526
|
+
scheduleWorkspaceSync()
|
|
527
|
+
}
|
|
502
528
|
state.sync.lastCompletedAt = Date.now()
|
|
503
529
|
if (state.sync.sessionsFailed === 0) {
|
|
504
530
|
state.sync.lastErrorAt = 0
|
|
@@ -524,6 +550,7 @@ export function createSessionSync(host) {
|
|
|
524
550
|
return
|
|
525
551
|
}
|
|
526
552
|
state.workspaceSyncInFlight = true
|
|
553
|
+
state.sync.workspaceSyncRuns += 1
|
|
527
554
|
try {
|
|
528
555
|
await synchronizeWorkspaceRegistry()
|
|
529
556
|
} catch (err) {
|
|
@@ -538,9 +565,15 @@ export function createSessionSync(host) {
|
|
|
538
565
|
}
|
|
539
566
|
}
|
|
540
567
|
async function synchronizeWorkspaceRegistry() {
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
|
|
568
|
+
if (state.disposed) return
|
|
569
|
+
// A registry change that lands while a baseline is running used to be
|
|
570
|
+
// dropped outright, which is why a new workspace could stay invisible until
|
|
571
|
+
// the next restart. Remember it and re-run once the scan settles.
|
|
572
|
+
if (!state.scan.done) {
|
|
573
|
+
state.workspaceSyncDeferred = true
|
|
574
|
+
state.sync.workspaceSyncDeferred += 1
|
|
575
|
+
return
|
|
576
|
+
}
|
|
544
577
|
let workspaces
|
|
545
578
|
try {
|
|
546
579
|
workspaces = ctx.workspaceRegistry.list()
|
|
@@ -554,36 +587,40 @@ export function createSessionSync(host) {
|
|
|
554
587
|
for (const id of state.wsMeta.keys()) if (!next.wsMeta.has(id)) removedWs.add(id)
|
|
555
588
|
const addedWs = new Set()
|
|
556
589
|
for (const id of next.wsMeta.keys()) if (!state.wsMeta.has(id)) addedWs.add(id)
|
|
590
|
+
state.sync.workspaceSyncAdded += addedWs.size
|
|
591
|
+
state.sync.workspaceSyncRemoved += removedWs.size
|
|
557
592
|
// Reuse path: no membership change — metadata only, zero rescan.
|
|
558
593
|
if (removedWs.size === 0 && addedWs.size === 0) {
|
|
559
594
|
installWorkspaceSnapshot(next)
|
|
560
595
|
return
|
|
561
596
|
}
|
|
562
|
-
//
|
|
597
|
+
// Retire path: a deregistered workspace keeps everything it recorded. The
|
|
598
|
+
// ledger's promise is that flushed usage survives deletion, so sessions are
|
|
599
|
+
// re-applied under the shared deleted bucket instead of being subtracted and
|
|
600
|
+
// dropped. New sessions still cannot enter the workspace: pathIndex only
|
|
601
|
+
// ever carries registered workspaces, so workspaceForCwd() no longer
|
|
602
|
+
// resolves its cwd and the scan skips it.
|
|
563
603
|
if (removedWs.size > 0) {
|
|
564
|
-
const
|
|
604
|
+
const retiredSids = new Set()
|
|
565
605
|
for (const [sid, record] of state.ledgerRecords) {
|
|
566
|
-
if (record
|
|
606
|
+
if (record === null || record === undefined || !removedWs.has(record.workspaceId)) continue
|
|
607
|
+
retiredSids.add(sid)
|
|
567
608
|
}
|
|
568
|
-
for (const [sid, id] of state.memberOf) if (removedWs.has(id))
|
|
569
|
-
|
|
609
|
+
for (const [sid, id] of state.memberOf) if (removedWs.has(id)) retiredSids.add(sid)
|
|
610
|
+
let moved = 0
|
|
611
|
+
for (const sid of retiredSids) {
|
|
570
612
|
if (typeof sid !== 'string' || sid === '') continue
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
state.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
state.
|
|
580
|
-
cancelLiveResync(sid)
|
|
581
|
-
}
|
|
582
|
-
for (const id of removedWs) {
|
|
583
|
-
state.perWorkspace.delete(id)
|
|
584
|
-
for (const day of state.byDay.values()) day.byWs.delete(id)
|
|
585
|
-
for (const day of state.byDayUtc.values()) day.byWs.delete(id)
|
|
613
|
+
// Re-key the live aggregate first: usage folded from the feed but not yet
|
|
614
|
+
// persisted has no ledger row to replay, and a session the registry no
|
|
615
|
+
// longer lists is never re-scanned afterwards.
|
|
616
|
+
moved += host.aggregation.retargetSession(sid, RETIRED_WORKSPACE_ID)
|
|
617
|
+
const record = state.ledgerRecords.get(sid)
|
|
618
|
+
// Keep the in-memory record aligned with the aggregate; the persisted row
|
|
619
|
+
// keeps its original workspace id and cwd, so recovery re-derives this.
|
|
620
|
+
if (record !== undefined && (record.turns.length > 0 || record.usage.length > 0)) moved += 1
|
|
621
|
+
if (record !== undefined) state.ledgerRecords.set(sid, retargetLedgerRecord(record))
|
|
586
622
|
}
|
|
623
|
+
if (moved > 0) state.retiredBucketUsed = true
|
|
587
624
|
}
|
|
588
625
|
installWorkspaceSnapshot(next)
|
|
589
626
|
// Add path: scan only the sessions that belong to newly registered workspaces.
|
|
@@ -599,20 +636,10 @@ export function createSessionSync(host) {
|
|
|
599
636
|
state.sync.sessionsTotal = records.length
|
|
600
637
|
markStatsChanged('scan')
|
|
601
638
|
let snapshots = null
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
snapshots = new Map()
|
|
607
|
-
for (const row of rows) {
|
|
608
|
-
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
609
|
-
if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
} catch (err) {
|
|
613
|
-
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
614
|
-
markStatsChanged('scan')
|
|
615
|
-
}
|
|
639
|
+
try {
|
|
640
|
+
snapshots = await loadSessionRevisions()
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
616
643
|
}
|
|
617
644
|
for (const record of records) {
|
|
618
645
|
if (state.disposed) return
|
|
@@ -691,6 +718,20 @@ export function createSessionSync(host) {
|
|
|
691
718
|
}, RECONCILE_INTERVAL_MS)
|
|
692
719
|
if (state.reconcileTimer && typeof state.reconcileTimer.unref === 'function') state.reconcileTimer.unref()
|
|
693
720
|
}
|
|
721
|
+
// Belt and braces for the registry probe: the event fast path can be missed
|
|
722
|
+
// (dropped while scanning, or never delivered), and unlike the event this
|
|
723
|
+
// cannot silently leave the workspace list stale until a restart.
|
|
724
|
+
function scheduleWorkspaceProbeTimer() {
|
|
725
|
+
if (state.disposed || state.workspaceProbeTimer !== null) return
|
|
726
|
+
state.workspaceProbeTimer = setTimeout(() => {
|
|
727
|
+
state.workspaceProbeTimer = null
|
|
728
|
+
if (!state.disposed) {
|
|
729
|
+
void runWorkspaceSync()
|
|
730
|
+
scheduleWorkspaceProbeTimer()
|
|
731
|
+
}
|
|
732
|
+
}, WORKSPACE_PROBE_INTERVAL_MS)
|
|
733
|
+
if (state.workspaceProbeTimer && typeof state.workspaceProbeTimer.unref === 'function') state.workspaceProbeTimer.unref()
|
|
734
|
+
}
|
|
694
735
|
|
|
695
736
|
// ---------- live feed ----------
|
|
696
737
|
ctx.on('session/event', (session, event) => {
|
|
@@ -768,6 +809,7 @@ export function createSessionSync(host) {
|
|
|
768
809
|
if (change === undefined || change === null) return
|
|
769
810
|
if (change.domain !== 'workspace') return
|
|
770
811
|
if (change.table !== 'workspaces' && change.table !== '') return
|
|
812
|
+
state.sync.workspaceProbeEvents += 1
|
|
771
813
|
scheduleWorkspaceSync()
|
|
772
814
|
})
|
|
773
815
|
|
|
@@ -793,6 +835,7 @@ export function createSessionSync(host) {
|
|
|
793
835
|
reconcileSessions,
|
|
794
836
|
scheduleReconcileHint,
|
|
795
837
|
scheduleReconcileTimer,
|
|
838
|
+
scheduleWorkspaceProbeTimer,
|
|
796
839
|
markLedgerDirty
|
|
797
840
|
}
|
|
798
841
|
}
|