Skip to content

Commit b4b1923

Browse files
committed
Move core module loading logic to service-core.
1 parent 9059015 commit b4b1923

File tree

10 files changed

+173
-138
lines changed

10 files changed

+173
-138
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ResolvedPowerSyncConfig } from '../util/util-index.js';
2+
import { AbstractModule } from './AbstractModule.js';
3+
4+
interface DynamicModuleMap {
5+
[key: string]: () => Promise<AbstractModule>;
6+
}
7+
8+
export interface ModuleLoaders {
9+
storage: DynamicModuleMap;
10+
connection: DynamicModuleMap;
11+
}
12+
/**
13+
* Utility function to dynamically load and instantiate modules.
14+
*/
15+
export async function loadModules(config: ResolvedPowerSyncConfig, loaders: ModuleLoaders) {
16+
const requiredConnections = [...new Set(config.connections?.map((connection) => connection.type) || [])];
17+
const missingConnectionModules: string[] = [];
18+
const modulePromises: Promise<AbstractModule>[] = [];
19+
20+
// 1. Map connection types to their module loading promises making note of any
21+
// missing connection types.
22+
requiredConnections.forEach((connectionType) => {
23+
const modulePromise = loaders.connection[connectionType];
24+
if (modulePromise !== undefined) {
25+
modulePromises.push(modulePromise());
26+
} else {
27+
missingConnectionModules.push(connectionType);
28+
}
29+
});
30+
31+
// Fail if any connection types are not found.
32+
if (missingConnectionModules.length > 0) {
33+
throw new Error(`Invalid connection types: "${[...missingConnectionModules].join(', ')}"`);
34+
}
35+
36+
if (loaders.storage[config.storage.type] !== undefined) {
37+
modulePromises.push(loaders.storage[config.storage.type]());
38+
} else {
39+
throw new Error(`Invalid storage type: "${config.storage.type}"`);
40+
}
41+
42+
// 2. Dynamically import and instantiate module classes and resolve all promises
43+
// raising errors if any modules could not be imported.
44+
const moduleInstances = await Promise.all(modulePromises);
45+
46+
return moduleInstances;
47+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './ModuleManager.js';
22
export * from './AbstractModule.js';
3+
export * from './loader.js';
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { AbstractModule, loadModules, ServiceContextContainer, TearDownOptions } from '@/index.js';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
interface MockConfig {
5+
connections?: { type: string }[];
6+
storage: { type: string };
7+
}
8+
9+
class MockMySQLModule extends AbstractModule {
10+
constructor() {
11+
super({ name: 'MySQLModule' });
12+
}
13+
async initialize(context: ServiceContextContainer): Promise<void> {}
14+
async teardown(options: TearDownOptions): Promise<void> {}
15+
}
16+
class MockPostgresModule extends AbstractModule {
17+
constructor() {
18+
super({ name: 'PostgresModule' });
19+
}
20+
async initialize(context: ServiceContextContainer): Promise<void> {}
21+
async teardown(options: TearDownOptions): Promise<void> {}
22+
}
23+
class MockPostgresStorageModule extends AbstractModule {
24+
constructor() {
25+
super({ name: 'PostgresStorageModule' });
26+
}
27+
async initialize(context: ServiceContextContainer): Promise<void> {}
28+
async teardown(options: TearDownOptions): Promise<void> {}
29+
}
30+
const mockLoaders = {
31+
connection: {
32+
mysql: async () => {
33+
return new MockMySQLModule();
34+
},
35+
postgresql: async () => {
36+
return new MockPostgresModule();
37+
}
38+
},
39+
storage: {
40+
postgresql: async () => {
41+
return new MockPostgresStorageModule();
42+
}
43+
}
44+
};
45+
46+
describe('module loader', () => {
47+
it('should load all modules defined in connections and storage', async () => {
48+
const config: MockConfig = {
49+
connections: [{ type: 'mysql' }, { type: 'postgresql' }],
50+
storage: { type: 'postgresql' }
51+
};
52+
53+
const modules = await loadModules(config as any, mockLoaders);
54+
55+
expect(modules.length).toBe(3);
56+
expect(modules[0]).toBeInstanceOf(MockMySQLModule);
57+
expect(modules[1]).toBeInstanceOf(MockPostgresModule);
58+
expect(modules[2]).toBeInstanceOf(MockPostgresStorageModule);
59+
});
60+
61+
it('should handle duplicate connection types (e.g., mysql used twice)', async () => {
62+
const config: MockConfig = {
63+
connections: [{ type: 'mysql' }, { type: 'postgresql' }, { type: 'mysql' }], // mysql duplicated
64+
storage: { type: 'postgresql' }
65+
};
66+
67+
const modules = await loadModules(config as any, mockLoaders);
68+
69+
// Expect 3 modules: mysql, postgresql, postgresql-storage
70+
expect(modules.length).toBe(3);
71+
expect(modules.filter((m) => m instanceof MockMySQLModule).length).toBe(1);
72+
expect(modules.filter((m) => m instanceof MockPostgresModule).length).toBe(1);
73+
expect(modules.filter((m) => m instanceof MockPostgresStorageModule).length).toBe(1);
74+
});
75+
76+
it('should throw an error if any modules are not found in ModuleMap', async () => {
77+
const config: MockConfig = {
78+
connections: [{ type: 'mysql' }, { type: 'redis' }],
79+
storage: { type: 'postgresql' }
80+
};
81+
82+
await expect(loadModules(config as any, mockLoaders)).rejects.toThrowError();
83+
});
84+
85+
it('should throw an error if one dynamic connection module import fails', async () => {
86+
const config: MockConfig = {
87+
connections: [{ type: 'mysql' }],
88+
storage: { type: 'postgresql' }
89+
};
90+
91+
const loaders = {
92+
connection: {
93+
mysql: async () => {
94+
throw new Error('Failed to load MySQL module');
95+
}
96+
},
97+
storage: mockLoaders.storage
98+
};
99+
100+
await expect(loadModules(config as any, loaders)).rejects.toThrowError('Failed to load MySQL module');
101+
});
102+
});

service/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
"scripts": {
88
"build": "tsc -b",
99
"watch": "nodemon -w ../ -e ts -e js --delay 1 -x node --loader ts-node/esm src/entry.ts start",
10-
"clean": "rm -rf ./lib && tsc -b --clean",
11-
"test": "vitest"
10+
"clean": "rm -rf ./lib && tsc -b --clean"
1211
},
1312
"dependencies": {
1413
"@powersync/service-core": "workspace:*",
@@ -29,4 +28,4 @@
2928
"npm-check-updates": "^16.14.4",
3029
"ts-node": "^10.9.1"
3130
}
32-
}
31+
}

