@abloatai/humans 0.42.0 → 0.43.0
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/dist/local/BaseSyncedStore.js +1 -0
- package/dist/local/InstanceCache.d.ts +12 -0
- package/dist/local/InstanceCache.js +20 -0
- package/dist/local/SyncClient.d.ts +11 -0
- package/dist/local/SyncClient.js +45 -1
- package/dist/local/sync/deltaPipeline.d.ts +2 -0
- package/dist/local/sync/deltaPipeline.js +2 -1
- package/dist/local/views/ViewRegistry.d.ts +1 -0
- package/dist/local/views/ViewRegistry.js +3 -0
- package/package.json +2 -2
- package/src/local/BaseSyncedStore.ts +2 -0
- package/src/local/InstanceCache.ts +23 -0
- package/src/local/SyncClient.ts +52 -1
- package/src/local/sync/deltaPipeline.ts +4 -1
- package/src/local/views/ViewRegistry.ts +4 -0
|
@@ -1173,6 +1173,7 @@ export class BaseSyncedStore {
|
|
|
1173
1173
|
advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
|
|
1174
1174
|
// Persistence + pool writes.
|
|
1175
1175
|
processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
|
|
1176
|
+
projectDeltaBatchForPool: (results) => this.syncClient.projectDeltaBatchForPool(results),
|
|
1176
1177
|
applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
|
|
1177
1178
|
acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
|
|
1178
1179
|
get objectPool() { return store.objectPool; },
|
|
@@ -57,6 +57,18 @@ export declare class InstanceCache {
|
|
|
57
57
|
*/
|
|
58
58
|
subscribe<T extends Model>(modelClass: ModelConstructor<T>, callback: (model: T) => void): () => void;
|
|
59
59
|
private notifySubscribers;
|
|
60
|
+
/**
|
|
61
|
+
* Maximum fresh wire rows worth constructing from one atomic apply batch.
|
|
62
|
+
*
|
|
63
|
+
* A headless cache has no recency signal: every never-read resident is
|
|
64
|
+
* equally useful, while the local database remains the authoritative full
|
|
65
|
+
* state. Once that cache is full, replacing cold rows with other cold rows
|
|
66
|
+
* is pure construction/eviction churn, so only currently free slots are
|
|
67
|
+
* admitted. Active row subscribers and incremental views are different:
|
|
68
|
+
* they must observe every addition even when the cache is bounded, so
|
|
69
|
+
* those batches opt out of admission limiting.
|
|
70
|
+
*/
|
|
71
|
+
wireAddRetentionLimit(modelNames: ReadonlySet<string>): number | undefined;
|
|
60
72
|
constructor(config?: PoolConfig, modelRegistry?: ModelRegistry);
|
|
61
73
|
private resolveModel;
|
|
62
74
|
get<T extends Model = Model>(id: string): T | undefined;
|
|
@@ -111,6 +111,26 @@ export class InstanceCache {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Maximum fresh wire rows worth constructing from one atomic apply batch.
|
|
116
|
+
*
|
|
117
|
+
* A headless cache has no recency signal: every never-read resident is
|
|
118
|
+
* equally useful, while the local database remains the authoritative full
|
|
119
|
+
* state. Once that cache is full, replacing cold rows with other cold rows
|
|
120
|
+
* is pure construction/eviction churn, so only currently free slots are
|
|
121
|
+
* admitted. Active row subscribers and incremental views are different:
|
|
122
|
+
* they must observe every addition even when the cache is bounded, so
|
|
123
|
+
* those batches opt out of admission limiting.
|
|
124
|
+
*/
|
|
125
|
+
wireAddRetentionLimit(modelNames) {
|
|
126
|
+
for (const modelName of modelNames) {
|
|
127
|
+
if ((this.subscriptions.get(modelName)?.size ?? 0) > 0 ||
|
|
128
|
+
this.viewRegistry.hasViews(modelName)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return Math.max(0, this.config.maxSize - this.entries.size);
|
|
133
|
+
}
|
|
114
134
|
constructor(config = {}, modelRegistry) {
|
|
115
135
|
this.config = {
|
|
116
136
|
maxSize: config.maxSize ?? 10000,
|
|
@@ -483,6 +483,17 @@ export declare class SyncClient extends EventEmitter {
|
|
|
483
483
|
*/
|
|
484
484
|
getMutationQueue(): MutationQueue;
|
|
485
485
|
applyDeltaBatchToPool(dbResults: readonly AppliedChange[], enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>): void;
|
|
486
|
+
/**
|
|
487
|
+
* Drop fresh adds beyond a headless cache's currently free capacity. The
|
|
488
|
+
* rows are already durable; with no row-level consumer there is no recency
|
|
489
|
+
* signal that makes replacing one cold resident with another useful, so the
|
|
490
|
+
* projection avoids constructing thousands of doomed Model/MobX objects.
|
|
491
|
+
*
|
|
492
|
+
* This must run before apply slicing. A 300k catch-up batch is revealed in
|
|
493
|
+
* ~600-delta slices, each smaller than the 10k cache cap; projecting each
|
|
494
|
+
* slice independently would therefore miss the redundant work entirely.
|
|
495
|
+
*/
|
|
496
|
+
projectDeltaBatchForPool(dbResults: readonly AppliedChange[]): readonly AppliedChange[];
|
|
486
497
|
private applyDeltaBatchToPoolInAction;
|
|
487
498
|
/**
|
|
488
499
|
* Apply bootstrap data to the InstanceCache with ghost removal.
|
package/dist/local/SyncClient.js
CHANGED
|
@@ -1525,6 +1525,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1525
1525
|
return this.mutationQueue;
|
|
1526
1526
|
}
|
|
1527
1527
|
applyDeltaBatchToPool(dbResults, enrichRelations) {
|
|
1528
|
+
const projectedResults = this.projectDeltaBatchForPool(dbResults);
|
|
1528
1529
|
// The WHOLE batch — conflict resolution and model mutation included, not
|
|
1529
1530
|
// just the pool bookkeeping at the end — runs in one MobX action.
|
|
1530
1531
|
// `resolveConflicts` writes model fields via `updateFromData`; when those
|
|
@@ -1533,7 +1534,50 @@ export class SyncClient extends EventEmitter {
|
|
|
1533
1534
|
// reaction pass per delta and an observer could see a partially applied
|
|
1534
1535
|
// frame between them.
|
|
1535
1536
|
runInAction(() => {
|
|
1536
|
-
this.applyDeltaBatchToPoolInAction(
|
|
1537
|
+
this.applyDeltaBatchToPoolInAction(projectedResults, enrichRelations);
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Drop fresh adds beyond a headless cache's currently free capacity. The
|
|
1542
|
+
* rows are already durable; with no row-level consumer there is no recency
|
|
1543
|
+
* signal that makes replacing one cold resident with another useful, so the
|
|
1544
|
+
* projection avoids constructing thousands of doomed Model/MobX objects.
|
|
1545
|
+
*
|
|
1546
|
+
* This must run before apply slicing. A 300k catch-up batch is revealed in
|
|
1547
|
+
* ~600-delta slices, each smaller than the 10k cache cap; projecting each
|
|
1548
|
+
* slice independently would therefore miss the redundant work entirely.
|
|
1549
|
+
*/
|
|
1550
|
+
projectDeltaBatchForPool(dbResults) {
|
|
1551
|
+
const idsBeingRemoved = new Set();
|
|
1552
|
+
const freshAddTypes = new Set();
|
|
1553
|
+
let freshAddCount = 0;
|
|
1554
|
+
for (const result of dbResults) {
|
|
1555
|
+
if (result.action === 'remove')
|
|
1556
|
+
idsBeingRemoved.add(result.modelId);
|
|
1557
|
+
}
|
|
1558
|
+
for (const result of dbResults) {
|
|
1559
|
+
if (result.action === 'add' &&
|
|
1560
|
+
result.data &&
|
|
1561
|
+
!idsBeingRemoved.has(result.modelId) &&
|
|
1562
|
+
!this.objectPool.has(result.modelId)) {
|
|
1563
|
+
freshAddTypes.add(result.modelName);
|
|
1564
|
+
freshAddCount++;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
const retentionLimit = this.objectPool.wireAddRetentionLimit(freshAddTypes);
|
|
1568
|
+
let freshAddsToDiscard = retentionLimit === undefined ? 0 : Math.max(0, freshAddCount - retentionLimit);
|
|
1569
|
+
if (freshAddsToDiscard === 0)
|
|
1570
|
+
return dbResults;
|
|
1571
|
+
return dbResults.filter((result) => {
|
|
1572
|
+
if (freshAddsToDiscard > 0 &&
|
|
1573
|
+
result.action === 'add' &&
|
|
1574
|
+
result.data &&
|
|
1575
|
+
!idsBeingRemoved.has(result.modelId) &&
|
|
1576
|
+
!this.objectPool.has(result.modelId)) {
|
|
1577
|
+
freshAddsToDiscard--;
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
return true;
|
|
1537
1581
|
});
|
|
1538
1582
|
}
|
|
1539
1583
|
applyDeltaBatchToPoolInAction(dbResults, enrichRelations) {
|
|
@@ -60,6 +60,8 @@ export interface DeltaPipelineContext {
|
|
|
60
60
|
}>;
|
|
61
61
|
/** Applies persisted delta results to the in-memory pool, with the host's relation enrichment bound. */
|
|
62
62
|
applyDeltaBatchToPool(results: AppliedChange[]): void;
|
|
63
|
+
/** Optional whole-batch projection performed before bounded apply slicing. */
|
|
64
|
+
projectDeltaBatchForPool?(results: readonly AppliedChange[]): readonly AppliedChange[];
|
|
63
65
|
/** Acknowledges a sync id back to the server; a no-op when the socket is down. */
|
|
64
66
|
acknowledge(syncId: number): void;
|
|
65
67
|
readonly objectPool: {
|
|
@@ -437,7 +437,8 @@ async function flushDeltaBatchInner(ctx, queuedDeltas) {
|
|
|
437
437
|
// synchronous block. Each slice is still one MobX action (reactions fire
|
|
438
438
|
// once per slice), and a transaction never splits across slices — the
|
|
439
439
|
// commit remains the atomic unit of visibility.
|
|
440
|
-
const
|
|
440
|
+
const poolResults = ctx.projectDeltaBatchForPool?.(dbResults) ?? dbResults;
|
|
441
|
+
const slices = sliceApplyChanges(poolResults, ctx.smartSyncOptions.applySliceDeltas);
|
|
441
442
|
await timeDrainStageAsync('apply', async () => {
|
|
442
443
|
const hasApplyPlugins = pluginsForStage(stagePlugins, 'apply').length > 0;
|
|
443
444
|
// Yield on a TIME budget, not per slice: a host yield costs milliseconds
|
|
@@ -11,6 +11,7 @@ export declare class ViewRegistry {
|
|
|
11
11
|
private views;
|
|
12
12
|
register(typename: string, view: IncrementalView): void;
|
|
13
13
|
unregister(typename: string, view: IncrementalView): void;
|
|
14
|
+
hasViews(typename: string): boolean;
|
|
14
15
|
/** Called by InstanceCache after a model is added to the pool. */
|
|
15
16
|
notifyAdded(typename: string, model: Model): void;
|
|
16
17
|
/** Called by InstanceCache after a model is updated in the pool. */
|
|
@@ -25,6 +25,9 @@ export class ViewRegistry {
|
|
|
25
25
|
this.views.delete(typename);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
hasViews(typename) {
|
|
29
|
+
return (this.views.get(typename)?.size ?? 0) > 0;
|
|
30
|
+
}
|
|
28
31
|
/** Called by InstanceCache after a model is added to the pool. */
|
|
29
32
|
notifyAdded(typename, model) {
|
|
30
33
|
const set = this.views.get(typename);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.
|
|
87
|
+
"@abloatai/transaction": "^0.43.0",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
|
@@ -1568,6 +1568,8 @@ export class BaseSyncedStore<
|
|
|
1568
1568
|
advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
|
|
1569
1569
|
// Persistence + pool writes.
|
|
1570
1570
|
processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
|
|
1571
|
+
projectDeltaBatchForPool: (results) =>
|
|
1572
|
+
this.syncClient.projectDeltaBatchForPool(results),
|
|
1571
1573
|
applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
|
|
1572
1574
|
acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
|
|
1573
1575
|
get objectPool() { return store.objectPool; },
|
|
@@ -162,6 +162,29 @@ export class InstanceCache {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Maximum fresh wire rows worth constructing from one atomic apply batch.
|
|
167
|
+
*
|
|
168
|
+
* A headless cache has no recency signal: every never-read resident is
|
|
169
|
+
* equally useful, while the local database remains the authoritative full
|
|
170
|
+
* state. Once that cache is full, replacing cold rows with other cold rows
|
|
171
|
+
* is pure construction/eviction churn, so only currently free slots are
|
|
172
|
+
* admitted. Active row subscribers and incremental views are different:
|
|
173
|
+
* they must observe every addition even when the cache is bounded, so
|
|
174
|
+
* those batches opt out of admission limiting.
|
|
175
|
+
*/
|
|
176
|
+
wireAddRetentionLimit(modelNames: ReadonlySet<string>): number | undefined {
|
|
177
|
+
for (const modelName of modelNames) {
|
|
178
|
+
if (
|
|
179
|
+
(this.subscriptions.get(modelName)?.size ?? 0) > 0 ||
|
|
180
|
+
this.viewRegistry.hasViews(modelName)
|
|
181
|
+
) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return Math.max(0, this.config.maxSize - this.entries.size);
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
constructor(config: PoolConfig = {}, modelRegistry?: ModelRegistry) {
|
|
166
189
|
this.config = {
|
|
167
190
|
maxSize: config.maxSize ?? 10000,
|
package/src/local/SyncClient.ts
CHANGED
|
@@ -1889,6 +1889,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1889
1889
|
dbResults: readonly AppliedChange[],
|
|
1890
1890
|
enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>,
|
|
1891
1891
|
): void {
|
|
1892
|
+
const projectedResults = this.projectDeltaBatchForPool(dbResults);
|
|
1892
1893
|
// The WHOLE batch — conflict resolution and model mutation included, not
|
|
1893
1894
|
// just the pool bookkeeping at the end — runs in one MobX action.
|
|
1894
1895
|
// `resolveConflicts` writes model fields via `updateFromData`; when those
|
|
@@ -1897,7 +1898,57 @@ export class SyncClient extends EventEmitter {
|
|
|
1897
1898
|
// reaction pass per delta and an observer could see a partially applied
|
|
1898
1899
|
// frame between them.
|
|
1899
1900
|
runInAction(() => {
|
|
1900
|
-
this.applyDeltaBatchToPoolInAction(
|
|
1901
|
+
this.applyDeltaBatchToPoolInAction(projectedResults, enrichRelations);
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Drop fresh adds beyond a headless cache's currently free capacity. The
|
|
1907
|
+
* rows are already durable; with no row-level consumer there is no recency
|
|
1908
|
+
* signal that makes replacing one cold resident with another useful, so the
|
|
1909
|
+
* projection avoids constructing thousands of doomed Model/MobX objects.
|
|
1910
|
+
*
|
|
1911
|
+
* This must run before apply slicing. A 300k catch-up batch is revealed in
|
|
1912
|
+
* ~600-delta slices, each smaller than the 10k cache cap; projecting each
|
|
1913
|
+
* slice independently would therefore miss the redundant work entirely.
|
|
1914
|
+
*/
|
|
1915
|
+
projectDeltaBatchForPool(
|
|
1916
|
+
dbResults: readonly AppliedChange[],
|
|
1917
|
+
): readonly AppliedChange[] {
|
|
1918
|
+
const idsBeingRemoved = new Set<string>();
|
|
1919
|
+
const freshAddTypes = new Set<string>();
|
|
1920
|
+
let freshAddCount = 0;
|
|
1921
|
+
for (const result of dbResults) {
|
|
1922
|
+
if (result.action === 'remove') idsBeingRemoved.add(result.modelId);
|
|
1923
|
+
}
|
|
1924
|
+
for (const result of dbResults) {
|
|
1925
|
+
if (
|
|
1926
|
+
result.action === 'add' &&
|
|
1927
|
+
result.data &&
|
|
1928
|
+
!idsBeingRemoved.has(result.modelId) &&
|
|
1929
|
+
!this.objectPool.has(result.modelId)
|
|
1930
|
+
) {
|
|
1931
|
+
freshAddTypes.add(result.modelName);
|
|
1932
|
+
freshAddCount++;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const retentionLimit = this.objectPool.wireAddRetentionLimit(freshAddTypes);
|
|
1936
|
+
let freshAddsToDiscard =
|
|
1937
|
+
retentionLimit === undefined ? 0 : Math.max(0, freshAddCount - retentionLimit);
|
|
1938
|
+
if (freshAddsToDiscard === 0) return dbResults;
|
|
1939
|
+
|
|
1940
|
+
return dbResults.filter((result) => {
|
|
1941
|
+
if (
|
|
1942
|
+
freshAddsToDiscard > 0 &&
|
|
1943
|
+
result.action === 'add' &&
|
|
1944
|
+
result.data &&
|
|
1945
|
+
!idsBeingRemoved.has(result.modelId) &&
|
|
1946
|
+
!this.objectPool.has(result.modelId)
|
|
1947
|
+
) {
|
|
1948
|
+
freshAddsToDiscard--;
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
return true;
|
|
1901
1952
|
});
|
|
1902
1953
|
}
|
|
1903
1954
|
|
|
@@ -85,6 +85,8 @@ export interface DeltaPipelineContext {
|
|
|
85
85
|
): Promise<{ results: AppliedChange[]; persistedSyncId: number }>;
|
|
86
86
|
/** Applies persisted delta results to the in-memory pool, with the host's relation enrichment bound. */
|
|
87
87
|
applyDeltaBatchToPool(results: AppliedChange[]): void;
|
|
88
|
+
/** Optional whole-batch projection performed before bounded apply slicing. */
|
|
89
|
+
projectDeltaBatchForPool?(results: readonly AppliedChange[]): readonly AppliedChange[];
|
|
88
90
|
/** Acknowledges a sync id back to the server; a no-op when the socket is down. */
|
|
89
91
|
acknowledge(syncId: number): void;
|
|
90
92
|
|
|
@@ -572,7 +574,8 @@ async function flushDeltaBatchInner(
|
|
|
572
574
|
// synchronous block. Each slice is still one MobX action (reactions fire
|
|
573
575
|
// once per slice), and a transaction never splits across slices — the
|
|
574
576
|
// commit remains the atomic unit of visibility.
|
|
575
|
-
const
|
|
577
|
+
const poolResults = ctx.projectDeltaBatchForPool?.(dbResults) ?? dbResults;
|
|
578
|
+
const slices = sliceApplyChanges(poolResults, ctx.smartSyncOptions.applySliceDeltas);
|
|
576
579
|
await timeDrainStageAsync('apply', async () => {
|
|
577
580
|
const hasApplyPlugins = pluginsForStage(stagePlugins, 'apply').length > 0;
|
|
578
581
|
// Yield on a TIME budget, not per slice: a host yield costs milliseconds
|
|
@@ -30,6 +30,10 @@ export class ViewRegistry {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
hasViews(typename: string): boolean {
|
|
34
|
+
return (this.views.get(typename)?.size ?? 0) > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
/** Called by InstanceCache after a model is added to the pool. */
|
|
34
38
|
notifyAdded(typename: string, model: Model): void {
|
|
35
39
|
const set = this.views.get(typename);
|