@abloatai/ablo 0.3.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/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockSyncContext — Creates a fully-wired SyncEngineContext for tests.
|
|
3
|
+
*
|
|
4
|
+
* `createTestContext()` is the primary test utility: it builds a complete
|
|
5
|
+
* DI container with mock implementations, calls initSyncEngine(), and
|
|
6
|
+
* returns handles to all mocks for test assertions.
|
|
7
|
+
*/
|
|
8
|
+
import type { SyncEngineContext } from '../../SyncEngineContext.js';
|
|
9
|
+
import type { SyncLogger, SyncObservabilityProvider, SessionErrorDetector, MutationDispatcher, SyncEngineConfig } from '../../interfaces/index.js';
|
|
10
|
+
import { MockMutationExecutor } from './MockMutationExecutor.js';
|
|
11
|
+
import { MockNetworkMonitor } from './MockNetworkMonitor.js';
|
|
12
|
+
export interface TestContextOptions {
|
|
13
|
+
/** Override the logger (default: noopLogger) */
|
|
14
|
+
logger?: SyncLogger;
|
|
15
|
+
/** Override observability (default: noopObservability) */
|
|
16
|
+
observability?: SyncObservabilityProvider;
|
|
17
|
+
/** Override session error detector */
|
|
18
|
+
sessionErrorDetector?: SessionErrorDetector;
|
|
19
|
+
/** Override mutation executor options */
|
|
20
|
+
mutationExecutorOptions?: ConstructorParameters<typeof MockMutationExecutor>[0];
|
|
21
|
+
/** Override the sync engine config */
|
|
22
|
+
config?: Partial<SyncEngineConfig>;
|
|
23
|
+
/** Start offline (default: false) */
|
|
24
|
+
startOffline?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface TestContextResult {
|
|
27
|
+
/** The full SyncEngineContext passed to initSyncEngine */
|
|
28
|
+
context: SyncEngineContext;
|
|
29
|
+
/** Mock handles for test assertions */
|
|
30
|
+
mocks: {
|
|
31
|
+
mutationExecutor: MockMutationExecutor;
|
|
32
|
+
mutationDispatcher: MockMutationDispatcher;
|
|
33
|
+
networkMonitor: MockNetworkMonitor;
|
|
34
|
+
};
|
|
35
|
+
/** Cleanup: calls resetSyncEngine() */
|
|
36
|
+
cleanup: () => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Simple mock mutation dispatcher that records dispatch calls.
|
|
40
|
+
*/
|
|
41
|
+
export declare class MockMutationDispatcher implements MutationDispatcher {
|
|
42
|
+
readonly dispatched: Array<{
|
|
43
|
+
operationName: string;
|
|
44
|
+
variables: Record<string, unknown>;
|
|
45
|
+
}>;
|
|
46
|
+
private _shouldSucceed;
|
|
47
|
+
private _error?;
|
|
48
|
+
dispatch(operationName: string, variables: Record<string, unknown>): Promise<void>;
|
|
49
|
+
failAll(error?: Error): void;
|
|
50
|
+
succeedAll(): void;
|
|
51
|
+
reset(): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a test SyncEngineContext with all mocks pre-wired.
|
|
55
|
+
* Calls initSyncEngine() so the global context is set.
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* ```ts
|
|
59
|
+
* const { context, mocks, cleanup } = createTestContext();
|
|
60
|
+
* // ... run tests using mocks.mutationExecutor, mocks.networkMonitor
|
|
61
|
+
* cleanup();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function createTestContext(options?: TestContextOptions): TestContextResult;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockSyncContext — Creates a fully-wired SyncEngineContext for tests.
|
|
3
|
+
*
|
|
4
|
+
* `createTestContext()` is the primary test utility: it builds a complete
|
|
5
|
+
* DI container with mock implementations, calls initSyncEngine(), and
|
|
6
|
+
* returns handles to all mocks for test assertions.
|
|
7
|
+
*/
|
|
8
|
+
import { noopLogger, noopObservability, noopAnalytics, defaultSessionErrorDetector, emptyConfig, } from '../../SyncEngineContext.js';
|
|
9
|
+
import { initSyncEngine, resetSyncEngine } from '../../context.js';
|
|
10
|
+
import { ModelRegistry, setActiveRegistry, hasActiveRegistry, } from '../../ModelRegistry.js';
|
|
11
|
+
import { registerTestModels } from '../fixtures/models.js';
|
|
12
|
+
import { MockMutationExecutor } from './MockMutationExecutor.js';
|
|
13
|
+
import { MockNetworkMonitor } from './MockNetworkMonitor.js';
|
|
14
|
+
/**
|
|
15
|
+
* Simple mock mutation dispatcher that records dispatch calls.
|
|
16
|
+
*/
|
|
17
|
+
export class MockMutationDispatcher {
|
|
18
|
+
dispatched = [];
|
|
19
|
+
_shouldSucceed = true;
|
|
20
|
+
_error;
|
|
21
|
+
async dispatch(operationName, variables) {
|
|
22
|
+
this.dispatched.push({ operationName, variables });
|
|
23
|
+
if (!this._shouldSucceed) {
|
|
24
|
+
throw this._error ?? new Error(`Mock dispatch failed: ${operationName}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
failAll(error) {
|
|
28
|
+
this._shouldSucceed = false;
|
|
29
|
+
this._error = error;
|
|
30
|
+
}
|
|
31
|
+
succeedAll() {
|
|
32
|
+
this._shouldSucceed = true;
|
|
33
|
+
this._error = undefined;
|
|
34
|
+
}
|
|
35
|
+
reset() {
|
|
36
|
+
this.dispatched.length = 0;
|
|
37
|
+
this._shouldSucceed = true;
|
|
38
|
+
this._error = undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a test SyncEngineContext with all mocks pre-wired.
|
|
43
|
+
* Calls initSyncEngine() so the global context is set.
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* ```ts
|
|
47
|
+
* const { context, mocks, cleanup } = createTestContext();
|
|
48
|
+
* // ... run tests using mocks.mutationExecutor, mocks.networkMonitor
|
|
49
|
+
* cleanup();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function createTestContext(options = {}) {
|
|
53
|
+
const mutationExecutor = new MockMutationExecutor(options.mutationExecutorOptions);
|
|
54
|
+
const mutationDispatcher = new MockMutationDispatcher();
|
|
55
|
+
const networkMonitor = new MockNetworkMonitor(!options.startOffline);
|
|
56
|
+
const config = {
|
|
57
|
+
...emptyConfig,
|
|
58
|
+
...options.config,
|
|
59
|
+
// Merge maps/sets properly if overrides provided
|
|
60
|
+
modelCreatePriority: options.config?.modelCreatePriority ?? emptyConfig.modelCreatePriority,
|
|
61
|
+
};
|
|
62
|
+
const context = {
|
|
63
|
+
logger: options.logger ?? noopLogger,
|
|
64
|
+
observability: options.observability ?? noopObservability,
|
|
65
|
+
analytics: noopAnalytics,
|
|
66
|
+
sessionErrorDetector: options.sessionErrorDetector ?? defaultSessionErrorDetector,
|
|
67
|
+
onlineStatus: networkMonitor,
|
|
68
|
+
mutationExecutor,
|
|
69
|
+
mutationDispatcher,
|
|
70
|
+
config,
|
|
71
|
+
};
|
|
72
|
+
initSyncEngine(context);
|
|
73
|
+
// Bootstrap a default ModelRegistry with test models if none is active.
|
|
74
|
+
// Tests that manage their own registry call setActiveRegistry before this.
|
|
75
|
+
const bootstrappedRegistry = !hasActiveRegistry();
|
|
76
|
+
if (bootstrappedRegistry) {
|
|
77
|
+
const defaultRegistry = new ModelRegistry();
|
|
78
|
+
setActiveRegistry(defaultRegistry);
|
|
79
|
+
registerTestModels(defaultRegistry);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
context,
|
|
83
|
+
mocks: {
|
|
84
|
+
mutationExecutor,
|
|
85
|
+
mutationDispatcher,
|
|
86
|
+
networkMonitor,
|
|
87
|
+
},
|
|
88
|
+
cleanup: () => {
|
|
89
|
+
resetSyncEngine();
|
|
90
|
+
// Intentionally do NOT clear the active ModelRegistry — async callbacks
|
|
91
|
+
// from in-flight transactions (e.g. fc.asyncProperty iterations) may
|
|
92
|
+
// call Model.toJSON() after afterEach runs. Leaving the default
|
|
93
|
+
// registry in place keeps those calls valid; the next createTestContext
|
|
94
|
+
// with hasActiveRegistry()===true simply reuses it.
|
|
95
|
+
mutationExecutor.reset();
|
|
96
|
+
mutationDispatcher.reset();
|
|
97
|
+
networkMonitor.reset();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockSyncStore — a SyncStoreContract implementation for testing.
|
|
3
|
+
*
|
|
4
|
+
* Provides an in-memory store that tests can configure, inspect, and mutate.
|
|
5
|
+
* All reactive operations are synchronous and observable via Jest spies.
|
|
6
|
+
*/
|
|
7
|
+
import type { Model } from '../../Model.js';
|
|
8
|
+
import type { ModelScope } from '../../ObjectPool.js';
|
|
9
|
+
import type { SyncStoreContract } from '../../react/context.js';
|
|
10
|
+
interface QueryOptions<T extends Model> {
|
|
11
|
+
predicate?: (model: T) => boolean;
|
|
12
|
+
scope?: ModelScope;
|
|
13
|
+
orderBy?: keyof T;
|
|
14
|
+
order?: 'asc' | 'desc';
|
|
15
|
+
limit?: number;
|
|
16
|
+
offset?: number;
|
|
17
|
+
}
|
|
18
|
+
interface QueryResult<T extends Model> {
|
|
19
|
+
data: T[];
|
|
20
|
+
}
|
|
21
|
+
type ModelCtor<T extends Model> = abstract new (...args: never[]) => T;
|
|
22
|
+
/**
|
|
23
|
+
* MockSyncStore is an in-memory implementation of SyncStoreContract.
|
|
24
|
+
* Tests can seed data with `setModels()`, inspect calls via `calls`,
|
|
25
|
+
* and assert behavior without needing a real sync backend.
|
|
26
|
+
*/
|
|
27
|
+
export declare class MockSyncStore implements SyncStoreContract {
|
|
28
|
+
private byClass;
|
|
29
|
+
calls: {
|
|
30
|
+
retrieve: Array<{
|
|
31
|
+
modelClass: ModelCtor<Model>;
|
|
32
|
+
id: string;
|
|
33
|
+
}>;
|
|
34
|
+
query: Array<{
|
|
35
|
+
modelClass: ModelCtor<Model>;
|
|
36
|
+
options?: QueryOptions<Model>;
|
|
37
|
+
}>;
|
|
38
|
+
save: Model[];
|
|
39
|
+
delete: Model[];
|
|
40
|
+
archive: Model[];
|
|
41
|
+
unarchive: Model[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Seed the store with models of a specific class.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* mockStore.setModels(Task, [task1, task2]);
|
|
48
|
+
*/
|
|
49
|
+
setModels<T extends Model>(modelClass: ModelCtor<T>, models: T[]): void;
|
|
50
|
+
/**
|
|
51
|
+
* Add a single model (upsert).
|
|
52
|
+
*/
|
|
53
|
+
addModel<T extends Model>(modelClass: ModelCtor<T>, model: T): void;
|
|
54
|
+
/**
|
|
55
|
+
* Remove a model by ID.
|
|
56
|
+
*/
|
|
57
|
+
removeModel<T extends Model>(modelClass: ModelCtor<T>, id: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Clear all seeded data and call history.
|
|
60
|
+
*/
|
|
61
|
+
reset(): void;
|
|
62
|
+
retrieve<T extends Model>(modelClass: ModelCtor<T>, id: string): T | undefined;
|
|
63
|
+
queryByClass<T extends Model>(modelClass: ModelCtor<T>, options?: QueryOptions<T>): QueryResult<T>;
|
|
64
|
+
save(model: Model): Promise<void>;
|
|
65
|
+
delete(model: Model): Promise<void>;
|
|
66
|
+
archive(model: Model): Promise<void>;
|
|
67
|
+
unarchive(model: Model): Promise<void>;
|
|
68
|
+
isReady: boolean;
|
|
69
|
+
isSyncing: boolean;
|
|
70
|
+
isOffline: boolean;
|
|
71
|
+
isReconnecting: boolean;
|
|
72
|
+
isError: boolean;
|
|
73
|
+
hasUnsyncedChanges: boolean;
|
|
74
|
+
syncStatus: {
|
|
75
|
+
state: "idle";
|
|
76
|
+
progress: number;
|
|
77
|
+
pendingChanges: number;
|
|
78
|
+
isSessionError: boolean;
|
|
79
|
+
};
|
|
80
|
+
/** Mock pool for useEntity/useEntities hooks. */
|
|
81
|
+
pool: SyncStoreContract['pool'];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a new MockSyncStore.
|
|
85
|
+
* Shorthand for `new MockSyncStore()`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createMockSyncStore(): MockSyncStore;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockSyncStore — a SyncStoreContract implementation for testing.
|
|
3
|
+
*
|
|
4
|
+
* Provides an in-memory store that tests can configure, inspect, and mutate.
|
|
5
|
+
* All reactive operations are synchronous and observable via Jest spies.
|
|
6
|
+
*/
|
|
7
|
+
import { ViewRegistry } from '../../core/ViewRegistry.js';
|
|
8
|
+
import { AbloValidationError } from '../../errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* MockSyncStore is an in-memory implementation of SyncStoreContract.
|
|
11
|
+
* Tests can seed data with `setModels()`, inspect calls via `calls`,
|
|
12
|
+
* and assert behavior without needing a real sync backend.
|
|
13
|
+
*/
|
|
14
|
+
export class MockSyncStore {
|
|
15
|
+
// Seeded data, keyed by model class
|
|
16
|
+
byClass = new Map();
|
|
17
|
+
// Call tracking for assertions
|
|
18
|
+
calls = {
|
|
19
|
+
retrieve: [],
|
|
20
|
+
query: [],
|
|
21
|
+
save: [],
|
|
22
|
+
delete: [],
|
|
23
|
+
archive: [],
|
|
24
|
+
unarchive: [],
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Seed the store with models of a specific class.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* mockStore.setModels(Task, [task1, task2]);
|
|
31
|
+
*/
|
|
32
|
+
setModels(modelClass, models) {
|
|
33
|
+
const map = new Map();
|
|
34
|
+
for (const m of models) {
|
|
35
|
+
map.set(m.id, m);
|
|
36
|
+
}
|
|
37
|
+
this.byClass.set(modelClass, map);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Add a single model (upsert).
|
|
41
|
+
*/
|
|
42
|
+
addModel(modelClass, model) {
|
|
43
|
+
let map = this.byClass.get(modelClass);
|
|
44
|
+
if (!map) {
|
|
45
|
+
map = new Map();
|
|
46
|
+
this.byClass.set(modelClass, map);
|
|
47
|
+
}
|
|
48
|
+
map.set(model.id, model);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Remove a model by ID.
|
|
52
|
+
*/
|
|
53
|
+
removeModel(modelClass, id) {
|
|
54
|
+
this.byClass.get(modelClass)?.delete(id);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clear all seeded data and call history.
|
|
58
|
+
*/
|
|
59
|
+
reset() {
|
|
60
|
+
this.byClass.clear();
|
|
61
|
+
this.calls = {
|
|
62
|
+
retrieve: [],
|
|
63
|
+
query: [],
|
|
64
|
+
save: [],
|
|
65
|
+
delete: [],
|
|
66
|
+
archive: [],
|
|
67
|
+
unarchive: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ── SyncStoreContract implementation ──────────────────────────────────
|
|
71
|
+
retrieve(modelClass, id) {
|
|
72
|
+
this.calls.retrieve.push({ modelClass: modelClass, id });
|
|
73
|
+
return this.byClass.get(modelClass)?.get(id);
|
|
74
|
+
}
|
|
75
|
+
queryByClass(modelClass, options) {
|
|
76
|
+
this.calls.query.push({
|
|
77
|
+
modelClass: modelClass,
|
|
78
|
+
options: options,
|
|
79
|
+
});
|
|
80
|
+
const map = this.byClass.get(modelClass);
|
|
81
|
+
if (!map) {
|
|
82
|
+
return { data: [] };
|
|
83
|
+
}
|
|
84
|
+
let data = Array.from(map.values());
|
|
85
|
+
// Apply predicate
|
|
86
|
+
if (options?.predicate) {
|
|
87
|
+
data = data.filter(options.predicate);
|
|
88
|
+
}
|
|
89
|
+
// Apply ordering
|
|
90
|
+
if (options?.orderBy) {
|
|
91
|
+
const key = options.orderBy;
|
|
92
|
+
const order = options.order ?? 'asc';
|
|
93
|
+
data.sort((a, b) => {
|
|
94
|
+
const av = a[key];
|
|
95
|
+
const bv = b[key];
|
|
96
|
+
if (av === bv)
|
|
97
|
+
return 0;
|
|
98
|
+
const cmp = av < bv ? -1 : 1;
|
|
99
|
+
return order === 'asc' ? cmp : -cmp;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Apply pagination
|
|
103
|
+
if (options?.offset) {
|
|
104
|
+
data = data.slice(options.offset);
|
|
105
|
+
}
|
|
106
|
+
if (options?.limit !== undefined) {
|
|
107
|
+
data = data.slice(0, options.limit);
|
|
108
|
+
}
|
|
109
|
+
return { data };
|
|
110
|
+
}
|
|
111
|
+
async save(model) {
|
|
112
|
+
this.calls.save.push(model);
|
|
113
|
+
// Auto-seed on save so findById returns it afterwards
|
|
114
|
+
// Consumer passes a concrete class-less object; we store by constructor
|
|
115
|
+
const ctor = model.constructor;
|
|
116
|
+
this.addModel(ctor, model);
|
|
117
|
+
}
|
|
118
|
+
async delete(model) {
|
|
119
|
+
this.calls.delete.push(model);
|
|
120
|
+
const ctor = model.constructor;
|
|
121
|
+
this.removeModel(ctor, model.id);
|
|
122
|
+
}
|
|
123
|
+
async archive(model) {
|
|
124
|
+
this.calls.archive.push(model);
|
|
125
|
+
}
|
|
126
|
+
async unarchive(model) {
|
|
127
|
+
this.calls.unarchive.push(model);
|
|
128
|
+
}
|
|
129
|
+
// Sync-status getters default to "ready, idle". Tests that exercise
|
|
130
|
+
// offline/reconnect flows can set these before rendering.
|
|
131
|
+
isReady = true;
|
|
132
|
+
isSyncing = false;
|
|
133
|
+
isOffline = false;
|
|
134
|
+
isReconnecting = false;
|
|
135
|
+
isError = false;
|
|
136
|
+
hasUnsyncedChanges = false;
|
|
137
|
+
syncStatus = {
|
|
138
|
+
state: 'idle',
|
|
139
|
+
progress: 100,
|
|
140
|
+
pendingChanges: 0,
|
|
141
|
+
isSessionError: false,
|
|
142
|
+
};
|
|
143
|
+
/** Mock pool for useEntity/useEntities hooks. */
|
|
144
|
+
pool = {
|
|
145
|
+
get: (id) => {
|
|
146
|
+
for (const models of this.byClass.values()) {
|
|
147
|
+
const model = models.get(id);
|
|
148
|
+
if (model)
|
|
149
|
+
return model;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
},
|
|
153
|
+
getByTypeName: (_typename) => [],
|
|
154
|
+
getByForeignKey: () => [],
|
|
155
|
+
createFromData: () => null,
|
|
156
|
+
hasForeignKeyIndex: () => false,
|
|
157
|
+
createView: () => {
|
|
158
|
+
throw new AbloValidationError('MockSyncStore does not support createView', {
|
|
159
|
+
code: 'mock_unsupported_operation',
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
viewRegistry: new ViewRegistry(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create a new MockSyncStore.
|
|
167
|
+
* Shorthand for `new MockSyncStore()`.
|
|
168
|
+
*/
|
|
169
|
+
export function createMockSyncStore() {
|
|
170
|
+
return new MockSyncStore();
|
|
171
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockWebSocket — Controllable WebSocket for sync engine tests.
|
|
3
|
+
*
|
|
4
|
+
* Simulates the SyncWebSocket event interface without a real connection.
|
|
5
|
+
* Provides methods to inject deltas, simulate disconnection/reconnection,
|
|
6
|
+
* and trigger bootstrap hints.
|
|
7
|
+
*/
|
|
8
|
+
import type { SyncActionType } from '../../types/index.js';
|
|
9
|
+
/** Delta shape matching the SyncAction interface */
|
|
10
|
+
export interface MockDelta {
|
|
11
|
+
id: number;
|
|
12
|
+
modelName: string;
|
|
13
|
+
modelId: string;
|
|
14
|
+
action: SyncActionType;
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** Bootstrap hint from server */
|
|
18
|
+
export interface MockBootstrapHint {
|
|
19
|
+
reason: 'too_far_behind' | 'too_many_deltas' | 'missing_entities';
|
|
20
|
+
tables?: string[];
|
|
21
|
+
staleTables?: string[];
|
|
22
|
+
}
|
|
23
|
+
type EventHandler = (...args: unknown[]) => void;
|
|
24
|
+
/**
|
|
25
|
+
* MockWebSocket provides a controllable event-based interface
|
|
26
|
+
* for testing sync engine components that consume WebSocket events.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MockWebSocket {
|
|
29
|
+
private _connected;
|
|
30
|
+
private _sessionError;
|
|
31
|
+
private _listeners;
|
|
32
|
+
/** Track all emitted events for assertions */
|
|
33
|
+
readonly emittedEvents: Array<{
|
|
34
|
+
type: string;
|
|
35
|
+
data: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
get connected(): boolean;
|
|
38
|
+
get sessionError(): boolean;
|
|
39
|
+
on(event: string, handler: EventHandler): () => void;
|
|
40
|
+
private emit;
|
|
41
|
+
/** Simulate successful connection */
|
|
42
|
+
simulateConnect(): void;
|
|
43
|
+
/** Simulate disconnection */
|
|
44
|
+
simulateDisconnect(): void;
|
|
45
|
+
/** Simulate reconnection attempt */
|
|
46
|
+
simulateReconnecting(attempt: number, delay: number): void;
|
|
47
|
+
/** Simulate session error (401/403) */
|
|
48
|
+
simulateSessionError(code?: number): void;
|
|
49
|
+
/** Simulate reconnection failure (max attempts reached) */
|
|
50
|
+
simulateReconnectFailed(): void;
|
|
51
|
+
/** Inject a single delta (as if received from server) */
|
|
52
|
+
receiveDelta(delta: MockDelta): void;
|
|
53
|
+
/** Inject a batch of deltas */
|
|
54
|
+
receiveDeltas(deltas: MockDelta[]): void;
|
|
55
|
+
/** Inject a bootstrap_required hint from server */
|
|
56
|
+
simulateBootstrapHint(hint: MockBootstrapHint): void;
|
|
57
|
+
/** Inject a presence update */
|
|
58
|
+
simulatePresenceUpdate(data: Record<string, unknown>): void;
|
|
59
|
+
/** Get all events of a specific type */
|
|
60
|
+
getEvents(type: string): unknown[];
|
|
61
|
+
/** Check if a specific event was emitted */
|
|
62
|
+
hasEmitted(type: string): boolean;
|
|
63
|
+
/** Reset all state */
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockWebSocket — Controllable WebSocket for sync engine tests.
|
|
3
|
+
*
|
|
4
|
+
* Simulates the SyncWebSocket event interface without a real connection.
|
|
5
|
+
* Provides methods to inject deltas, simulate disconnection/reconnection,
|
|
6
|
+
* and trigger bootstrap hints.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* MockWebSocket provides a controllable event-based interface
|
|
10
|
+
* for testing sync engine components that consume WebSocket events.
|
|
11
|
+
*/
|
|
12
|
+
export class MockWebSocket {
|
|
13
|
+
_connected = false;
|
|
14
|
+
_sessionError = false;
|
|
15
|
+
_listeners = new Map();
|
|
16
|
+
/** Track all emitted events for assertions */
|
|
17
|
+
emittedEvents = [];
|
|
18
|
+
get connected() {
|
|
19
|
+
return this._connected;
|
|
20
|
+
}
|
|
21
|
+
get sessionError() {
|
|
22
|
+
return this._sessionError;
|
|
23
|
+
}
|
|
24
|
+
// ─────────────────────────────────────────────
|
|
25
|
+
// Event subscription (matches SyncWebSocket API)
|
|
26
|
+
// ─────────────────────────────────────────────
|
|
27
|
+
on(event, handler) {
|
|
28
|
+
if (!this._listeners.has(event)) {
|
|
29
|
+
this._listeners.set(event, new Set());
|
|
30
|
+
}
|
|
31
|
+
this._listeners.get(event).add(handler);
|
|
32
|
+
return () => {
|
|
33
|
+
this._listeners.get(event)?.delete(handler);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
emit(event, ...args) {
|
|
37
|
+
this.emittedEvents.push({ type: event, data: args[0] });
|
|
38
|
+
const handlers = this._listeners.get(event);
|
|
39
|
+
if (handlers) {
|
|
40
|
+
for (const handler of handlers) {
|
|
41
|
+
handler(...args);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ─────────────────────────────────────────────
|
|
46
|
+
// Test control: connection lifecycle
|
|
47
|
+
// ─────────────────────────────────────────────
|
|
48
|
+
/** Simulate successful connection */
|
|
49
|
+
simulateConnect() {
|
|
50
|
+
this._connected = true;
|
|
51
|
+
this._sessionError = false;
|
|
52
|
+
this.emit('connected');
|
|
53
|
+
}
|
|
54
|
+
/** Simulate disconnection */
|
|
55
|
+
simulateDisconnect() {
|
|
56
|
+
this._connected = false;
|
|
57
|
+
this.emit('disconnected');
|
|
58
|
+
}
|
|
59
|
+
/** Simulate reconnection attempt */
|
|
60
|
+
simulateReconnecting(attempt, delay) {
|
|
61
|
+
this.emit('reconnecting', { attempt, delay });
|
|
62
|
+
}
|
|
63
|
+
/** Simulate session error (401/403) */
|
|
64
|
+
simulateSessionError(code = 401) {
|
|
65
|
+
this._sessionError = true;
|
|
66
|
+
this._connected = false;
|
|
67
|
+
this.emit('session_error', { code });
|
|
68
|
+
}
|
|
69
|
+
/** Simulate reconnection failure (max attempts reached) */
|
|
70
|
+
simulateReconnectFailed() {
|
|
71
|
+
this._connected = false;
|
|
72
|
+
this.emit('reconnect_failed');
|
|
73
|
+
}
|
|
74
|
+
// ─────────────────────────────────────────────
|
|
75
|
+
// Test control: delta injection
|
|
76
|
+
// ─────────────────────────────────────────────
|
|
77
|
+
/** Inject a single delta (as if received from server) */
|
|
78
|
+
receiveDelta(delta) {
|
|
79
|
+
this.emit('delta', delta);
|
|
80
|
+
}
|
|
81
|
+
/** Inject a batch of deltas */
|
|
82
|
+
receiveDeltas(deltas) {
|
|
83
|
+
this.emit('delta_batch', deltas);
|
|
84
|
+
}
|
|
85
|
+
// ─────────────────────────────────────────────
|
|
86
|
+
// Test control: bootstrap hints
|
|
87
|
+
// ─────────────────────────────────────────────
|
|
88
|
+
/** Inject a bootstrap_required hint from server */
|
|
89
|
+
simulateBootstrapHint(hint) {
|
|
90
|
+
this.emit('bootstrap_required', hint);
|
|
91
|
+
}
|
|
92
|
+
// ─────────────────────────────────────────────
|
|
93
|
+
// Test control: presence
|
|
94
|
+
// ─────────────────────────────────────────────
|
|
95
|
+
/** Inject a presence update */
|
|
96
|
+
simulatePresenceUpdate(data) {
|
|
97
|
+
this.emit('presence_update', data);
|
|
98
|
+
}
|
|
99
|
+
// ─────────────────────────────────────────────
|
|
100
|
+
// Assertions
|
|
101
|
+
// ─────────────────────────────────────────────
|
|
102
|
+
/** Get all events of a specific type */
|
|
103
|
+
getEvents(type) {
|
|
104
|
+
return this.emittedEvents.filter((e) => e.type === type).map((e) => e.data);
|
|
105
|
+
}
|
|
106
|
+
/** Check if a specific event was emitted */
|
|
107
|
+
hasEmitted(type) {
|
|
108
|
+
return this.emittedEvents.some((e) => e.type === type);
|
|
109
|
+
}
|
|
110
|
+
/** Reset all state */
|
|
111
|
+
reset() {
|
|
112
|
+
this._connected = false;
|
|
113
|
+
this._sessionError = false;
|
|
114
|
+
this._listeners.clear();
|
|
115
|
+
this.emittedEvents.length = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OptimisticEchoTracker — receive-layer reconciliation primitive.
|
|
3
|
+
*
|
|
4
|
+
* Tracks the set of transaction ids the client has applied locally
|
|
5
|
+
* but the server has not yet confirmed. When a sync delta arrives
|
|
6
|
+
* carrying a `transactionId` the tracker recognizes, the pool
|
|
7
|
+
* mutation is suppressed (the optimistic write already reflects it).
|
|
8
|
+
* Drained when the matching delta echo lands or when the originating
|
|
9
|
+
* transaction is rolled back.
|
|
10
|
+
*
|
|
11
|
+
* Architectural framing: see
|
|
12
|
+
* `apps/sync-server/docs/OPTIMISTIC_RECONCILIATION.md`. This is the
|
|
13
|
+
* "discriminator at the apply layer" the doc names — without it the
|
|
14
|
+
* authoritative-layer apply path blindly re-applies confirmations on
|
|
15
|
+
* top of whatever optimistic state has since diverged, producing the
|
|
16
|
+
* chart-delete flicker.
|
|
17
|
+
*
|
|
18
|
+
* Bounded by `maxSize` to defend against runaway growth from a
|
|
19
|
+
* pathological "transactions never confirmed and never rolled back"
|
|
20
|
+
* loop. When the bound is hit, the OLDEST id is evicted (FIFO via
|
|
21
|
+
* insertion-ordered Map). Eviction means a future echo of that
|
|
22
|
+
* transaction will be applied as a foreign mutation — no correctness
|
|
23
|
+
* risk for the originating tab (the pool already reflects the local
|
|
24
|
+
* write); the worst case is a single redundant pool re-set.
|
|
25
|
+
*/
|
|
26
|
+
export interface OptimisticEchoTrackerOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Hard upper bound on tracked ids. FIFO eviction beyond this. The
|
|
29
|
+
* default of 10_000 covers a 200-tab user mid-bulk-edit (each tab
|
|
30
|
+
* tracking dozens of unconfirmed transactions) with two orders of
|
|
31
|
+
* magnitude of headroom.
|
|
32
|
+
*/
|
|
33
|
+
maxSize?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface OptimisticEchoMetrics {
|
|
36
|
+
/** Total ids currently tracked. */
|
|
37
|
+
size: number;
|
|
38
|
+
/** Cumulative ids ever added since construction. */
|
|
39
|
+
totalAdded: number;
|
|
40
|
+
/** Cumulative successful echo matches (delta arrived → drained). */
|
|
41
|
+
hits: number;
|
|
42
|
+
/** Cumulative explicit rollback drains (transaction never made it). */
|
|
43
|
+
rollbacks: number;
|
|
44
|
+
/** Cumulative ids evicted due to maxSize pressure. */
|
|
45
|
+
evictions: number;
|
|
46
|
+
}
|
|
47
|
+
export declare class OptimisticEchoTracker {
|
|
48
|
+
private readonly ids;
|
|
49
|
+
private readonly maxSize;
|
|
50
|
+
private _totalAdded;
|
|
51
|
+
private _hits;
|
|
52
|
+
private _rollbacks;
|
|
53
|
+
private _evictions;
|
|
54
|
+
constructor(options?: OptimisticEchoTrackerOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Mark a transaction as locally-applied. The next sync delta with a
|
|
57
|
+
* matching `transactionId` will be recognized as the server's
|
|
58
|
+
* confirmation of this same write. Idempotent — repeated calls with
|
|
59
|
+
* the same id are no-ops.
|
|
60
|
+
*/
|
|
61
|
+
markPending(transactionId: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* If the id is currently tracked, drain it and return true (signal
|
|
64
|
+
* to the caller: this is an echo, skip the pool mutation).
|
|
65
|
+
* Otherwise return false (foreign mutation, apply normally).
|
|
66
|
+
*
|
|
67
|
+
* Combined check+drain into one method to make the receive-path
|
|
68
|
+
* idiom hard to misuse: a separate `has` then `drain` would race
|
|
69
|
+
* if multiple deltas with the same id arrived in the same batch.
|
|
70
|
+
*/
|
|
71
|
+
consumeEcho(transactionId: string | null | undefined): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Drain on rollback. The transaction was cancelled before a server
|
|
74
|
+
* confirmation arrived — no echo will ever come, so the pending
|
|
75
|
+
* entry would otherwise leak. Counts as a separate metric category
|
|
76
|
+
* so a spike of `rollbacks` (vs `hits`) signals network or
|
|
77
|
+
* server-side health issues.
|
|
78
|
+
*/
|
|
79
|
+
drainOnRollback(transactionId: string): void;
|
|
80
|
+
getMetrics(): Readonly<OptimisticEchoMetrics>;
|
|
81
|
+
clear(): void;
|
|
82
|
+
}
|