@flink-app/flink 2.0.0-alpha.81 → 2.0.0-alpha.83

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @flink-app/flink
2
2
 
3
+ ## 2.0.0-alpha.83
4
+
5
+ ### Minor Changes
6
+
7
+ - Add FlinkService as optional business logic layer with auto-discovery, ctx injection, async onInit(), and mockCtx() test helper
8
+
9
+ ## 2.0.0-alpha.82
10
+
3
11
  ## 2.0.0-alpha.81
4
12
 
5
13
  ## 2.0.0-alpha.80
package/cli/cli-utils.ts CHANGED
@@ -116,6 +116,7 @@ export async function compile(opts: CompileOptions): Promise<void> {
116
116
  await compiler.parseTools();
117
117
  await compiler.parseAgents();
118
118
  await compiler.parseJobs();
119
+ await compiler.parseServices();
119
120
  await compiler.parseAllExtensionDirs();
120
121
  await compiler.parseHandlers();
121
122
  await compiler.generateStartScript(entry);
@@ -125,20 +125,23 @@ function compile(opts) {
125
125
  return [4 /*yield*/, compiler.parseJobs()];
126
126
  case 5:
127
127
  _c.sent();
128
- return [4 /*yield*/, compiler.parseAllExtensionDirs()];
128
+ return [4 /*yield*/, compiler.parseServices()];
129
129
  case 6:
130
130
  _c.sent();
131
- return [4 /*yield*/, compiler.parseHandlers()];
131
+ return [4 /*yield*/, compiler.parseAllExtensionDirs()];
132
132
  case 7:
133
133
  _c.sent();
134
- return [4 /*yield*/, compiler.generateStartScript(entry)];
134
+ return [4 /*yield*/, compiler.parseHandlers()];
135
135
  case 8:
136
136
  _c.sent();
137
- return [4 /*yield*/, compiler.generateAllSchemas()];
137
+ return [4 /*yield*/, compiler.generateStartScript(entry)];
138
138
  case 9:
139
139
  _c.sent();
140
- return [4 /*yield*/, compiler.saveAllModifiedFiles()];
140
+ return [4 /*yield*/, compiler.generateAllSchemas()];
141
141
  case 10:
142
+ _c.sent();
143
+ return [4 /*yield*/, compiler.saveAllModifiedFiles()];
144
+ case 11:
142
145
  _c.sent();
143
146
  if (typeCheck) {
144
147
  stepStart = Date.now();
@@ -148,14 +151,14 @@ function compile(opts) {
148
151
  time("Type checking (getPreEmitDiagnostics)", stepStart);
149
152
  }
150
153
  stepStart = Date.now();
151
- if (!(process.env.FLINK_USE_TSC === "true")) return [3 /*break*/, 11];
154
+ if (!(process.env.FLINK_USE_TSC === "true")) return [3 /*break*/, 12];
152
155
  compiler.emitWithTsc();
153
- return [3 /*break*/, 13];
154
- case 11: return [4 /*yield*/, compiler.emit()];
155
- case 12:
156
- _c.sent();
157
- _c.label = 13;
156
+ return [3 /*break*/, 14];
157
+ case 12: return [4 /*yield*/, compiler.emit()];
158
158
  case 13:
159
+ _c.sent();
160
+ _c.label = 14;
161
+ case 14:
159
162
  time("Transpilation (".concat(process.env.FLINK_USE_TSC === "true" ? "tsc" : "swc", ")"), stepStart);
160
163
  if (timingLogs) {
161
164
  console.log("Compilation done in ".concat(Date.now() - start, "ms"));
@@ -55,6 +55,14 @@ export declare const autoRegisteredTools: FlinkToolFile[];
55
55
  * are picked up by TypeScript compiler
56
56
  */
57
57
  export declare const autoRegisteredAgents: FlinkAgentFile<any, any>[];
58
+ /**
59
+ * This will be populated at compile time when the apps services
60
+ * are picked up by TypeScript compiler
61
+ */
62
+ export declare const autoRegisteredServices: {
63
+ serviceInstanceName: string;
64
+ Service: any;
65
+ }[];
58
66
  export interface FlinkOptions {
59
67
  /**
60
68
  * Name of application, will only show in logs and in HTTP header.
@@ -280,6 +288,7 @@ export declare class FlinkApp<C extends FlinkContext> {
280
288
  private expressServer;
281
289
  private onError?;
282
290
  private repos;
291
+ private services;
283
292
  private llmAdapters;
284
293
  private tools;
285
294
  private agents;
@@ -50,7 +50,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
50
50
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
51
  };
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
- exports.FlinkApp = exports.autoRegisteredAgents = exports.autoRegisteredTools = exports.autoRegisteredJobs = exports.autoRegisteredRepos = exports.autoRegisteredHandlers = exports.expressFn = void 0;
53
+ exports.FlinkApp = exports.autoRegisteredServices = exports.autoRegisteredAgents = exports.autoRegisteredTools = exports.autoRegisteredJobs = exports.autoRegisteredRepos = exports.autoRegisteredHandlers = exports.expressFn = void 0;
54
54
  var ajv_1 = __importDefault(require("ajv"));
55
55
  var ajv_formats_1 = __importDefault(require("ajv-formats"));
56
56
  var body_parser_1 = __importDefault(require("body-parser"));
@@ -110,6 +110,11 @@ exports.autoRegisteredTools = [];
110
110
  * are picked up by TypeScript compiler
111
111
  */
112
112
  exports.autoRegisteredAgents = [];
113
+ /**
114
+ * This will be populated at compile time when the apps services
115
+ * are picked up by TypeScript compiler
116
+ */
117
+ exports.autoRegisteredServices = [];
113
118
  var FlinkApp = /** @class */ (function () {
114
119
  function FlinkApp(opts) {
115
120
  var _a;
@@ -120,6 +125,7 @@ var FlinkApp = /** @class */ (function () {
120
125
  this.routingConfigured = false;
121
126
  this.disableHttpServer = false;
122
127
  this.repos = {};
128
+ this.services = {};
123
129
  this.llmAdapters = new Map();
124
130
  this.tools = {};
125
131
  this.agents = {}; // FlinkAgent<C> instances
@@ -1123,37 +1129,60 @@ var FlinkApp = /** @class */ (function () {
1123
1129
  */
1124
1130
  FlinkApp.prototype.buildContext = function () {
1125
1131
  return __awaiter(this, void 0, void 0, function () {
1126
- var _i, autoRegisteredRepos_1, _a, collectionName, repoInstanceName, Repo, repoInstance, pluginCtx, _b, _c, repo;
1127
- return __generator(this, function (_d) {
1128
- if (this.dbOpts) {
1129
- for (_i = 0, autoRegisteredRepos_1 = exports.autoRegisteredRepos; _i < autoRegisteredRepos_1.length; _i++) {
1130
- _a = autoRegisteredRepos_1[_i], collectionName = _a.collectionName, repoInstanceName = _a.repoInstanceName, Repo = _a.Repo;
1131
- repoInstance = new Repo(collectionName, this.db, this.dbClient);
1132
- this.repos[repoInstanceName] = repoInstance;
1133
- initLog.info("Registered repo ".concat(repoInstanceName));
1134
- }
1135
- }
1136
- else if (exports.autoRegisteredRepos.length > 0) {
1137
- FlinkLog_1.log.warn("No db configured but found repo(s)");
1138
- }
1139
- pluginCtx = this.plugins.reduce(function (out, plugin) {
1140
- if (out[plugin.id]) {
1141
- throw new Error("Plugin ".concat(plugin.id, " is already registered"));
1142
- }
1143
- out[plugin.id] = plugin.ctx;
1144
- return out;
1145
- }, {});
1146
- this._ctx = {
1147
- repos: this.repos,
1148
- plugins: pluginCtx,
1149
- auth: this.auth,
1150
- agents: this.agents,
1151
- };
1152
- for (_b = 0, _c = Object.values(this.repos); _b < _c.length; _b++) {
1153
- repo = _c[_b];
1154
- repo.ctx = this.ctx;
1132
+ var _i, autoRegisteredRepos_1, _a, collectionName, repoInstanceName, Repo, repoInstance, pluginCtx, _b, autoRegisteredServices_1, _c, serviceInstanceName, Service, serviceInstance, _d, _e, repo, _f, _g, service, servicesWithInit;
1133
+ return __generator(this, function (_h) {
1134
+ switch (_h.label) {
1135
+ case 0:
1136
+ if (this.dbOpts) {
1137
+ for (_i = 0, autoRegisteredRepos_1 = exports.autoRegisteredRepos; _i < autoRegisteredRepos_1.length; _i++) {
1138
+ _a = autoRegisteredRepos_1[_i], collectionName = _a.collectionName, repoInstanceName = _a.repoInstanceName, Repo = _a.Repo;
1139
+ repoInstance = new Repo(collectionName, this.db, this.dbClient);
1140
+ this.repos[repoInstanceName] = repoInstance;
1141
+ initLog.info("Registered repo ".concat(repoInstanceName));
1142
+ }
1143
+ }
1144
+ else if (exports.autoRegisteredRepos.length > 0) {
1145
+ FlinkLog_1.log.warn("No db configured but found repo(s)");
1146
+ }
1147
+ pluginCtx = this.plugins.reduce(function (out, plugin) {
1148
+ if (out[plugin.id]) {
1149
+ throw new Error("Plugin ".concat(plugin.id, " is already registered"));
1150
+ }
1151
+ out[plugin.id] = plugin.ctx;
1152
+ return out;
1153
+ }, {});
1154
+ // Instantiate services (ctx not yet available - constructors must not access it)
1155
+ for (_b = 0, autoRegisteredServices_1 = exports.autoRegisteredServices; _b < autoRegisteredServices_1.length; _b++) {
1156
+ _c = autoRegisteredServices_1[_b], serviceInstanceName = _c.serviceInstanceName, Service = _c.Service;
1157
+ serviceInstance = new Service();
1158
+ this.services[serviceInstanceName] = serviceInstance;
1159
+ initLog.info("Registered service ".concat(serviceInstanceName));
1160
+ }
1161
+ this._ctx = {
1162
+ repos: this.repos,
1163
+ plugins: pluginCtx,
1164
+ auth: this.auth,
1165
+ agents: this.agents,
1166
+ services: this.services,
1167
+ };
1168
+ // Inject context into repos
1169
+ for (_d = 0, _e = Object.values(this.repos); _d < _e.length; _d++) {
1170
+ repo = _e[_d];
1171
+ repo.ctx = this.ctx;
1172
+ }
1173
+ // Inject context into services, then call onInit() in parallel
1174
+ for (_f = 0, _g = Object.values(this.services); _f < _g.length; _f++) {
1175
+ service = _g[_f];
1176
+ service.ctx = this.ctx;
1177
+ }
1178
+ servicesWithInit = Object.values(this.services).filter(function (s) { return typeof s.onInit === "function"; });
1179
+ if (!(servicesWithInit.length > 0)) return [3 /*break*/, 2];
1180
+ return [4 /*yield*/, Promise.all(servicesWithInit.map(function (s) { return s.onInit(); }))];
1181
+ case 1:
1182
+ _h.sent();
1183
+ _h.label = 2;
1184
+ case 2: return [2 /*return*/];
1155
1185
  }
1156
- return [2 /*return*/];
1157
1186
  });
1158
1187
  });
1159
1188
  };
@@ -1,6 +1,7 @@
1
1
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
2
2
  import { FlinkRepo } from "./FlinkRepo";
3
3
  import { FlinkAgent } from "./ai/FlinkAgent";
4
+ import { FlinkService } from "./FlinkService";
4
5
  export interface FlinkContext<P = any> {
5
6
  repos: {
6
7
  [x: string]: FlinkRepo<any, any>;
@@ -30,4 +31,23 @@ export interface FlinkContext<P = any> {
30
31
  agents?: {
31
32
  [x: string]: FlinkAgent<any>;
32
33
  };
34
+ /**
35
+ * Optional services namespace for shared business logic.
36
+ *
37
+ * Define services directly in your context interface:
38
+ *
39
+ * @example
40
+ * interface AppCtx extends FlinkContext<PluginCtx> {
41
+ * repos: {
42
+ * carRepo: CarRepo;
43
+ * };
44
+ * services: {
45
+ * carService: CarService;
46
+ * orderService: OrderService;
47
+ * };
48
+ * }
49
+ */
50
+ services?: {
51
+ [x: string]: FlinkService<any>;
52
+ };
33
53
  }
@@ -0,0 +1,38 @@
1
+ import { FlinkContext } from "./FlinkContext";
2
+ /**
3
+ * Base class for Flink services - optional business logic layer.
4
+ *
5
+ * Services provide a place for shared business logic that can be used by
6
+ * handlers, jobs, agents, and other services via `ctx.services`.
7
+ *
8
+ * Context (`this.ctx`) is not available in the constructor - use `onInit()`
9
+ * for any setup that requires context.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * class CarService extends FlinkService<AppCtx> {
14
+ * async onInit() {
15
+ * // Called after all services are instantiated and ctx is fully wired
16
+ * }
17
+ *
18
+ * async getWithOwner(carId: string) {
19
+ * const car = await this.ctx.repos.carRepo.getById(carId);
20
+ * if (!car) throw notFound("Car not found");
21
+ * return car;
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export declare abstract class FlinkService<C extends FlinkContext> {
27
+ private _ctx?;
28
+ set ctx(ctx: FlinkContext);
29
+ get ctx(): C;
30
+ /**
31
+ * Optional async initialization hook called after all services are
32
+ * instantiated and ctx is fully wired (repos, plugins, agents, services all available).
33
+ *
34
+ * All service onInit() methods run in parallel via Promise.all.
35
+ * Do not depend on another service's onInit() having completed.
36
+ */
37
+ onInit?(): Promise<void>;
38
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FlinkService = void 0;
4
+ /**
5
+ * Base class for Flink services - optional business logic layer.
6
+ *
7
+ * Services provide a place for shared business logic that can be used by
8
+ * handlers, jobs, agents, and other services via `ctx.services`.
9
+ *
10
+ * Context (`this.ctx`) is not available in the constructor - use `onInit()`
11
+ * for any setup that requires context.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * class CarService extends FlinkService<AppCtx> {
16
+ * async onInit() {
17
+ * // Called after all services are instantiated and ctx is fully wired
18
+ * }
19
+ *
20
+ * async getWithOwner(carId: string) {
21
+ * const car = await this.ctx.repos.carRepo.getById(carId);
22
+ * if (!car) throw notFound("Car not found");
23
+ * return car;
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ var FlinkService = /** @class */ (function () {
29
+ function FlinkService() {
30
+ }
31
+ Object.defineProperty(FlinkService.prototype, "ctx", {
32
+ get: function () {
33
+ if (!this._ctx) {
34
+ throw new Error("FlinkService: ctx is not available in constructor. Use onInit() for setup logic.");
35
+ }
36
+ return this._ctx;
37
+ },
38
+ set: function (ctx) {
39
+ this._ctx = ctx;
40
+ },
41
+ enumerable: false,
42
+ configurable: true
43
+ });
44
+ return FlinkService;
45
+ }());
46
+ exports.FlinkService = FlinkService;
@@ -21,6 +21,7 @@ declare class TypeScriptCompiler {
21
21
  private toolFiles;
22
22
  private agentFiles;
23
23
  private jobFiles;
24
+ private serviceFiles;
24
25
  /**
25
26
  * Tool ID registry for agent validation (built during segmentation)
26
27
  */
@@ -29,6 +30,7 @@ declare class TypeScriptCompiler {
29
30
  * Compiler plugins loaded from flink.config.js
30
31
  */
31
32
  private compilerPlugins;
33
+ private disableServices;
32
34
  /**
33
35
  * Extension files collected during segmentation, keyed by generatedFile name
34
36
  */
@@ -215,6 +217,10 @@ declare class TypeScriptCompiler {
215
217
  * Scans project for jobs so they can be registered during start.
216
218
  */
217
219
  parseJobs(): Promise<SourceFile>;
220
+ /**
221
+ * Scans project for services so they can be registered during start.
222
+ */
223
+ parseServices(): Promise<SourceFile>;
218
224
  /**
219
225
  * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
220
226
  * Mirrors the same namespace-import + spread pattern used by parseJobs.
@@ -95,7 +95,7 @@ var perfLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.perf");
95
95
  var initLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
96
96
  var TypeScriptCompiler = /** @class */ (function () {
97
97
  function TypeScriptCompiler(cwd) {
98
- var _a, _b, _c;
98
+ var _a, _b, _c, _d;
99
99
  /**
100
100
  * Handler schemas collected during parseHandlers, to be generated later
101
101
  */
@@ -112,6 +112,7 @@ var TypeScriptCompiler = /** @class */ (function () {
112
112
  this.toolFiles = [];
113
113
  this.agentFiles = [];
114
114
  this.jobFiles = [];
115
+ this.serviceFiles = [];
115
116
  /**
116
117
  * Tool ID registry for agent validation (built during segmentation)
117
118
  */
@@ -120,6 +121,7 @@ var TypeScriptCompiler = /** @class */ (function () {
120
121
  * Compiler plugins loaded from flink.config.js
121
122
  */
122
123
  this.compilerPlugins = [];
124
+ this.disableServices = false;
123
125
  /**
124
126
  * Extension files collected during segmentation, keyed by generatedFile name
125
127
  */
@@ -198,9 +200,10 @@ var TypeScriptCompiler = /** @class */ (function () {
198
200
  var loadTime = Date.now() - loadStartTime;
199
201
  var fileCount = this.project.getSourceFiles().length;
200
202
  perfLog.debug("\u2713 All source files loaded in ".concat(loadTime, "ms (").concat(fileCount, " files)"));
201
- // Load compiler plugins from flink.config.js
203
+ // Load config from flink.config.js
202
204
  var flinkCfg = (0, loadFlinkConfig_1.loadFlinkConfig)(this.cwd);
203
205
  this.compilerPlugins = (_c = flinkCfg === null || flinkCfg === void 0 ? void 0 : flinkCfg.compilerPlugins) !== null && _c !== void 0 ? _c : [];
206
+ this.disableServices = (_d = flinkCfg === null || flinkCfg === void 0 ? void 0 : flinkCfg.disableServices) !== null && _d !== void 0 ? _d : false;
204
207
  if (this.compilerPlugins.length > 0) {
205
208
  initLog.info("Compiler plugins loaded (".concat(this.compilerPlugins.length, "):"), this.compilerPlugins.map(function (p) { return "".concat(p.package, " \u2192 ").concat(p.scanDir); }).join(", "));
206
209
  }
@@ -444,7 +447,7 @@ var TypeScriptCompiler = /** @class */ (function () {
444
447
  var _a;
445
448
  var startTime = Date.now();
446
449
  var allFiles = this.project.getSourceFiles();
447
- var handlerTime = 0, repoTime = 0, toolTime = 0, agentTime = 0, jobTime = 0;
450
+ var handlerTime = 0, repoTime = 0, toolTime = 0, agentTime = 0, jobTime = 0, serviceTime = 0;
448
451
  for (var _i = 0, allFiles_1 = allFiles; _i < allFiles_1.length; _i++) {
449
452
  var sf = allFiles_1[_i];
450
453
  var filePath = sf.getFilePath();
@@ -491,6 +494,12 @@ var TypeScriptCompiler = /** @class */ (function () {
491
494
  jobTime += Date.now() - fileStartTime;
492
495
  continue;
493
496
  }
497
+ // Services: simple path check (opt-out via flink.config.js disableServices)
498
+ if (!this.disableServices && filePath.includes("src/services/")) {
499
+ this.serviceFiles.push(sf);
500
+ serviceTime += Date.now() - fileStartTime;
501
+ continue;
502
+ }
494
503
  // Extension dirs from compiler plugins
495
504
  for (var _b = 0, _c = this.compilerPlugins; _b < _c.length; _b++) {
496
505
  var ext = _c[_b];
@@ -506,7 +515,7 @@ var TypeScriptCompiler = /** @class */ (function () {
506
515
  }
507
516
  var segmentTime = Date.now() - startTime;
508
517
  perfLog.debug("\u2713 File segmentation completed in ".concat(segmentTime, "ms ") +
509
- "(".concat(this.handlerFiles.length, " handlers, ").concat(this.repoFiles.length, " repos, ").concat(this.toolFiles.length, " tools, ").concat(this.agentFiles.length, " agents, ").concat(this.jobFiles.length, " jobs)"));
518
+ "(".concat(this.handlerFiles.length, " handlers, ").concat(this.repoFiles.length, " repos, ").concat(this.toolFiles.length, " tools, ").concat(this.agentFiles.length, " agents, ").concat(this.jobFiles.length, " jobs, ").concat(this.serviceFiles.length, " services)"));
510
519
  };
511
520
  /**
512
521
  * Detects if the project is using ESM (ECMAScript Modules)
@@ -1172,7 +1181,7 @@ var TypeScriptCompiler = /** @class */ (function () {
1172
1181
  extensionImports = this.compilerPlugins
1173
1182
  .map(function (ext) { return "import \"./".concat(ext.generatedFile).concat(_this.isEsm ? ".js" : "", "\";"); })
1174
1183
  .join("\n");
1175
- sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedRepos").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedTools").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedAgents").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedJobs").concat(this.isEsm ? ".js" : "", "\";\n").concat(extensionImports ? extensionImports + "\n" : "", "import \"..").concat(appEntryScript.replace(/\.ts/g, "")).concat(this.isEsm ? ".js" : "", "\";\nexport default {}; // Export an empty object to make it a module\n"));
1184
+ sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedRepos").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedTools").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedAgents").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedJobs").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedServices").concat(this.isEsm ? ".js" : "", "\";\n").concat(extensionImports ? extensionImports + "\n" : "", "import \"..").concat(appEntryScript.replace(/\.ts/g, "")).concat(this.isEsm ? ".js" : "", "\";\nexport default {}; // Export an empty object to make it a module\n"));
1176
1185
  // Defer save until batch save at end (performance optimization)
1177
1186
  return [2 /*return*/, sf];
1178
1187
  }
@@ -1518,6 +1527,42 @@ var TypeScriptCompiler = /** @class */ (function () {
1518
1527
  });
1519
1528
  });
1520
1529
  };
1530
+ /**
1531
+ * Scans project for services so they can be registered during start.
1532
+ */
1533
+ TypeScriptCompiler.prototype.parseServices = function () {
1534
+ return __awaiter(this, void 0, void 0, function () {
1535
+ var startTime, generatedFile, servicesArr, imports, serviceElements, _i, _a, sf, serviceParseTime;
1536
+ return __generator(this, function (_b) {
1537
+ if (this.disableServices) {
1538
+ initLog.info("Services disabled via flink.config.js (disableServices: true)");
1539
+ }
1540
+ startTime = Date.now();
1541
+ generatedFile = this.createSourceFile(["generatedServices.ts"], "// Generated ".concat(new Date(), "\nimport { autoRegisteredServices } from \"@flink-app/flink\";\nexport const services: any[] = [];\nautoRegisteredServices.push(...services);\n "));
1542
+ servicesArr = generatedFile.getVariableDeclarationOrThrow("services").getFirstDescendantByKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
1543
+ imports = [];
1544
+ serviceElements = [];
1545
+ for (_i = 0, _a = this.serviceFiles; _i < _a.length; _i++) {
1546
+ sf = _a[_i];
1547
+ console.log("Detected service ".concat(sf.getBaseName()));
1548
+ imports.push({
1549
+ defaultImport: sf.getBaseNameWithoutExtension(),
1550
+ moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
1551
+ });
1552
+ serviceElements.push("{serviceInstanceName: \"".concat((0, utils_1.getRepoInstanceName)(sf.getBaseName()), "\", Service: ").concat(sf.getBaseNameWithoutExtension(), "}"));
1553
+ }
1554
+ servicesArr.addElements(serviceElements);
1555
+ generatedFile.addImportDeclarations(imports);
1556
+ // Cleanup: forget service file nodes to reduce memory overhead
1557
+ this.serviceFiles.forEach(function (sf) {
1558
+ sf.getClasses().forEach(function (cls) { return cls.forget(); });
1559
+ });
1560
+ serviceParseTime = Date.now() - startTime;
1561
+ perfLog.info("\u2713 Service parsing completed in ".concat(serviceParseTime, "ms (").concat(serviceElements.length, " services)"));
1562
+ return [2 /*return*/, generatedFile];
1563
+ });
1564
+ });
1565
+ };
1521
1566
  /**
1522
1567
  * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
1523
1568
  * Mirrors the same namespace-import + spread pattern used by parseJobs.
@@ -10,6 +10,7 @@ export * from "./FlinkRequestContext";
10
10
  export * from "./FlinkErrors";
11
11
  export * from "./FlinkPlugin";
12
12
  export * from "./FlinkJob";
13
+ export * from "./FlinkService";
13
14
  export { LeaderElection } from "./LeaderElection";
14
15
  export type { LeaderElectionOptions } from "./LeaderElection";
15
16
  export * from "./auth/FlinkAuthUser";
package/dist/src/index.js CHANGED
@@ -27,6 +27,7 @@ __exportStar(require("./FlinkRequestContext"), exports);
27
27
  __exportStar(require("./FlinkErrors"), exports);
28
28
  __exportStar(require("./FlinkPlugin"), exports);
29
29
  __exportStar(require("./FlinkJob"), exports);
30
+ __exportStar(require("./FlinkService"), exports);
30
31
  var LeaderElection_1 = require("./LeaderElection");
31
32
  Object.defineProperty(exports, "LeaderElection", { enumerable: true, get: function () { return LeaderElection_1.LeaderElection; } });
32
33
  __exportStar(require("./auth/FlinkAuthUser"), exports);
@@ -26,6 +26,8 @@ export interface FlinkConfig {
26
26
  logging?: LoggingConfig;
27
27
  /** Compiler plugins that extend auto-discovery to custom directories */
28
28
  compilerPlugins?: FlinkCompilerPlugin[];
29
+ /** Disable auto-discovery of services from src/services/. Default: false */
30
+ disableServices?: boolean;
29
31
  }
30
32
  /**
31
33
  * Load Flink configuration from flink.config.js in current working directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/flink",
3
- "version": "2.0.0-alpha.81",
3
+ "version": "2.0.0-alpha.83",
4
4
  "description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "main": "dist/src/index.js",
package/src/FlinkApp.ts CHANGED
@@ -23,6 +23,7 @@ import { log } from "./FlinkLog";
23
23
  import { FlinkLogFactory } from "./FlinkLogFactory";
24
24
  import { FlinkPlugin } from "./FlinkPlugin";
25
25
  import { FlinkRepo } from "./FlinkRepo";
26
+ import { FlinkService } from "./FlinkService";
26
27
  import { FlinkResponse } from "./FlinkResponse";
27
28
  import { requestContext } from "./FlinkRequestContext";
28
29
  import { StreamWriterFactory } from "./handlers/StreamWriterFactory";
@@ -88,6 +89,15 @@ export const autoRegisteredTools: FlinkToolFile[] = [];
88
89
  */
89
90
  export const autoRegisteredAgents: FlinkAgentFile<any, any>[] = [];
90
91
 
92
+ /**
93
+ * This will be populated at compile time when the apps services
94
+ * are picked up by TypeScript compiler
95
+ */
96
+ export const autoRegisteredServices: {
97
+ serviceInstanceName: string;
98
+ Service: any;
99
+ }[] = [];
100
+
91
101
  export interface FlinkOptions {
92
102
  /**
93
103
  * Name of application, will only show in logs and in HTTP header.
@@ -338,6 +348,7 @@ export class FlinkApp<C extends FlinkContext> {
338
348
  private onError?: FlinkOptions["onError"];
339
349
 
340
350
  private repos: { [x: string]: FlinkRepo<C, any> } = {};
351
+ private services: { [x: string]: FlinkService<C> } = {};
341
352
 
342
353
  private llmAdapters: Map<string, LLMAdapter> = new Map();
343
354
  private tools: { [x: string]: ToolExecutor<C> } = {};
@@ -1443,16 +1454,35 @@ export class FlinkApp<C extends FlinkContext> {
1443
1454
  return out;
1444
1455
  }, {});
1445
1456
 
1457
+ // Instantiate services (ctx not yet available - constructors must not access it)
1458
+ for (const { serviceInstanceName, Service } of autoRegisteredServices) {
1459
+ const serviceInstance: FlinkService<C> = new Service();
1460
+ this.services[serviceInstanceName] = serviceInstance;
1461
+ initLog.info(`Registered service ${serviceInstanceName}`);
1462
+ }
1463
+
1446
1464
  this._ctx = {
1447
1465
  repos: this.repos,
1448
1466
  plugins: pluginCtx,
1449
1467
  auth: this.auth,
1450
1468
  agents: this.agents,
1469
+ services: this.services,
1451
1470
  } as C;
1452
1471
 
1472
+ // Inject context into repos
1453
1473
  for (const repo of Object.values(this.repos)) {
1454
1474
  repo.ctx = this.ctx;
1455
1475
  }
1476
+
1477
+ // Inject context into services, then call onInit() in parallel
1478
+ for (const service of Object.values(this.services)) {
1479
+ service.ctx = this.ctx;
1480
+ }
1481
+
1482
+ const servicesWithInit = Object.values(this.services).filter((s) => typeof s.onInit === "function");
1483
+ if (servicesWithInit.length > 0) {
1484
+ await Promise.all(servicesWithInit.map((s) => s.onInit!()));
1485
+ }
1456
1486
  }
1457
1487
 
1458
1488
  /**
@@ -1,6 +1,7 @@
1
1
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
2
2
  import { FlinkRepo } from "./FlinkRepo";
3
3
  import { FlinkAgent } from "./ai/FlinkAgent";
4
+ import { FlinkService } from "./FlinkService";
4
5
 
5
6
  export interface FlinkContext<P = any> {
6
7
  repos: {
@@ -34,4 +35,24 @@ export interface FlinkContext<P = any> {
34
35
  agents?: {
35
36
  [x: string]: FlinkAgent<any>;
36
37
  };
38
+
39
+ /**
40
+ * Optional services namespace for shared business logic.
41
+ *
42
+ * Define services directly in your context interface:
43
+ *
44
+ * @example
45
+ * interface AppCtx extends FlinkContext<PluginCtx> {
46
+ * repos: {
47
+ * carRepo: CarRepo;
48
+ * };
49
+ * services: {
50
+ * carService: CarService;
51
+ * orderService: OrderService;
52
+ * };
53
+ * }
54
+ */
55
+ services?: {
56
+ [x: string]: FlinkService<any>;
57
+ };
37
58
  }
@@ -0,0 +1,49 @@
1
+ import { FlinkContext } from "./FlinkContext";
2
+
3
+ /**
4
+ * Base class for Flink services - optional business logic layer.
5
+ *
6
+ * Services provide a place for shared business logic that can be used by
7
+ * handlers, jobs, agents, and other services via `ctx.services`.
8
+ *
9
+ * Context (`this.ctx`) is not available in the constructor - use `onInit()`
10
+ * for any setup that requires context.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * class CarService extends FlinkService<AppCtx> {
15
+ * async onInit() {
16
+ * // Called after all services are instantiated and ctx is fully wired
17
+ * }
18
+ *
19
+ * async getWithOwner(carId: string) {
20
+ * const car = await this.ctx.repos.carRepo.getById(carId);
21
+ * if (!car) throw notFound("Car not found");
22
+ * return car;
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export abstract class FlinkService<C extends FlinkContext> {
28
+ private _ctx?: C;
29
+
30
+ set ctx(ctx: FlinkContext) {
31
+ this._ctx = ctx as C;
32
+ }
33
+
34
+ get ctx(): C {
35
+ if (!this._ctx) {
36
+ throw new Error("FlinkService: ctx is not available in constructor. Use onInit() for setup logic.");
37
+ }
38
+ return this._ctx;
39
+ }
40
+
41
+ /**
42
+ * Optional async initialization hook called after all services are
43
+ * instantiated and ctx is fully wired (repos, plugins, agents, services all available).
44
+ *
45
+ * All service onInit() methods run in parallel via Promise.all.
46
+ * Do not depend on another service's onInit() having completed.
47
+ */
48
+ onInit?(): Promise<void>;
49
+ }
@@ -45,6 +45,7 @@ class TypeScriptCompiler {
45
45
  private toolFiles: SourceFile[] = [];
46
46
  private agentFiles: SourceFile[] = [];
47
47
  private jobFiles: SourceFile[] = [];
48
+ private serviceFiles: SourceFile[] = [];
48
49
 
49
50
  /**
50
51
  * Tool ID registry for agent validation (built during segmentation)
@@ -55,6 +56,7 @@ class TypeScriptCompiler {
55
56
  * Compiler plugins loaded from flink.config.js
56
57
  */
57
58
  private compilerPlugins: FlinkCompilerPlugin[] = [];
59
+ private disableServices = false;
58
60
 
59
61
  /**
60
62
  * Extension files collected during segmentation, keyed by generatedFile name
@@ -227,9 +229,10 @@ class TypeScriptCompiler {
227
229
  const fileCount = this.project.getSourceFiles().length;
228
230
  perfLog.debug(`✓ All source files loaded in ${loadTime}ms (${fileCount} files)`);
229
231
 
230
- // Load compiler plugins from flink.config.js
232
+ // Load config from flink.config.js
231
233
  const flinkCfg = loadFlinkConfig(this.cwd);
232
234
  this.compilerPlugins = flinkCfg?.compilerPlugins ?? [];
235
+ this.disableServices = flinkCfg?.disableServices ?? false;
233
236
 
234
237
  if (this.compilerPlugins.length > 0) {
235
238
  initLog.info(
@@ -429,7 +432,8 @@ class TypeScriptCompiler {
429
432
  repoTime = 0,
430
433
  toolTime = 0,
431
434
  agentTime = 0,
432
- jobTime = 0;
435
+ jobTime = 0,
436
+ serviceTime = 0;
433
437
 
434
438
  for (const sf of allFiles) {
435
439
  const filePath = sf.getFilePath();
@@ -484,6 +488,13 @@ class TypeScriptCompiler {
484
488
  continue;
485
489
  }
486
490
 
491
+ // Services: simple path check (opt-out via flink.config.js disableServices)
492
+ if (!this.disableServices && filePath.includes("src/services/")) {
493
+ this.serviceFiles.push(sf);
494
+ serviceTime += Date.now() - fileStartTime;
495
+ continue;
496
+ }
497
+
487
498
  // Extension dirs from compiler plugins
488
499
  for (const ext of this.compilerPlugins) {
489
500
  if (filePath.includes(ext.scanDir)) {
@@ -500,7 +511,7 @@ class TypeScriptCompiler {
500
511
  const segmentTime = Date.now() - startTime;
501
512
  perfLog.debug(
502
513
  `✓ File segmentation completed in ${segmentTime}ms ` +
503
- `(${this.handlerFiles.length} handlers, ${this.repoFiles.length} repos, ${this.toolFiles.length} tools, ${this.agentFiles.length} agents, ${this.jobFiles.length} jobs)`
514
+ `(${this.handlerFiles.length} handlers, ${this.repoFiles.length} repos, ${this.toolFiles.length} tools, ${this.agentFiles.length} agents, ${this.jobFiles.length} jobs, ${this.serviceFiles.length} services)`
504
515
  );
505
516
  }
506
517
 
@@ -1205,6 +1216,7 @@ import "./generatedRepos${this.isEsm ? ".js" : ""}";
1205
1216
  import "./generatedTools${this.isEsm ? ".js" : ""}";
1206
1217
  import "./generatedAgents${this.isEsm ? ".js" : ""}";
1207
1218
  import "./generatedJobs${this.isEsm ? ".js" : ""}";
1219
+ import "./generatedServices${this.isEsm ? ".js" : ""}";
1208
1220
  ${extensionImports ? extensionImports + "\n" : ""}import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
1209
1221
  export default {}; // Export an empty object to make it a module
1210
1222
  `
@@ -1604,6 +1616,59 @@ autoRegisteredJobs.push(...jobs);
1604
1616
  return generatedFile;
1605
1617
  }
1606
1618
 
1619
+ /**
1620
+ * Scans project for services so they can be registered during start.
1621
+ */
1622
+ async parseServices() {
1623
+ if (this.disableServices) {
1624
+ initLog.info("Services disabled via flink.config.js (disableServices: true)");
1625
+ }
1626
+
1627
+ const startTime = Date.now();
1628
+
1629
+ const generatedFile = this.createSourceFile(
1630
+ ["generatedServices.ts"],
1631
+ `// Generated ${new Date()}
1632
+ import { autoRegisteredServices } from "@flink-app/flink";
1633
+ export const services: any[] = [];
1634
+ autoRegisteredServices.push(...services);
1635
+ `
1636
+ );
1637
+
1638
+ const servicesArr = generatedFile.getVariableDeclarationOrThrow("services").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
1639
+
1640
+ const imports: OptionalKind<ImportDeclarationStructure>[] = [];
1641
+
1642
+ const serviceElements: string[] = [];
1643
+
1644
+ for (const sf of this.serviceFiles) {
1645
+ console.log(`Detected service ${sf.getBaseName()}`);
1646
+
1647
+ imports.push({
1648
+ defaultImport: sf.getBaseNameWithoutExtension(),
1649
+ moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
1650
+ });
1651
+
1652
+ serviceElements.push(
1653
+ `{serviceInstanceName: "${getRepoInstanceName(sf.getBaseName())}", Service: ${sf.getBaseNameWithoutExtension()}}`
1654
+ );
1655
+ }
1656
+
1657
+ servicesArr.addElements(serviceElements);
1658
+
1659
+ generatedFile.addImportDeclarations(imports);
1660
+
1661
+ // Cleanup: forget service file nodes to reduce memory overhead
1662
+ this.serviceFiles.forEach((sf) => {
1663
+ sf.getClasses().forEach((cls) => cls.forget());
1664
+ });
1665
+
1666
+ const serviceParseTime = Date.now() - startTime;
1667
+ perfLog.info(`✓ Service parsing completed in ${serviceParseTime}ms (${serviceElements.length} services)`);
1668
+
1669
+ return generatedFile;
1670
+ }
1671
+
1607
1672
  /**
1608
1673
  * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
1609
1674
  * Mirrors the same namespace-import + spread pattern used by parseJobs.
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./FlinkRequestContext";
10
10
  export * from "./FlinkErrors";
11
11
  export * from "./FlinkPlugin";
12
12
  export * from "./FlinkJob";
13
+ export * from "./FlinkService";
13
14
  export { LeaderElection } from "./LeaderElection";
14
15
  export type { LeaderElectionOptions } from "./LeaderElection";
15
16
  export * from "./auth/FlinkAuthUser";
@@ -30,6 +30,8 @@ export interface FlinkConfig {
30
30
  logging?: LoggingConfig;
31
31
  /** Compiler plugins that extend auto-discovery to custom directories */
32
32
  compilerPlugins?: FlinkCompilerPlugin[];
33
+ /** Disable auto-discovery of services from src/services/. Default: false */
34
+ disableServices?: boolean;
33
35
  }
34
36
 
35
37
  /**