@powersync/service-core 0.2.2 → 0.4.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 +31 -0
- package/dist/api/diagnostics.js +2 -2
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/api/schema.js.map +1 -1
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/JwtPayload.d.ts +6 -2
- package/dist/auth/KeySpec.js.map +1 -1
- package/dist/auth/KeyStore.js +3 -9
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/LeakyBucket.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/SupabaseKeyCollector.js.map +1 -1
- package/dist/db/mongo.js.map +1 -1
- package/dist/entry/cli-entry.js +2 -2
- package/dist/entry/cli-entry.js.map +1 -1
- package/dist/entry/commands/config-command.js.map +1 -1
- package/dist/entry/commands/migrate-action.js +12 -4
- package/dist/entry/commands/migrate-action.js.map +1 -1
- package/dist/entry/commands/start-action.js.map +1 -1
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/locks/LockManager.d.ts +10 -0
- package/dist/locks/LockManager.js +7 -0
- package/dist/locks/LockManager.js.map +1 -0
- package/dist/locks/MongoLocks.d.ts +36 -0
- package/dist/locks/MongoLocks.js +81 -0
- package/dist/locks/MongoLocks.js.map +1 -0
- package/dist/locks/locks-index.d.ts +2 -0
- package/dist/locks/locks-index.js +3 -0
- package/dist/locks/locks-index.js.map +1 -0
- package/dist/metrics/Metrics.js +6 -6
- package/dist/metrics/Metrics.js.map +1 -1
- package/dist/migrations/db/migrations/1684951997326-init.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js.map +1 -1
- package/dist/migrations/definitions.d.ts +18 -0
- package/dist/migrations/definitions.js +6 -0
- package/dist/migrations/definitions.js.map +1 -0
- package/dist/migrations/executor.d.ts +16 -0
- package/dist/migrations/executor.js +64 -0
- package/dist/migrations/executor.js.map +1 -0
- package/dist/migrations/migrations-index.d.ts +3 -0
- package/dist/migrations/migrations-index.js +4 -0
- package/dist/migrations/migrations-index.js.map +1 -0
- package/dist/migrations/migrations.d.ts +1 -1
- package/dist/migrations/migrations.js +12 -8
- package/dist/migrations/migrations.js.map +1 -1
- package/dist/migrations/store/migration-store.d.ts +11 -0
- package/dist/migrations/store/migration-store.js +46 -0
- package/dist/migrations/store/migration-store.js.map +1 -0
- package/dist/replication/ErrorRateLimiter.js.map +1 -1
- package/dist/replication/PgRelation.js.map +1 -1
- package/dist/replication/WalConnection.js.map +1 -1
- package/dist/replication/WalStream.d.ts +0 -1
- package/dist/replication/WalStream.js +21 -25
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamManager.js +12 -13
- package/dist/replication/WalStreamManager.js.map +1 -1
- package/dist/replication/WalStreamRunner.js +8 -8
- package/dist/replication/WalStreamRunner.js.map +1 -1
- package/dist/replication/util.js.map +1 -1
- package/dist/routes/auth.d.ts +8 -10
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/endpoints/admin.d.ts +1011 -0
- package/dist/routes/{admin.js → endpoints/admin.js} +33 -18
- package/dist/routes/endpoints/admin.js.map +1 -0
- package/dist/routes/endpoints/checkpointing.d.ts +76 -0
- package/dist/routes/endpoints/checkpointing.js +36 -0
- package/dist/routes/endpoints/checkpointing.js.map +1 -0
- package/dist/routes/endpoints/dev.d.ts +312 -0
- package/dist/routes/{dev.js → endpoints/dev.js} +25 -16
- package/dist/routes/endpoints/dev.js.map +1 -0
- package/dist/routes/endpoints/route-endpoints-index.d.ts +6 -0
- package/dist/routes/endpoints/route-endpoints-index.js +7 -0
- package/dist/routes/endpoints/route-endpoints-index.js.map +1 -0
- package/dist/routes/endpoints/socket-route.d.ts +2 -0
- package/dist/routes/{socket-route.js → endpoints/socket-route.js} +12 -12
- package/dist/routes/endpoints/socket-route.js.map +1 -0
- package/dist/routes/endpoints/sync-rules.d.ts +174 -0
- package/dist/routes/{sync-rules.js → endpoints/sync-rules.js} +44 -24
- package/dist/routes/endpoints/sync-rules.js.map +1 -0
- package/dist/routes/endpoints/sync-stream.d.ts +132 -0
- package/dist/routes/{sync-stream.js → endpoints/sync-stream.js} +28 -19
- package/dist/routes/endpoints/sync-stream.js.map +1 -0
- package/dist/routes/hooks.d.ts +10 -0
- package/dist/routes/hooks.js +31 -0
- package/dist/routes/hooks.js.map +1 -0
- package/dist/routes/route-register.d.ts +10 -0
- package/dist/routes/route-register.js +87 -0
- package/dist/routes/route-register.js.map +1 -0
- package/dist/routes/router.d.ts +16 -4
- package/dist/routes/router.js +6 -1
- package/dist/routes/router.js.map +1 -1
- package/dist/routes/routes-index.d.ts +5 -3
- package/dist/routes/routes-index.js +5 -3
- package/dist/routes/routes-index.js.map +1 -1
- package/dist/runner/teardown.js +9 -9
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +3 -0
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/MongoBucketStorage.js +5 -5
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/mongo/MongoBucketBatch.js +23 -18
- package/dist/storage/mongo/MongoBucketBatch.js.map +1 -1
- package/dist/storage/mongo/MongoIdSequence.js.map +1 -1
- package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/mongo/MongoSyncRulesLock.js +3 -3
- package/dist/storage/mongo/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/mongo/OperationBatch.js.map +1 -1
- package/dist/storage/mongo/PersistedBatch.js +2 -2
- package/dist/storage/mongo/PersistedBatch.js.map +1 -1
- package/dist/storage/mongo/db.d.ts +2 -2
- package/dist/storage/mongo/db.js.map +1 -1
- package/dist/storage/mongo/util.js.map +1 -1
- package/dist/sync/BroadcastIterable.js.map +1 -1
- package/dist/sync/LastValueSink.js.map +1 -1
- package/dist/sync/merge.js.map +1 -1
- package/dist/sync/safeRace.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js +5 -5
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/system/CorePowerSyncSystem.d.ts +12 -7
- package/dist/system/CorePowerSyncSystem.js +26 -2
- package/dist/system/CorePowerSyncSystem.js.map +1 -1
- package/dist/system/system-index.d.ts +1 -0
- package/dist/system/system-index.js +2 -0
- package/dist/system/system-index.js.map +1 -0
- package/dist/util/Mutex.js.map +1 -1
- package/dist/util/PgManager.js.map +1 -1
- package/dist/util/alerting.d.ts +0 -2
- package/dist/util/alerting.js +0 -6
- package/dist/util/alerting.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +3 -3
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/filesystem-config-collector.js +7 -5
- package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +4 -4
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
- package/dist/util/config.js.map +1 -1
- package/dist/util/env.d.ts +1 -2
- package/dist/util/env.js +3 -2
- package/dist/util/env.js.map +1 -1
- package/dist/util/memory-tracking.js +2 -2
- package/dist/util/memory-tracking.js.map +1 -1
- package/dist/util/migration_lib.js.map +1 -1
- package/dist/util/pgwire_utils.js +2 -2
- package/dist/util/pgwire_utils.js.map +1 -1
- package/dist/util/populate_test_data.js.map +1 -1
- package/dist/util/secs.js.map +1 -1
- package/dist/util/utils.js +4 -4
- package/dist/util/utils.js.map +1 -1
- package/package.json +13 -10
- package/src/api/diagnostics.ts +5 -5
- package/src/api/schema.ts +1 -1
- package/src/auth/JwtPayload.ts +6 -2
- package/src/auth/KeyStore.ts +3 -9
- package/src/entry/cli-entry.ts +3 -4
- package/src/entry/commands/config-command.ts +1 -1
- package/src/entry/commands/migrate-action.ts +14 -6
- package/src/entry/commands/start-action.ts +1 -1
- package/src/entry/commands/teardown-action.ts +1 -1
- package/src/index.ts +5 -2
- package/src/locks/LockManager.ts +16 -0
- package/src/locks/MongoLocks.ts +142 -0
- package/src/locks/locks-index.ts +2 -0
- package/src/metrics/Metrics.ts +8 -8
- package/src/migrations/db/migrations/1684951997326-init.ts +3 -3
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +3 -3
- package/src/migrations/db/migrations/1711543888062-write-checkpoint-index.ts +2 -2
- package/src/migrations/definitions.ts +21 -0
- package/src/migrations/executor.ts +87 -0
- package/src/migrations/migrations-index.ts +3 -0
- package/src/migrations/migrations.ts +15 -11
- package/src/migrations/store/migration-store.ts +63 -0
- package/src/replication/WalConnection.ts +2 -2
- package/src/replication/WalStream.ts +24 -29
- package/src/replication/WalStreamManager.ts +14 -15
- package/src/replication/WalStreamRunner.ts +10 -10
- package/src/replication/util.ts +1 -1
- package/src/routes/auth.ts +22 -16
- package/src/routes/endpoints/admin.ts +237 -0
- package/src/routes/endpoints/checkpointing.ts +41 -0
- package/src/routes/endpoints/dev.ts +199 -0
- package/src/routes/endpoints/route-endpoints-index.ts +6 -0
- package/src/routes/{socket-route.ts → endpoints/socket-route.ts} +13 -16
- package/src/routes/endpoints/sync-rules.ts +227 -0
- package/src/routes/endpoints/sync-stream.ts +98 -0
- package/src/routes/hooks.ts +45 -0
- package/src/routes/route-register.ts +104 -0
- package/src/routes/router.ts +34 -6
- package/src/routes/routes-index.ts +5 -4
- package/src/runner/teardown.ts +9 -9
- package/src/storage/BucketStorage.ts +7 -2
- package/src/storage/ChecksumCache.ts +2 -2
- package/src/storage/MongoBucketStorage.ts +8 -8
- package/src/storage/SourceTable.ts +2 -2
- package/src/storage/mongo/MongoBucketBatch.ts +29 -22
- package/src/storage/mongo/MongoSyncBucketStorage.ts +3 -3
- package/src/storage/mongo/MongoSyncRulesLock.ts +3 -3
- package/src/storage/mongo/OperationBatch.ts +1 -1
- package/src/storage/mongo/PersistedBatch.ts +3 -3
- package/src/storage/mongo/db.ts +3 -4
- package/src/sync/sync.ts +11 -11
- package/src/sync/util.ts +2 -2
- package/src/system/CorePowerSyncSystem.ts +31 -10
- package/src/system/system-index.ts +1 -0
- package/src/util/alerting.ts +0 -8
- package/src/util/config/collectors/config-collector.ts +5 -3
- package/src/util/config/collectors/impl/filesystem-config-collector.ts +8 -6
- package/src/util/config/compound-config-collector.ts +4 -4
- package/src/util/env.ts +4 -2
- package/src/util/memory-tracking.ts +2 -2
- package/src/util/pgwire_utils.ts +3 -3
- package/src/util/utils.ts +5 -5
- package/test/src/auth.test.ts +4 -2
- package/test/src/data_storage.test.ts +181 -19
- package/test/src/env.ts +6 -6
- package/test/src/setup.ts +7 -0
- package/test/src/slow_tests.test.ts +45 -6
- package/test/src/sync.test.ts +6 -5
- package/test/tsconfig.json +1 -1
- package/tsconfig.json +5 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +1 -3
- package/dist/migrations/db/store.d.ts +0 -3
- package/dist/migrations/db/store.js +0 -10
- package/dist/migrations/db/store.js.map +0 -1
- package/dist/routes/admin.d.ts +0 -7
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/checkpointing.d.ts +0 -3
- package/dist/routes/checkpointing.js +0 -30
- package/dist/routes/checkpointing.js.map +0 -1
- package/dist/routes/dev.d.ts +0 -6
- package/dist/routes/dev.js.map +0 -1
- package/dist/routes/route-generators.d.ts +0 -15
- package/dist/routes/route-generators.js +0 -32
- package/dist/routes/route-generators.js.map +0 -1
- package/dist/routes/socket-route.d.ts +0 -2
- package/dist/routes/socket-route.js.map +0 -1
- package/dist/routes/sync-rules.d.ts +0 -6
- package/dist/routes/sync-rules.js.map +0 -1
- package/dist/routes/sync-stream.d.ts +0 -5
- package/dist/routes/sync-stream.js.map +0 -1
- package/src/migrations/db/store.ts +0 -11
- package/src/routes/admin.ts +0 -229
- package/src/routes/checkpointing.ts +0 -38
- package/src/routes/dev.ts +0 -194
- package/src/routes/route-generators.ts +0 -39
- package/src/routes/sync-rules.ts +0 -210
- package/src/routes/sync-stream.ts +0 -95
- package/test/src/sql_functions.test.ts +0 -254
- package/test/src/sql_operators.test.ts +0 -132
- package/test/src/sync_rules.test.ts +0 -1053
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as t from 'ts-codec';
|
|
2
|
+
import { router, schema } from '@powersync/lib-services-framework';
|
|
3
|
+
|
|
4
|
+
import * as util from '../../util/util-index.js';
|
|
5
|
+
import { authUser } from '../auth.js';
|
|
6
|
+
import { routeDefinition } from '../router.js';
|
|
7
|
+
|
|
8
|
+
const WriteCheckpointRequest = t.object({});
|
|
9
|
+
|
|
10
|
+
export const writeCheckpoint = routeDefinition({
|
|
11
|
+
path: '/write-checkpoint.json',
|
|
12
|
+
method: router.HTTPMethod.GET,
|
|
13
|
+
authorize: authUser,
|
|
14
|
+
validator: schema.createTsCodecValidator(WriteCheckpointRequest, { allowAdditional: true }),
|
|
15
|
+
handler: async (payload) => {
|
|
16
|
+
const system = payload.context.system;
|
|
17
|
+
const storage = system.storage;
|
|
18
|
+
|
|
19
|
+
const checkpoint = await util.getClientCheckpoint(system.requirePgPool(), storage);
|
|
20
|
+
return {
|
|
21
|
+
checkpoint
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const writeCheckpoint2 = routeDefinition({
|
|
27
|
+
path: '/write-checkpoint2.json',
|
|
28
|
+
method: router.HTTPMethod.GET,
|
|
29
|
+
authorize: authUser,
|
|
30
|
+
validator: schema.createTsCodecValidator(WriteCheckpointRequest, { allowAdditional: true }),
|
|
31
|
+
handler: async (payload) => {
|
|
32
|
+
const { user_id, system } = payload.context;
|
|
33
|
+
const storage = system.storage;
|
|
34
|
+
const write_checkpoint = await util.createWriteCheckpoint(system.requirePgPool(), storage, user_id!);
|
|
35
|
+
return {
|
|
36
|
+
write_checkpoint: String(write_checkpoint)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const CHECKPOINT_ROUTES = [writeCheckpoint, writeCheckpoint2];
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as t from 'ts-codec';
|
|
2
|
+
import * as pgwire from '@powersync/service-jpgwire';
|
|
3
|
+
import { errors, router, schema } from '@powersync/lib-services-framework';
|
|
4
|
+
|
|
5
|
+
import * as util from '../../util/util-index.js';
|
|
6
|
+
import { authDevUser, authUser, endpoint, issueDevToken, issueLegacyDevToken, issuePowerSyncToken } from '../auth.js';
|
|
7
|
+
import { routeDefinition } from '../router.js';
|
|
8
|
+
|
|
9
|
+
const AuthParams = t.object({
|
|
10
|
+
user: t.string,
|
|
11
|
+
password: t.string
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// For legacy web client only. Remove soon.
|
|
15
|
+
export const auth = routeDefinition({
|
|
16
|
+
path: '/auth.json',
|
|
17
|
+
method: router.HTTPMethod.POST,
|
|
18
|
+
validator: schema.createTsCodecValidator(AuthParams, { allowAdditional: true }),
|
|
19
|
+
handler: async (payload) => {
|
|
20
|
+
const { user, password } = payload.params;
|
|
21
|
+
const config = payload.context.system.config;
|
|
22
|
+
|
|
23
|
+
if (config.dev.demo_auth == false || config.dev.demo_password == null) {
|
|
24
|
+
throw new errors.AuthorizationError(['Demo auth disabled']);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (password == config.dev.demo_password) {
|
|
28
|
+
const token = await issueLegacyDevToken(payload.request, user, payload.context.system.config);
|
|
29
|
+
return { token, user_id: user, endpoint: endpoint(payload.request) };
|
|
30
|
+
} else {
|
|
31
|
+
throw new errors.AuthorizationError(['Authentication failed']);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const auth2 = routeDefinition({
|
|
37
|
+
path: '/dev/auth.json',
|
|
38
|
+
method: router.HTTPMethod.POST,
|
|
39
|
+
validator: schema.createTsCodecValidator(AuthParams, { allowAdditional: true }),
|
|
40
|
+
handler: async (payload) => {
|
|
41
|
+
const { user, password } = payload.params;
|
|
42
|
+
const config = payload.context.system.config;
|
|
43
|
+
|
|
44
|
+
if (config.dev.demo_auth == false || config.dev.demo_password == null) {
|
|
45
|
+
throw new errors.AuthorizationError(['Demo auth disabled']);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (password == config.dev.demo_password) {
|
|
49
|
+
const token = await issueDevToken(payload.request, user, payload.context.system.config);
|
|
50
|
+
return { token, user_id: user };
|
|
51
|
+
} else {
|
|
52
|
+
throw new errors.AuthorizationError(['Authentication failed']);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const TokenParams = t.object({});
|
|
58
|
+
|
|
59
|
+
export const token = routeDefinition({
|
|
60
|
+
path: '/dev/token.json',
|
|
61
|
+
method: router.HTTPMethod.POST,
|
|
62
|
+
validator: schema.createTsCodecValidator(TokenParams, { allowAdditional: true }),
|
|
63
|
+
authorize: authDevUser,
|
|
64
|
+
handler: async (payload) => {
|
|
65
|
+
const { user_id } = payload.context;
|
|
66
|
+
const outToken = await issuePowerSyncToken(payload.request, user_id!, payload.context.system.config);
|
|
67
|
+
return { token: outToken, user_id: user_id, endpoint: endpoint(payload.request) };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const OpType = {
|
|
72
|
+
PUT: 'PUT',
|
|
73
|
+
PATCH: 'PATCH',
|
|
74
|
+
DELETE: 'DELETE'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const CrudEntry = t.object({
|
|
78
|
+
op: t.Enum(OpType),
|
|
79
|
+
type: t.string,
|
|
80
|
+
id: t.string,
|
|
81
|
+
op_id: t.number.optional(),
|
|
82
|
+
data: t.any.optional()
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const CrudRequest = t.object({
|
|
86
|
+
data: t.array(CrudEntry),
|
|
87
|
+
write_checkpoint: t.boolean.optional()
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const crud = routeDefinition({
|
|
91
|
+
path: '/crud.json',
|
|
92
|
+
method: router.HTTPMethod.POST,
|
|
93
|
+
validator: schema.createTsCodecValidator(CrudRequest, { allowAdditional: true }),
|
|
94
|
+
authorize: authUser,
|
|
95
|
+
|
|
96
|
+
handler: async (payload) => {
|
|
97
|
+
const { user_id, system } = payload.context;
|
|
98
|
+
|
|
99
|
+
const pool = system.requirePgPool();
|
|
100
|
+
|
|
101
|
+
if (!system.config.dev.crud_api) {
|
|
102
|
+
throw new Error('CRUD api disabled');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const params = payload.params;
|
|
106
|
+
|
|
107
|
+
let statements: pgwire.Statement[] = [];
|
|
108
|
+
|
|
109
|
+
// Implementation note:
|
|
110
|
+
// Postgres does automatic "assigment cast" for query literals,
|
|
111
|
+
// e.g. a string literal to uuid. However, the same doesn't apply
|
|
112
|
+
// to query parameters.
|
|
113
|
+
// To handle those automatically, we use `json_populate_record`
|
|
114
|
+
// to automatically cast to the correct types.
|
|
115
|
+
|
|
116
|
+
for (let op of params.data) {
|
|
117
|
+
const table = util.escapeIdentifier(op.type);
|
|
118
|
+
if (op.op == 'PUT') {
|
|
119
|
+
const data = op.data as Record<string, any>;
|
|
120
|
+
const with_id = { ...data, id: op.id };
|
|
121
|
+
|
|
122
|
+
const columnsEscaped = Object.keys(with_id).map(util.escapeIdentifier);
|
|
123
|
+
const columnsJoined = columnsEscaped.join(', ');
|
|
124
|
+
|
|
125
|
+
let updateClauses: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (let key of Object.keys(data)) {
|
|
128
|
+
updateClauses.push(`${util.escapeIdentifier(key)} = EXCLUDED.${util.escapeIdentifier(key)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const updateClause = updateClauses.length > 0 ? `DO UPDATE SET ${updateClauses.join(', ')}` : `DO NOTHING`;
|
|
132
|
+
|
|
133
|
+
const statement = `
|
|
134
|
+
WITH data_row AS (
|
|
135
|
+
SELECT (json_populate_record(null::${table}, $1::json)).*
|
|
136
|
+
)
|
|
137
|
+
INSERT INTO ${table} (${columnsJoined})
|
|
138
|
+
SELECT ${columnsJoined} FROM data_row
|
|
139
|
+
ON CONFLICT(id) ${updateClause}`;
|
|
140
|
+
|
|
141
|
+
statements.push({
|
|
142
|
+
statement: statement,
|
|
143
|
+
params: [{ type: 'varchar', value: JSON.stringify(with_id) }]
|
|
144
|
+
});
|
|
145
|
+
} else if (op.op == 'PATCH') {
|
|
146
|
+
const data = op.data as Record<string, any>;
|
|
147
|
+
const with_id = { ...data, id: op.id };
|
|
148
|
+
|
|
149
|
+
let updateClauses: string[] = [];
|
|
150
|
+
|
|
151
|
+
for (let key of Object.keys(data)) {
|
|
152
|
+
updateClauses.push(`${util.escapeIdentifier(key)} = data_row.${util.escapeIdentifier(key)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const statement = `
|
|
156
|
+
WITH data_row AS (
|
|
157
|
+
SELECT (json_populate_record(null::${table}, $1::json)).*
|
|
158
|
+
)
|
|
159
|
+
UPDATE ${table}
|
|
160
|
+
SET ${updateClauses.join(', ')}
|
|
161
|
+
FROM data_row
|
|
162
|
+
WHERE ${table}.id = data_row.id`;
|
|
163
|
+
|
|
164
|
+
statements.push({
|
|
165
|
+
statement: statement,
|
|
166
|
+
params: [{ type: 'varchar', value: JSON.stringify(with_id) }]
|
|
167
|
+
});
|
|
168
|
+
} else if (op.op == 'DELETE') {
|
|
169
|
+
statements.push({
|
|
170
|
+
statement: `
|
|
171
|
+
WITH data_row AS (
|
|
172
|
+
SELECT (json_populate_record(null::${table}, $1::json)).*
|
|
173
|
+
)
|
|
174
|
+
DELETE FROM ${table}
|
|
175
|
+
USING data_row
|
|
176
|
+
WHERE ${table}.id = data_row.id`,
|
|
177
|
+
params: [{ type: 'varchar', value: JSON.stringify({ id: op.id }) }]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
await pool.query(...statements);
|
|
182
|
+
|
|
183
|
+
const storage = system.storage;
|
|
184
|
+
if (payload.params.write_checkpoint === true) {
|
|
185
|
+
const write_checkpoint = await util.createWriteCheckpoint(pool, storage, payload.context.user_id!);
|
|
186
|
+
return { write_checkpoint: String(write_checkpoint) };
|
|
187
|
+
} else if (payload.params.write_checkpoint === false) {
|
|
188
|
+
return {};
|
|
189
|
+
} else {
|
|
190
|
+
// Legacy
|
|
191
|
+
const checkpoint = await util.getClientCheckpoint(pool, storage);
|
|
192
|
+
return {
|
|
193
|
+
checkpoint
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const DEV_ROUTES = [auth, auth2, token, crud];
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { errors, logger, schema } from '@powersync/lib-services-framework';
|
|
2
|
+
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
1
3
|
import { serialize } from 'bson';
|
|
2
|
-
import { SyncParameters, normalizeTokenParameters } from '@powersync/service-sync-rules';
|
|
3
|
-
import * as micro from '@journeyapps-platform/micro';
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { streamResponse } from '
|
|
5
|
+
import { Metrics } from '../../metrics/Metrics.js';
|
|
6
|
+
import { streamResponse } from '../../sync/sync.js';
|
|
7
|
+
import * as util from '../../util/util-index.js';
|
|
8
|
+
import { SocketRouteGenerator } from '../router-socket.js';
|
|
7
9
|
import { SyncRoutes } from './sync-stream.js';
|
|
8
|
-
import { SocketRouteGenerator } from './router-socket.js';
|
|
9
|
-
import { Metrics } from '@/metrics/Metrics.js';
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
12
12
|
router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
|
|
13
13
|
authorize: ({ context }) => {
|
|
14
14
|
return {
|
|
@@ -16,13 +16,13 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
|
|
|
16
16
|
errors: ['Authentication required'].concat(context.token_errors ?? [])
|
|
17
17
|
};
|
|
18
18
|
},
|
|
19
|
-
validator:
|
|
19
|
+
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
20
20
|
handler: async ({ context, params, responder, observer, initialN }) => {
|
|
21
21
|
const { system } = context;
|
|
22
22
|
|
|
23
23
|
if (system.closed) {
|
|
24
24
|
responder.onError(
|
|
25
|
-
new
|
|
25
|
+
new errors.JourneyError({
|
|
26
26
|
status: 503,
|
|
27
27
|
code: 'SERVICE_UNAVAILABLE',
|
|
28
28
|
description: 'Service temporarily unavailable'
|
|
@@ -34,17 +34,14 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
|
|
|
34
34
|
|
|
35
35
|
const controller = new AbortController();
|
|
36
36
|
|
|
37
|
-
const syncParams
|
|
38
|
-
context.token_payload?.parameters ?? {},
|
|
39
|
-
params.parameters ?? {}
|
|
40
|
-
);
|
|
37
|
+
const syncParams = new RequestParameters(context.token_payload!, params.parameters ?? {});
|
|
41
38
|
|
|
42
39
|
const storage = system.storage;
|
|
43
40
|
// Sanity check before we start the stream
|
|
44
41
|
const cp = await storage.getActiveCheckpoint();
|
|
45
42
|
if (!cp.hasSyncRules()) {
|
|
46
43
|
responder.onError(
|
|
47
|
-
new
|
|
44
|
+
new errors.JourneyError({
|
|
48
45
|
status: 500,
|
|
49
46
|
code: 'NO_SYNC_RULES',
|
|
50
47
|
description: 'No sync rules available'
|
|
@@ -122,8 +119,8 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
|
|
|
122
119
|
} catch (ex) {
|
|
123
120
|
// Convert to our standard form before responding.
|
|
124
121
|
// This ensures the error can be serialized.
|
|
125
|
-
const error = new
|
|
126
|
-
|
|
122
|
+
const error = new errors.InternalServerError(ex);
|
|
123
|
+
logger.error('Sync stream error', error);
|
|
127
124
|
responder.onError(error);
|
|
128
125
|
} finally {
|
|
129
126
|
responder.onComplete();
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as t from 'ts-codec';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import * as pgwire from '@powersync/service-jpgwire';
|
|
4
|
+
import { errors, router, schema } from '@powersync/lib-services-framework';
|
|
5
|
+
import { SqlSyncRules, SyncRulesErrors } from '@powersync/service-sync-rules';
|
|
6
|
+
|
|
7
|
+
import * as replication from '../../replication/replication-index.js';
|
|
8
|
+
import { authApi } from '../auth.js';
|
|
9
|
+
import { routeDefinition } from '../router.js';
|
|
10
|
+
|
|
11
|
+
const DeploySyncRulesRequest = t.object({
|
|
12
|
+
content: t.string
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const yamlPlugin: FastifyPluginAsync = async (fastify) => {
|
|
16
|
+
fastify.addContentTypeParser('application/yaml', async (request, payload, _d) => {
|
|
17
|
+
const data: any[] = [];
|
|
18
|
+
for await (const chunk of payload) {
|
|
19
|
+
data.push(chunk);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
request.params = { content: Buffer.concat(data).toString('utf8') };
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Declares the plugin should be available on the same scope
|
|
28
|
+
* without requiring the `fastify-plugin` package as a dependency.
|
|
29
|
+
* https://fastify.dev/docs/latest/Reference/Plugins/#handle-the-scope
|
|
30
|
+
*/
|
|
31
|
+
//@ts-expect-error
|
|
32
|
+
yamlPlugin[Symbol.for('skip-override')] = true;
|
|
33
|
+
|
|
34
|
+
export const deploySyncRules = routeDefinition({
|
|
35
|
+
path: '/api/sync-rules/v1/deploy',
|
|
36
|
+
method: router.HTTPMethod.POST,
|
|
37
|
+
authorize: authApi,
|
|
38
|
+
parse: true,
|
|
39
|
+
plugins: [yamlPlugin],
|
|
40
|
+
validator: schema.createTsCodecValidator(DeploySyncRulesRequest, { allowAdditional: true }),
|
|
41
|
+
handler: async (payload) => {
|
|
42
|
+
if (payload.context.system.config.sync_rules.present) {
|
|
43
|
+
// If sync rules are configured via the config, disable deploy via the API.
|
|
44
|
+
throw new errors.JourneyError({
|
|
45
|
+
status: 422,
|
|
46
|
+
code: 'API_DISABLED',
|
|
47
|
+
description: 'Sync rules API disabled',
|
|
48
|
+
details: 'Use the management API to deploy sync rules'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const content = payload.params.content;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
SqlSyncRules.fromYaml(payload.params.content);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw new errors.JourneyError({
|
|
57
|
+
status: 422,
|
|
58
|
+
code: 'INVALID_SYNC_RULES',
|
|
59
|
+
description: 'Sync rules parsing failed',
|
|
60
|
+
details: e.message
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sync_rules = await payload.context.system.storage.updateSyncRules({
|
|
65
|
+
content: content
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
slot_name: sync_rules.slot_name
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const ValidateSyncRulesRequest = t.object({
|
|
75
|
+
content: t.string
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const validateSyncRules = routeDefinition({
|
|
79
|
+
path: '/api/sync-rules/v1/validate',
|
|
80
|
+
method: router.HTTPMethod.POST,
|
|
81
|
+
authorize: authApi,
|
|
82
|
+
parse: true,
|
|
83
|
+
plugins: [yamlPlugin],
|
|
84
|
+
validator: schema.createTsCodecValidator(ValidateSyncRulesRequest, { allowAdditional: true }),
|
|
85
|
+
handler: async (payload) => {
|
|
86
|
+
const content = payload.params.content;
|
|
87
|
+
|
|
88
|
+
const info = await debugSyncRules(payload.context.system.requirePgPool(), content);
|
|
89
|
+
|
|
90
|
+
return replyPrettyJson(info);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const currentSyncRules = routeDefinition({
|
|
95
|
+
path: '/api/sync-rules/v1/current',
|
|
96
|
+
method: router.HTTPMethod.GET,
|
|
97
|
+
authorize: authApi,
|
|
98
|
+
handler: async (payload) => {
|
|
99
|
+
const storage = payload.context.system.storage;
|
|
100
|
+
const sync_rules = await storage.getActiveSyncRulesContent();
|
|
101
|
+
if (!sync_rules) {
|
|
102
|
+
throw new errors.JourneyError({
|
|
103
|
+
status: 422,
|
|
104
|
+
code: 'NO_SYNC_RULES',
|
|
105
|
+
description: 'No active sync rules'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const info = await debugSyncRules(payload.context.system.requirePgPool(), sync_rules.sync_rules_content);
|
|
109
|
+
const next = await storage.getNextSyncRulesContent();
|
|
110
|
+
|
|
111
|
+
const next_info = next
|
|
112
|
+
? await debugSyncRules(payload.context.system.requirePgPool(), next.sync_rules_content)
|
|
113
|
+
: null;
|
|
114
|
+
|
|
115
|
+
const response = {
|
|
116
|
+
current: {
|
|
117
|
+
slot_name: sync_rules.slot_name,
|
|
118
|
+
content: sync_rules.sync_rules_content,
|
|
119
|
+
...info
|
|
120
|
+
},
|
|
121
|
+
next:
|
|
122
|
+
next == null
|
|
123
|
+
? null
|
|
124
|
+
: {
|
|
125
|
+
slot_name: next.slot_name,
|
|
126
|
+
content: next.sync_rules_content,
|
|
127
|
+
...next_info
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return replyPrettyJson({ data: response });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const ReprocessSyncRulesRequest = t.object({});
|
|
136
|
+
|
|
137
|
+
export const reprocessSyncRules = routeDefinition({
|
|
138
|
+
path: '/api/sync-rules/v1/reprocess',
|
|
139
|
+
method: router.HTTPMethod.POST,
|
|
140
|
+
authorize: authApi,
|
|
141
|
+
validator: schema.createTsCodecValidator(ReprocessSyncRulesRequest),
|
|
142
|
+
handler: async (payload) => {
|
|
143
|
+
const storage = payload.context.system.storage;
|
|
144
|
+
const sync_rules = await storage.getActiveSyncRules();
|
|
145
|
+
if (sync_rules == null) {
|
|
146
|
+
throw new errors.JourneyError({
|
|
147
|
+
status: 422,
|
|
148
|
+
code: 'NO_SYNC_RULES',
|
|
149
|
+
description: 'No active sync rules'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const new_rules = await storage.updateSyncRules({
|
|
154
|
+
content: sync_rules.sync_rules.content
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
slot_name: new_rules.slot_name
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export const SYNC_RULES_ROUTES = [validateSyncRules, deploySyncRules, reprocessSyncRules, currentSyncRules];
|
|
163
|
+
|
|
164
|
+
function replyPrettyJson(payload: any) {
|
|
165
|
+
return new router.RouterResponse({
|
|
166
|
+
status: 200,
|
|
167
|
+
data: JSON.stringify(payload, null, 2) + '\n',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function debugSyncRules(db: pgwire.PgClient, sync_rules: string) {
|
|
173
|
+
try {
|
|
174
|
+
const rules = SqlSyncRules.fromYaml(sync_rules);
|
|
175
|
+
const source_table_patterns = rules.getSourceTables();
|
|
176
|
+
const wc = new replication.WalConnection({
|
|
177
|
+
db: db,
|
|
178
|
+
sync_rules: rules
|
|
179
|
+
});
|
|
180
|
+
const resolved_tables = await wc.getDebugTablesInfo(source_table_patterns);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
valid: true,
|
|
184
|
+
bucket_definitions: rules.bucket_descriptors.map((d) => {
|
|
185
|
+
let all_parameter_queries = [...d.parameter_queries.values()].flat();
|
|
186
|
+
let all_data_queries = [...d.data_queries.values()].flat();
|
|
187
|
+
return {
|
|
188
|
+
name: d.name,
|
|
189
|
+
bucket_parameters: d.bucket_parameters,
|
|
190
|
+
global_parameter_queries: d.global_parameter_queries.map((q) => {
|
|
191
|
+
return {
|
|
192
|
+
sql: q.sql
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
parameter_queries: all_parameter_queries.map((q) => {
|
|
196
|
+
return {
|
|
197
|
+
sql: q.sql,
|
|
198
|
+
table: q.sourceTable,
|
|
199
|
+
input_parameters: q.input_parameters
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
|
|
203
|
+
data_queries: all_data_queries.map((q) => {
|
|
204
|
+
return {
|
|
205
|
+
sql: q.sql,
|
|
206
|
+
table: q.sourceTable,
|
|
207
|
+
columns: q.columnOutputNames()
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
};
|
|
211
|
+
}),
|
|
212
|
+
source_tables: resolved_tables,
|
|
213
|
+
data_tables: rules.debugGetOutputTables()
|
|
214
|
+
};
|
|
215
|
+
} catch (e) {
|
|
216
|
+
if (e instanceof SyncRulesErrors) {
|
|
217
|
+
return {
|
|
218
|
+
valid: false,
|
|
219
|
+
errors: e.errors.map((e) => e.message)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
valid: false,
|
|
224
|
+
errors: [e.message]
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { errors, logger, router, schema } from '@powersync/lib-services-framework';
|
|
2
|
+
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
|
|
5
|
+
import * as sync from '../../sync/sync-index.js';
|
|
6
|
+
import * as util from '../../util/util-index.js';
|
|
7
|
+
|
|
8
|
+
import { Metrics } from '../../metrics/Metrics.js';
|
|
9
|
+
import { authUser } from '../auth.js';
|
|
10
|
+
import { routeDefinition } from '../router.js';
|
|
11
|
+
|
|
12
|
+
export enum SyncRoutes {
|
|
13
|
+
STREAM = '/sync/stream'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const syncStreamed = routeDefinition({
|
|
17
|
+
path: SyncRoutes.STREAM,
|
|
18
|
+
method: router.HTTPMethod.POST,
|
|
19
|
+
authorize: authUser,
|
|
20
|
+
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
21
|
+
handler: async (payload) => {
|
|
22
|
+
const system = payload.context.system;
|
|
23
|
+
|
|
24
|
+
if (system.closed) {
|
|
25
|
+
throw new errors.JourneyError({
|
|
26
|
+
status: 503,
|
|
27
|
+
code: 'SERVICE_UNAVAILABLE',
|
|
28
|
+
description: 'Service temporarily unavailable'
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const params: util.StreamingSyncRequest = payload.params;
|
|
33
|
+
const syncParams = new RequestParameters(payload.context.token_payload!, payload.params.parameters ?? {});
|
|
34
|
+
|
|
35
|
+
const storage = system.storage;
|
|
36
|
+
// Sanity check before we start the stream
|
|
37
|
+
const cp = await storage.getActiveCheckpoint();
|
|
38
|
+
if (!cp.hasSyncRules()) {
|
|
39
|
+
throw new errors.JourneyError({
|
|
40
|
+
status: 500,
|
|
41
|
+
code: 'NO_SYNC_RULES',
|
|
42
|
+
description: 'No sync rules available'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
try {
|
|
47
|
+
Metrics.getInstance().concurrent_connections.add(1);
|
|
48
|
+
const stream = Readable.from(
|
|
49
|
+
sync.transformToBytesTracked(
|
|
50
|
+
sync.ndjson(
|
|
51
|
+
sync.streamResponse({
|
|
52
|
+
storage,
|
|
53
|
+
params,
|
|
54
|
+
syncParams,
|
|
55
|
+
token: payload.context.token_payload!,
|
|
56
|
+
signal: controller.signal
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
{ objectMode: false, highWaterMark: 16 * 1024 }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const deregister = system.addStopHandler(() => {
|
|
64
|
+
// This error is not currently propagated to the client
|
|
65
|
+
controller.abort();
|
|
66
|
+
stream.destroy(new Error('Shutting down system'));
|
|
67
|
+
});
|
|
68
|
+
stream.on('close', () => {
|
|
69
|
+
deregister();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
stream.on('error', (error) => {
|
|
73
|
+
controller.abort();
|
|
74
|
+
// Note: This appears as a 200 response in the logs.
|
|
75
|
+
if (error.message != 'Shutting down system') {
|
|
76
|
+
logger.error('Streaming sync request failed', error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return new router.RouterResponse({
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/x-ndjson'
|
|
84
|
+
},
|
|
85
|
+
data: stream,
|
|
86
|
+
afterSend: async () => {
|
|
87
|
+
controller.abort();
|
|
88
|
+
Metrics.getInstance().concurrent_connections.add(-1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} catch (ex) {
|
|
92
|
+
controller.abort();
|
|
93
|
+
Metrics.getInstance().concurrent_connections.add(-1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const SYNC_STREAM_ROUTES = [syncStreamed];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type fastify from 'fastify';
|
|
2
|
+
import a from 'async';
|
|
3
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
4
|
+
|
|
5
|
+
export type CreateRequestQueueParams = {
|
|
6
|
+
max_queue_depth: number;
|
|
7
|
+
concurrency: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a request queue which limits the amount of concurrent connections which
|
|
12
|
+
* are active at any time.
|
|
13
|
+
*/
|
|
14
|
+
export const createRequestQueueHook = (params: CreateRequestQueueParams): fastify.onRequestHookHandler => {
|
|
15
|
+
const request_queue = a.queue<() => Promise<void>>((event, done) => {
|
|
16
|
+
event().finally(done);
|
|
17
|
+
}, params.concurrency);
|
|
18
|
+
|
|
19
|
+
return (request, reply, next) => {
|
|
20
|
+
if (
|
|
21
|
+
(params.max_queue_depth == 0 && request_queue.running() == params.concurrency) ||
|
|
22
|
+
(params.max_queue_depth > 0 && request_queue.length() >= params.max_queue_depth)
|
|
23
|
+
) {
|
|
24
|
+
logger.warn(`${request.method} ${request.url}`, {
|
|
25
|
+
status: 429,
|
|
26
|
+
method: request.method,
|
|
27
|
+
path: request.url,
|
|
28
|
+
queue_overflow: true
|
|
29
|
+
});
|
|
30
|
+
return reply.status(429).send();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const finished = new Promise<void>((resolve) => {
|
|
34
|
+
reply.then(
|
|
35
|
+
() => resolve(),
|
|
36
|
+
() => resolve()
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
request_queue.push(() => {
|
|
41
|
+
next();
|
|
42
|
+
return finished;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
};
|