@powersync/service-core 1.22.0 → 1.23.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 +17 -0
- package/dist/api/RouteAPI.d.ts +2 -2
- package/dist/api/diagnostics.d.ts +12 -1
- package/dist/api/diagnostics.js +34 -22
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +1 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +6 -2
- package/dist/modules/AbstractModule.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +2 -2
- package/dist/replication/AbstractReplicator.js +24 -23
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +3 -3
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +12 -12
- package/dist/routes/endpoints/admin.d.ts +24 -24
- package/dist/routes/endpoints/admin.js +44 -34
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -1
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +22 -13
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +1 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/runner/teardown.js +8 -8
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +0 -6
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +22 -27
- package/dist/storage/BucketStorageFactory.js +4 -18
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
- package/dist/storage/ParsedSyncConfigSet.js +2 -0
- package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/PersistedReplicationStream.d.ts +32 -0
- package/dist/storage/PersistedReplicationStream.js +26 -0
- package/dist/storage/PersistedReplicationStream.js.map +1 -0
- package/dist/storage/PersistedSyncConfigContent.d.ts +60 -0
- package/dist/storage/PersistedSyncConfigContent.js +123 -0
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
- package/dist/storage/PersistedSyncConfigStatus.js +2 -0
- package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
- package/dist/storage/SourceTable.d.ts +9 -3
- package/dist/storage/SourceTable.js +13 -3
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +59 -16
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +117 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +277 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +62 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
- package/dist/storage/storage-index.d.ts +8 -1
- package/dist/storage/storage-index.js +8 -1
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/sync.js +5 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/lsn.d.ts +4 -0
- package/dist/util/lsn.js +10 -0
- package/dist/util/lsn.js.map +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +2 -2
- package/src/api/diagnostics.ts +43 -26
- package/src/entry/commands/compact-action.ts +1 -1
- package/src/modules/AbstractModule.ts +6 -2
- package/src/replication/AbstractReplicator.ts +27 -26
- package/src/replication/ReplicationModule.ts +2 -3
- package/src/routes/endpoints/admin.ts +70 -49
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +23 -13
- package/src/routes/endpoints/sync-stream.ts +1 -1
- package/src/runner/teardown.ts +8 -8
- package/src/storage/BucketStorageBatch.ts +0 -7
- package/src/storage/BucketStorageFactory.ts +30 -39
- package/src/storage/ParsedSyncConfigSet.ts +25 -0
- package/src/storage/PersistedReplicationStream.ts +55 -0
- package/src/storage/PersistedSyncConfigContent.ts +188 -0
- package/src/storage/PersistedSyncConfigStatus.ts +14 -0
- package/src/storage/SourceTable.ts +20 -3
- package/src/storage/SyncRulesBucketStorage.ts +64 -16
- package/src/storage/implementation/BucketDefinitionMapping.ts +436 -0
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +109 -0
- package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
- package/src/storage/storage-index.ts +9 -1
- package/src/sync/sync.ts +5 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/types.ts +1 -1
- package/src/util/lsn.ts +9 -0
- package/test/src/diagnostics.test.ts +61 -34
- package/test/src/module-loader.test.ts +33 -1
- package/test/src/routes/admin.test.ts +180 -18
- package/test/src/routes/mocks.ts +6 -2
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
- package/dist/storage/PersistedSyncRulesContent.js +0 -100
- package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
- package/src/storage/PersistedSyncRulesContent.ts +0 -174
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DiagnosticsOptions, getSyncRulesStatus } from '@/api/diagnostics.js';
|
|
2
2
|
import { RouteAPI, SlotWalBudgetInfo } from '@/api/RouteAPI.js';
|
|
3
|
-
import {
|
|
3
|
+
import { ParsedSyncConfigSet, storage } from '@/index.js';
|
|
4
4
|
import { SqlSyncRules } from '@powersync/service-sync-rules';
|
|
5
5
|
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
@@ -13,53 +13,56 @@ bucket_definitions:
|
|
|
13
13
|
- SELECT id FROM test_table
|
|
14
14
|
`;
|
|
15
15
|
|
|
16
|
-
function makeSyncRulesContent(overrides?: {
|
|
16
|
+
function makeSyncRulesContent(overrides?: {
|
|
17
|
+
slot_name?: string;
|
|
18
|
+
status?: storage.PersistedSyncConfigStatus;
|
|
19
|
+
}): storage.PersistedSyncConfigContent {
|
|
17
20
|
// We don't implement the entire interface correctly here - just enough to test the diagnostics logic.
|
|
18
21
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
replicationStreamId: 1,
|
|
23
|
+
syncConfigId: null,
|
|
24
|
+
replicationStreamName: overrides?.slot_name ?? 'test_slot',
|
|
21
25
|
sync_rules_content: MINIMAL_SYNC_RULES,
|
|
22
26
|
compiled_plan: null,
|
|
23
|
-
active: true,
|
|
24
27
|
storageVersion: 1,
|
|
25
|
-
last_checkpoint_lsn: 'some_lsn',
|
|
26
|
-
last_fatal_error: null,
|
|
27
|
-
last_fatal_error_ts: null,
|
|
28
|
-
last_keepalive_ts: new Date(),
|
|
29
|
-
last_checkpoint_ts: new Date(),
|
|
30
28
|
parsed(options?: any) {
|
|
31
29
|
const syncRules = SqlSyncRules.fromYaml(MINIMAL_SYNC_RULES, {
|
|
32
30
|
...options,
|
|
33
31
|
defaultSchema: 'public'
|
|
34
32
|
});
|
|
35
33
|
return {
|
|
36
|
-
|
|
37
|
-
} as
|
|
34
|
+
syncConfigs: [syncRules]
|
|
35
|
+
} as ParsedSyncConfigSet;
|
|
38
36
|
},
|
|
39
|
-
lock() {
|
|
40
|
-
throw new Error('Not implemented in mock');
|
|
41
|
-
},
|
|
42
|
-
current_lock: null,
|
|
43
37
|
logger: null as any,
|
|
44
38
|
asUpdateOptions: null as any,
|
|
45
|
-
getStorageConfig: null as any
|
|
46
|
-
|
|
39
|
+
getStorageConfig: null as any,
|
|
40
|
+
async getSyncConfigStatus() {
|
|
41
|
+
return (
|
|
42
|
+
overrides?.status ?? {
|
|
43
|
+
id: '1',
|
|
44
|
+
replicationStreamId: 1,
|
|
45
|
+
state: storage.SyncRuleState.ACTIVE,
|
|
46
|
+
last_checkpoint_lsn: 'some_lsn',
|
|
47
|
+
last_fatal_error: null,
|
|
48
|
+
last_fatal_error_ts: null,
|
|
49
|
+
last_keepalive_ts: new Date(),
|
|
50
|
+
last_checkpoint_ts: new Date()
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
} as storage.PersistedSyncConfigContent;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
function
|
|
57
|
+
function makeSystemStorage() {
|
|
50
58
|
return {
|
|
51
|
-
|
|
59
|
+
async getStatus() {
|
|
52
60
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
snapshot_done: true,
|
|
56
|
-
checkpoint_lsn: 'some_lsn',
|
|
57
|
-
active: true
|
|
58
|
-
};
|
|
59
|
-
}
|
|
61
|
+
snapshotDone: true,
|
|
62
|
+
resumeLsn: 'some_lsn'
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
|
-
} as
|
|
65
|
+
} as storage.SyncRulesBucketStorage;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function makeRouteAPI(walBudget?: SlotWalBudgetInfo | undefined): RouteAPI {
|
|
@@ -92,7 +95,8 @@ function makeRouteAPI(walBudget?: SlotWalBudgetInfo | undefined): RouteAPI {
|
|
|
92
95
|
const OPTIONS: DiagnosticsOptions = {
|
|
93
96
|
live_status: true,
|
|
94
97
|
check_connection: true,
|
|
95
|
-
include_content: false
|
|
98
|
+
include_content: false,
|
|
99
|
+
active: true
|
|
96
100
|
};
|
|
97
101
|
|
|
98
102
|
describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
@@ -102,7 +106,7 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
|
102
106
|
safe_wal_size: 4 * GB,
|
|
103
107
|
max_slot_wal_keep_size: 10 * GB
|
|
104
108
|
});
|
|
105
|
-
const result = await getSyncRulesStatus(
|
|
109
|
+
const result = await getSyncRulesStatus(api, makeSyncRulesContent(), OPTIONS, makeSystemStorage());
|
|
106
110
|
const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
|
|
107
111
|
expect(walWarnings).toHaveLength(1);
|
|
108
112
|
expect(walWarnings[0].level).toBe('warning');
|
|
@@ -115,7 +119,7 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
|
115
119
|
safe_wal_size: 8 * GB,
|
|
116
120
|
max_slot_wal_keep_size: 10 * GB
|
|
117
121
|
});
|
|
118
|
-
const result = await getSyncRulesStatus(
|
|
122
|
+
const result = await getSyncRulesStatus(api, makeSyncRulesContent(), OPTIONS, makeSystemStorage());
|
|
119
123
|
const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
|
|
120
124
|
expect(walWarnings).toHaveLength(0);
|
|
121
125
|
});
|
|
@@ -126,7 +130,7 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
|
126
130
|
safe_wal_size: -2.4 * GB,
|
|
127
131
|
max_slot_wal_keep_size: 1 * 1024 * 1024 // 1MB
|
|
128
132
|
});
|
|
129
|
-
const result = await getSyncRulesStatus(
|
|
133
|
+
const result = await getSyncRulesStatus(api, makeSyncRulesContent(), OPTIONS, makeSystemStorage());
|
|
130
134
|
const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
|
|
131
135
|
expect(walWarnings).toHaveLength(1);
|
|
132
136
|
expect(walWarnings[0].message).toContain('0%');
|
|
@@ -137,7 +141,7 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
|
137
141
|
const api = makeRouteAPI({
|
|
138
142
|
wal_status: 'lost'
|
|
139
143
|
});
|
|
140
|
-
const result = await getSyncRulesStatus(
|
|
144
|
+
const result = await getSyncRulesStatus(api, makeSyncRulesContent(), OPTIONS, makeSystemStorage());
|
|
141
145
|
const walErrors = result!.errors.filter(
|
|
142
146
|
(e) => e.message.includes('WAL budget') || e.message.includes('PSYNC_S1146')
|
|
143
147
|
);
|
|
@@ -146,10 +150,33 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
|
|
|
146
150
|
|
|
147
151
|
test('no WAL error when getSlotWalBudget is not defined', async () => {
|
|
148
152
|
const api = makeRouteAPI();
|
|
149
|
-
const result = await getSyncRulesStatus(
|
|
153
|
+
const result = await getSyncRulesStatus(api, makeSyncRulesContent(), OPTIONS, makeSystemStorage());
|
|
150
154
|
const walErrors = result!.errors.filter(
|
|
151
155
|
(e) => e.message.includes('WAL budget') || e.message.includes('PSYNC_S1146')
|
|
152
156
|
);
|
|
153
157
|
expect(walErrors).toHaveLength(0);
|
|
154
158
|
});
|
|
159
|
+
|
|
160
|
+
test('uses sync config status for status-derived diagnostics fields', async () => {
|
|
161
|
+
const configStatus: storage.PersistedSyncConfigStatus = {
|
|
162
|
+
id: 'config-a',
|
|
163
|
+
replicationStreamId: 1,
|
|
164
|
+
state: storage.SyncRuleState.ACTIVE,
|
|
165
|
+
last_checkpoint_lsn: 'config_lsn',
|
|
166
|
+
last_fatal_error: 'config failed',
|
|
167
|
+
last_fatal_error_ts: new Date('2026-01-01T00:00:00.000Z'),
|
|
168
|
+
last_keepalive_ts: new Date('2026-01-01T00:01:00.000Z'),
|
|
169
|
+
last_checkpoint_ts: new Date('2026-01-01T00:02:00.000Z')
|
|
170
|
+
};
|
|
171
|
+
const result = await getSyncRulesStatus(
|
|
172
|
+
makeRouteAPI(),
|
|
173
|
+
makeSyncRulesContent({ status: configStatus }),
|
|
174
|
+
OPTIONS,
|
|
175
|
+
makeSystemStorage()
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result!.connections[0].last_checkpoint_ts).toBe('2026-01-01T00:02:00.000Z');
|
|
179
|
+
expect(result!.connections[0].last_keepalive_ts).toBe('2026-01-01T00:01:00.000Z');
|
|
180
|
+
expect(result!.errors.some((error) => error.message == 'config failed')).toBe(true);
|
|
181
|
+
});
|
|
155
182
|
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AbstractModule,
|
|
3
|
+
loadModules,
|
|
4
|
+
ModuleManager,
|
|
5
|
+
ServiceContextContainer,
|
|
6
|
+
storage,
|
|
7
|
+
TearDownOptions
|
|
8
|
+
} from '@/index.js';
|
|
2
9
|
import { describe, expect, it } from 'vitest';
|
|
3
10
|
|
|
4
11
|
interface MockConfig {
|
|
@@ -27,6 +34,19 @@ class MockPostgresStorageModule extends AbstractModule {
|
|
|
27
34
|
async initialize(context: ServiceContextContainer): Promise<void> {}
|
|
28
35
|
async teardown(options: TearDownOptions): Promise<void> {}
|
|
29
36
|
}
|
|
37
|
+
class RecordingModule extends AbstractModule {
|
|
38
|
+
teardownOptions: TearDownOptions | undefined;
|
|
39
|
+
|
|
40
|
+
constructor(name: string) {
|
|
41
|
+
super({ name });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async initialize(context: ServiceContextContainer): Promise<void> {}
|
|
45
|
+
|
|
46
|
+
async teardown(options: TearDownOptions): Promise<void> {
|
|
47
|
+
this.teardownOptions = options;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
30
50
|
const mockLoaders = {
|
|
31
51
|
connection: {
|
|
32
52
|
mysql: async () => {
|
|
@@ -99,4 +119,16 @@ describe('module loader', () => {
|
|
|
99
119
|
|
|
100
120
|
await expect(loadModules(config as any, loaders)).rejects.toThrowError('Failed to load MySQL module');
|
|
101
121
|
});
|
|
122
|
+
|
|
123
|
+
it('passes replication streams through teardown options', async () => {
|
|
124
|
+
const module = new RecordingModule('RecordingModule');
|
|
125
|
+
const moduleManager = new ModuleManager();
|
|
126
|
+
const replicationStreams = [{ replicationStreamId: 1 }] as storage.PersistedReplicationStream[];
|
|
127
|
+
|
|
128
|
+
moduleManager.register([module]);
|
|
129
|
+
await moduleManager.tearDown({ replicationStreams, syncRules: replicationStreams });
|
|
130
|
+
|
|
131
|
+
expect(module.teardownOptions?.replicationStreams).toBe(replicationStreams);
|
|
132
|
+
expect(module.teardownOptions?.syncRules).toBe(replicationStreams);
|
|
133
|
+
});
|
|
102
134
|
});
|
|
@@ -1,27 +1,90 @@
|
|
|
1
|
-
import { BasicRouterRequest, Context, JwtPayload } from '@/index.js';
|
|
1
|
+
import { BasicRouterRequest, Context, JwtPayload, ParsedSyncConfigSet, storage } from '@/index.js';
|
|
2
2
|
import { logger } from '@powersync/lib-services-framework';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { SqlSyncRules } from '@powersync/service-sync-rules';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { diagnostics, reprocess, validate } from '../../../src/routes/endpoints/admin.js';
|
|
5
6
|
import { mockServiceContext } from './mocks.js';
|
|
6
7
|
|
|
7
8
|
describe('admin routes', () => {
|
|
9
|
+
const request: BasicRouterRequest = {
|
|
10
|
+
headers: {},
|
|
11
|
+
hostname: '',
|
|
12
|
+
protocol: 'http'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function makeContext(activeBucketStorage?: any): Context {
|
|
16
|
+
const service_context = mockServiceContext(null);
|
|
17
|
+
if (activeBucketStorage != null) {
|
|
18
|
+
(service_context.storageEngine as any).activeBucketStorage = activeBucketStorage;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
logger: logger,
|
|
23
|
+
service_context,
|
|
24
|
+
token_payload: new JwtPayload({
|
|
25
|
+
sub: '',
|
|
26
|
+
exp: 0,
|
|
27
|
+
iat: 0
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeSyncConfigContent(options: {
|
|
33
|
+
id?: number;
|
|
34
|
+
syncConfigId?: string;
|
|
35
|
+
active?: boolean;
|
|
36
|
+
content?: string;
|
|
37
|
+
}): storage.PersistedSyncConfigContent {
|
|
38
|
+
const id = options.id ?? 1;
|
|
39
|
+
const syncConfigId = options.syncConfigId ?? String(id);
|
|
40
|
+
const active = options.active ?? true;
|
|
41
|
+
const state = active ? storage.SyncRuleState.ACTIVE : storage.SyncRuleState.PROCESSING;
|
|
42
|
+
const lastKeepaliveTs = new Date('2026-01-01T00:00:00.000Z');
|
|
43
|
+
const lastCheckpointTs = new Date('2026-01-01T00:00:00.000Z');
|
|
44
|
+
const content = {
|
|
45
|
+
replicationStreamId: id,
|
|
46
|
+
syncConfigId,
|
|
47
|
+
replicationStreamName: `slot_${id}`,
|
|
48
|
+
sync_rules_content:
|
|
49
|
+
options.content ??
|
|
50
|
+
`
|
|
51
|
+
bucket_definitions:
|
|
52
|
+
global:
|
|
53
|
+
data:
|
|
54
|
+
- SELECT id FROM test
|
|
55
|
+
`,
|
|
56
|
+
compiled_plan: null,
|
|
57
|
+
storageVersion: storage.LEGACY_STORAGE_VERSION,
|
|
58
|
+
parsed(options?: any) {
|
|
59
|
+
const syncRules = SqlSyncRules.fromYaml(content.sync_rules_content, {
|
|
60
|
+
...options,
|
|
61
|
+
defaultSchema: 'public'
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
syncConfigs: [syncRules]
|
|
65
|
+
} as ParsedSyncConfigSet;
|
|
66
|
+
},
|
|
67
|
+
asUpdateOptions: vi.fn(),
|
|
68
|
+
getStorageConfig: vi.fn(),
|
|
69
|
+
async getSyncConfigStatus() {
|
|
70
|
+
return {
|
|
71
|
+
id: syncConfigId,
|
|
72
|
+
replicationStreamId: id,
|
|
73
|
+
state,
|
|
74
|
+
last_checkpoint_lsn: null,
|
|
75
|
+
last_fatal_error: null,
|
|
76
|
+
last_fatal_error_ts: null,
|
|
77
|
+
last_keepalive_ts: lastKeepaliveTs,
|
|
78
|
+
last_checkpoint_ts: lastCheckpointTs
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return content as unknown as storage.PersistedSyncConfigContent;
|
|
83
|
+
}
|
|
84
|
+
|
|
8
85
|
describe('validate', () => {
|
|
9
86
|
it('reports errors with source location', async () => {
|
|
10
|
-
const context
|
|
11
|
-
logger: logger,
|
|
12
|
-
service_context: mockServiceContext(null),
|
|
13
|
-
token_payload: new JwtPayload({
|
|
14
|
-
sub: '',
|
|
15
|
-
exp: 0,
|
|
16
|
-
iat: 0
|
|
17
|
-
})
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const request: BasicRouterRequest = {
|
|
21
|
-
headers: {},
|
|
22
|
-
hostname: '',
|
|
23
|
-
protocol: 'http'
|
|
24
|
-
};
|
|
87
|
+
const context = makeContext();
|
|
25
88
|
|
|
26
89
|
const response = await validate.handler({
|
|
27
90
|
context,
|
|
@@ -45,4 +108,103 @@ bucket_definitions:
|
|
|
45
108
|
]);
|
|
46
109
|
});
|
|
47
110
|
});
|
|
111
|
+
|
|
112
|
+
describe('diagnostics', () => {
|
|
113
|
+
it('returns deploying config status', async () => {
|
|
114
|
+
const active = makeSyncConfigContent({ id: 1, syncConfigId: 'active-config' });
|
|
115
|
+
const deploying = makeSyncConfigContent({ id: 2, syncConfigId: 'deploying-config', active: false });
|
|
116
|
+
const getInstance = vi.fn(() => ({
|
|
117
|
+
async getStatus() {
|
|
118
|
+
return {
|
|
119
|
+
snapshotDone: false,
|
|
120
|
+
resumeLsn: null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
const activeBucketStorage = {
|
|
125
|
+
getActiveSyncConfig: vi.fn(async () => ({
|
|
126
|
+
content: active,
|
|
127
|
+
replicationStream: {},
|
|
128
|
+
storage: getInstance()
|
|
129
|
+
})),
|
|
130
|
+
getDeployingSyncConfig: vi.fn(async () => ({
|
|
131
|
+
content: deploying,
|
|
132
|
+
replicationStream: {},
|
|
133
|
+
storage: getInstance()
|
|
134
|
+
}))
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const response = await diagnostics.handler({
|
|
138
|
+
context: makeContext(activeBucketStorage),
|
|
139
|
+
params: {},
|
|
140
|
+
request
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(response.deploying_sync_rules?.connections[0].slot_name).toBe('slot_2');
|
|
144
|
+
expect(response.active_sync_rules?.connections[0].slot_name).toBe('slot_1');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('reprocess', () => {
|
|
149
|
+
it('reprocesses the active sync config', async () => {
|
|
150
|
+
const active = makeSyncConfigContent({ id: 7, syncConfigId: 'active-config' });
|
|
151
|
+
const updateSyncRules = vi.fn(async () => ({
|
|
152
|
+
replicationStreamId: 8,
|
|
153
|
+
replicationStreamName: 'new_slot',
|
|
154
|
+
state: storage.SyncRuleState.PROCESSING,
|
|
155
|
+
storageVersion: storage.LEGACY_STORAGE_VERSION,
|
|
156
|
+
replicationJobId: '8',
|
|
157
|
+
current_lock: null
|
|
158
|
+
}));
|
|
159
|
+
const activeBucketStorage = {
|
|
160
|
+
getDeployingSyncConfig: vi.fn(async () => null),
|
|
161
|
+
getActiveSyncConfig: vi.fn(async () => ({
|
|
162
|
+
content: active,
|
|
163
|
+
replicationStream: {},
|
|
164
|
+
storage: {}
|
|
165
|
+
})),
|
|
166
|
+
getSyncConfigContent: vi.fn(),
|
|
167
|
+
updateSyncRules
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const response = await reprocess.handler({
|
|
171
|
+
context: makeContext(activeBucketStorage),
|
|
172
|
+
params: {},
|
|
173
|
+
request
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(activeBucketStorage.getActiveSyncConfig).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(activeBucketStorage.getSyncConfigContent).not.toHaveBeenCalled();
|
|
178
|
+
expect(updateSyncRules).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(response.connections[0].slot_name).toBe('new_slot');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects reprocess while a sync config is deploying', async () => {
|
|
183
|
+
const activeBucketStorage = {
|
|
184
|
+
getDeployingSyncConfig: vi.fn(async () => ({
|
|
185
|
+
content: makeSyncConfigContent({ id: 2, active: false }),
|
|
186
|
+
replicationStream: {},
|
|
187
|
+
storage: {}
|
|
188
|
+
})),
|
|
189
|
+
getActiveSyncConfig: vi.fn(),
|
|
190
|
+
updateSyncRules: vi.fn()
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
reprocess.handler({
|
|
195
|
+
context: makeContext(activeBucketStorage),
|
|
196
|
+
params: {},
|
|
197
|
+
request
|
|
198
|
+
})
|
|
199
|
+
).rejects.toMatchObject({
|
|
200
|
+
errorData: {
|
|
201
|
+
status: 409,
|
|
202
|
+
code: 'PSYNC_S4106',
|
|
203
|
+
description: 'Busy processing sync config - cannot reprocess'
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
expect(activeBucketStorage.getActiveSyncConfig).not.toHaveBeenCalled();
|
|
207
|
+
expect(activeBucketStorage.updateSyncRules).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
48
210
|
});
|
package/test/src/routes/mocks.ts
CHANGED
|
@@ -18,8 +18,12 @@ export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | nu
|
|
|
18
18
|
|
|
19
19
|
const storageEngine: StorageEngine = {
|
|
20
20
|
activeBucketStorage: {
|
|
21
|
-
async
|
|
22
|
-
return storage
|
|
21
|
+
async getActiveSyncConfig() {
|
|
22
|
+
return storage == null
|
|
23
|
+
? null
|
|
24
|
+
: {
|
|
25
|
+
storage
|
|
26
|
+
};
|
|
23
27
|
}
|
|
24
28
|
} as Partial<BucketStorageFactory>
|
|
25
29
|
} as any;
|
package/tsconfig.json
CHANGED