service/src/runners/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { container, logger } from '@powersync/lib-services-framework';
22
import * as core from '@powersync/service-core';
33

4-
import { loadModules } from '../util/module-loader.js';
54
import { logBooting } from '../util/version.js';
5+
import { DYNAMIC_MODULES } from '../util/modules.js';
66

77
/**
88
* Starts an API server
@@ -13,7 +13,7 @@ export async function startServer(runnerConfig: core.utils.RunnerConfig) {
1313
const config = await core.utils.loadConfig(runnerConfig);
1414

1515
const moduleManager = container.getImplementation(core.modules.ModuleManager);
16-
const modules = await loadModules(config);
16+
const modules = await core.loadModules(config, DYNAMIC_MODULES);
1717
if (modules.length > 0) {
1818
moduleManager.register(modules);
1919
}

service/src/runners/stream-worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { container, logger } from '@powersync/lib-services-framework';
22
import * as core from '@powersync/service-core';
33

4-
import { loadModules } from '../util/module-loader.js';
54
import { logBooting } from '../util/version.js';
5+
import { DYNAMIC_MODULES } from '../util/modules.js';
66

77
/**
88
* Configures the replication portion on a {@link serviceContext}
@@ -24,7 +24,7 @@ export const startStreamRunner = async (runnerConfig: core.utils.RunnerConfig) =
2424
const config = await core.utils.loadConfig(runnerConfig);
2525

2626
const moduleManager = container.getImplementation(core.modules.ModuleManager);
27-
const modules = await loadModules(config);
27+
const modules = await core.loadModules(config, DYNAMIC_MODULES);
2828
if (modules.length > 0) {
2929
moduleManager.register(modules);
3030
}

service/src/runners/unified-runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { container, logger } from '@powersync/lib-services-framework';
22
import * as core from '@powersync/service-core';
33

4-
import { loadModules } from '../util/module-loader.js';
54
import { logBooting } from '../util/version.js';
65
import { registerReplicationServices } from './stream-worker.js';
6+
import { DYNAMIC_MODULES } from '../util/modules.js';
77

88
/**
99
* Starts an API server
@@ -14,7 +14,7 @@ export const startUnifiedRunner = async (runnerConfig: core.utils.RunnerConfig)
1414
const config = await core.utils.loadConfig(runnerConfig);
1515

1616
const moduleManager = container.getImplementation(core.modules.ModuleManager);
17-
const modules = await loadModules(config);
17+
const modules = await core.loadModules(config, DYNAMIC_MODULES);
1818
if (modules.length > 0) {
1919
moduleManager.register(modules);
2020
}

service/src/util/module-loader.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

service/src/util/modules.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as core from '@powersync/service-core';
2+
3+
export const DYNAMIC_MODULES: core.ModuleLoaders = {
4+
connection: {
5+
mongodb: () => import('@powersync/service-module-mongodb').then((module) => new module.MongoModule()),
6+
mysql: () => import('@powersync/service-module-mysql').then((module) => new module.MySQLModule()),
7+
postgresql: () => import('@powersync/service-module-postgres').then((module) => new module.PostgresModule())
8+
},
9+
storage: {
10+
mongodb: () =>
11+
import('@powersync/service-module-mongodb-storage').then((module) => new module.MongoStorageModule()),
12+
postgresql: () =>
13+
import('@powersync/service-module-postgres-storage').then((module) => new module.PostgresStorageModule())
14+
}
15+
};

service/test/src/util/module-loader.test.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)