@bluelibs/runner 3.3.2 → 3.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.
Files changed (75) hide show
  1. package/README.md +437 -33
  2. package/dist/define.d.ts +5 -5
  3. package/dist/define.js +22 -2
  4. package/dist/define.js.map +1 -1
  5. package/dist/defs.d.ts +55 -21
  6. package/dist/defs.js.map +1 -1
  7. package/dist/defs.returnTag.d.ts +36 -0
  8. package/dist/defs.returnTag.js +4 -0
  9. package/dist/defs.returnTag.js.map +1 -0
  10. package/dist/errors.d.ts +60 -10
  11. package/dist/errors.js +103 -12
  12. package/dist/errors.js.map +1 -1
  13. package/dist/globals/globalMiddleware.d.ts +4 -4
  14. package/dist/globals/globalResources.d.ts +28 -10
  15. package/dist/globals/middleware/cache.middleware.d.ts +9 -9
  16. package/dist/globals/resources/queue.resource.d.ts +5 -2
  17. package/dist/index.d.ts +33 -14
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/models/DependencyProcessor.js +4 -4
  21. package/dist/models/DependencyProcessor.js.map +1 -1
  22. package/dist/models/EventManager.js +10 -1
  23. package/dist/models/EventManager.js.map +1 -1
  24. package/dist/models/Logger.d.ts +8 -0
  25. package/dist/models/Logger.js +24 -0
  26. package/dist/models/Logger.js.map +1 -1
  27. package/dist/models/OverrideManager.js +1 -1
  28. package/dist/models/OverrideManager.js.map +1 -1
  29. package/dist/models/ResourceInitializer.d.ts +2 -2
  30. package/dist/models/ResourceInitializer.js.map +1 -1
  31. package/dist/models/Store.d.ts +2 -2
  32. package/dist/models/Store.js +1 -1
  33. package/dist/models/Store.js.map +1 -1
  34. package/dist/models/StoreConstants.d.ts +6 -3
  35. package/dist/models/StoreRegistry.d.ts +2 -2
  36. package/dist/models/StoreRegistry.js +1 -1
  37. package/dist/models/StoreRegistry.js.map +1 -1
  38. package/dist/models/StoreTypes.d.ts +1 -1
  39. package/dist/models/StoreValidator.js +5 -5
  40. package/dist/models/StoreValidator.js.map +1 -1
  41. package/dist/models/TaskRunner.js +10 -0
  42. package/dist/models/TaskRunner.js.map +1 -1
  43. package/dist/run.d.ts +3 -3
  44. package/dist/run.js +1 -1
  45. package/dist/run.js.map +1 -1
  46. package/dist/t1.d.ts +1 -0
  47. package/dist/t1.js +13 -0
  48. package/dist/t1.js.map +1 -0
  49. package/dist/testing.d.ts +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/errors.test.ts +92 -11
  52. package/src/__tests__/models/EventManager.test.ts +0 -1
  53. package/src/__tests__/models/Logger.test.ts +82 -5
  54. package/src/__tests__/recursion/c.resource.ts +1 -1
  55. package/src/__tests__/run.overrides.test.ts +3 -3
  56. package/src/__tests__/typesafety.test.ts +112 -9
  57. package/src/__tests__/validation-edge-cases.test.ts +111 -0
  58. package/src/__tests__/validation-interface.test.ts +428 -0
  59. package/src/define.ts +47 -15
  60. package/src/defs.returnTag.ts +91 -0
  61. package/src/defs.ts +84 -27
  62. package/src/errors.ts +95 -23
  63. package/src/index.ts +1 -0
  64. package/src/models/DependencyProcessor.ts +9 -5
  65. package/src/models/EventManager.ts +12 -3
  66. package/src/models/Logger.ts +28 -0
  67. package/src/models/OverrideManager.ts +2 -7
  68. package/src/models/ResourceInitializer.ts +8 -3
  69. package/src/models/Store.ts +3 -3
  70. package/src/models/StoreRegistry.ts +2 -2
  71. package/src/models/StoreTypes.ts +1 -1
  72. package/src/models/StoreValidator.ts +6 -6
  73. package/src/models/TaskRunner.ts +10 -1
  74. package/src/run.ts +8 -5
  75. package/src/testing.ts +1 -1
@@ -0,0 +1,428 @@
1
+ import { defineTask, defineResource, defineEvent, defineMiddleware } from "../define";
2
+ import { run } from "../run";
3
+ import { IValidationSchema } from "../defs";
4
+
5
+ // Simple mock validation schemas for testing the interface
6
+ class MockValidationSchema<T> implements IValidationSchema<T> {
7
+ constructor(
8
+ private validator: (input: unknown) => T,
9
+ private errorMessage?: string
10
+ ) {}
11
+
12
+ parse(input: unknown): T {
13
+ try {
14
+ return this.validator(input);
15
+ } catch (error) {
16
+ throw new Error(this.errorMessage || "Validation failed");
17
+ }
18
+ }
19
+ }
20
+
21
+ // Helper functions to create mock schemas similar to Zod
22
+ const mockSchema = {
23
+ object: <T extends Record<string, any>>(shape: Record<keyof T, string>): IValidationSchema<T> => {
24
+ return new MockValidationSchema((input: unknown) => {
25
+ if (typeof input !== "object" || input === null) {
26
+ throw new Error("Expected object");
27
+ }
28
+ const obj = input as any;
29
+ for (const [key, expectedType] of Object.entries(shape)) {
30
+ if (expectedType === "string" && typeof obj[key] !== "string") {
31
+ throw new Error(`${key} must be string`);
32
+ }
33
+ if (expectedType === "number" && typeof obj[key] !== "number") {
34
+ throw new Error(`${key} must be number`);
35
+ }
36
+ if (expectedType === "boolean" && typeof obj[key] !== "boolean") {
37
+ throw new Error(`${key} must be boolean`);
38
+ }
39
+ }
40
+ return obj as T;
41
+ });
42
+ },
43
+
44
+ string: (): IValidationSchema<string> => {
45
+ return new MockValidationSchema((input: unknown) => {
46
+ if (typeof input !== "string") {
47
+ throw new Error("Expected string");
48
+ }
49
+ return input;
50
+ });
51
+ },
52
+
53
+ number: (): IValidationSchema<number> => {
54
+ return new MockValidationSchema((input: unknown) => {
55
+ if (typeof input !== "number") {
56
+ throw new Error("Expected number");
57
+ }
58
+ return input;
59
+ });
60
+ },
61
+
62
+ boolean: (): IValidationSchema<boolean> => {
63
+ return new MockValidationSchema((input: unknown) => {
64
+ if (typeof input !== "boolean") {
65
+ throw new Error("Expected boolean");
66
+ }
67
+ return input;
68
+ });
69
+ },
70
+
71
+ transform: <T, U>(schema: IValidationSchema<T>, transformer: (value: T) => U): IValidationSchema<U> => {
72
+ return new MockValidationSchema((input: unknown) => {
73
+ const validated = schema.parse(input);
74
+ return transformer(validated);
75
+ });
76
+ },
77
+
78
+ withDefaults: <T>(defaultValue: T): IValidationSchema<T> => {
79
+ return new MockValidationSchema((input: unknown) => {
80
+ return input === undefined ? defaultValue : (input as T);
81
+ });
82
+ },
83
+ };
84
+
85
+ describe("Generic Validation Interface", () => {
86
+ describe("Task Input Validation", () => {
87
+ it("should validate task input successfully with valid data", async () => {
88
+ const userSchema = mockSchema.object<{
89
+ name: string;
90
+ age: number;
91
+ }>({
92
+ name: "string",
93
+ age: "number",
94
+ });
95
+
96
+ const createUserTask = defineTask({
97
+ id: "task.createUser",
98
+ inputSchema: userSchema,
99
+ run: async (input) => {
100
+ return `Created user ${(input as any).name}`;
101
+ },
102
+ });
103
+
104
+ const app = defineResource({
105
+ id: "app",
106
+ register: [createUserTask],
107
+ dependencies: { createUserTask },
108
+ init: async (_, { createUserTask }) => {
109
+ const result = await createUserTask({
110
+ name: "John Doe",
111
+ age: 30,
112
+ });
113
+ expect(result).toBe("Created user John Doe");
114
+ return result;
115
+ },
116
+ });
117
+
118
+ await run(app);
119
+ });
120
+
121
+ it("should throw validation error for invalid task input", async () => {
122
+ const userSchema = new MockValidationSchema((input: unknown) => {
123
+ if (typeof input !== "object" || input === null) {
124
+ throw new Error("Expected object");
125
+ }
126
+ const obj = input as any;
127
+ if (typeof obj.name !== "string") {
128
+ throw new Error("name must be string");
129
+ }
130
+ if (typeof obj.age !== "number" || obj.age < 0) {
131
+ throw new Error("age must be positive number");
132
+ }
133
+ return obj;
134
+ });
135
+
136
+ const createUserTask = defineTask({
137
+ id: "task.createUser.invalid",
138
+ inputSchema: userSchema,
139
+ run: async (input) => {
140
+ return `Created user ${(input as any).name}`;
141
+ },
142
+ });
143
+
144
+ const app = defineResource({
145
+ id: "app",
146
+ register: [createUserTask],
147
+ dependencies: { createUserTask },
148
+ init: async (_, { createUserTask }) => {
149
+ await createUserTask({
150
+ name: "John Doe",
151
+ age: -5, // Invalid: negative age
152
+ });
153
+ },
154
+ });
155
+
156
+ await expect(run(app)).rejects.toThrow(/Task input validation failed/);
157
+ });
158
+
159
+ it("should transform input data when schema supports it", async () => {
160
+ const stringToNumberSchema = mockSchema.transform(
161
+ mockSchema.string(),
162
+ (val: string) => parseInt(val, 10)
163
+ );
164
+
165
+ const mathTask = defineTask({
166
+ id: "task.math",
167
+ inputSchema: stringToNumberSchema,
168
+ run: async (input: number) => {
169
+ expect(typeof input).toBe("number");
170
+ return input * 2;
171
+ },
172
+ });
173
+
174
+ const app = defineResource({
175
+ id: "app",
176
+ register: [mathTask],
177
+ dependencies: { mathTask },
178
+ init: async (_, { mathTask }) => {
179
+ const result = await (mathTask as any)("42"); // String input should be transformed to number
180
+ expect(result).toBe(84);
181
+ return result;
182
+ },
183
+ });
184
+
185
+ await run(app);
186
+ });
187
+ });
188
+
189
+ describe("Resource Config Validation", () => {
190
+ it("should validate resource config when .with() is called (fail fast)", async () => {
191
+ const configSchema = new MockValidationSchema((input: unknown) => {
192
+ if (typeof input !== "object" || input === null) {
193
+ throw new Error("Expected object");
194
+ }
195
+ const obj = input as any;
196
+ if (typeof obj.host !== "string") {
197
+ throw new Error("host must be string");
198
+ }
199
+ if (typeof obj.port !== "number" || obj.port < 1 || obj.port > 65535) {
200
+ throw new Error("port must be number between 1-65535");
201
+ }
202
+ return obj;
203
+ });
204
+
205
+ const databaseResource = defineResource({
206
+ id: "resource.database",
207
+ configSchema: configSchema,
208
+ init: async (config) => {
209
+ return {
210
+ connect: () => `Connected to ${(config as any).host}:${(config as any).port}`,
211
+ };
212
+ },
213
+ });
214
+
215
+ // This should throw immediately when .with() is called, not during init
216
+ expect(() => {
217
+ databaseResource.with({
218
+ host: "localhost",
219
+ port: 99999, // Invalid: port too high
220
+ });
221
+ }).toThrow(/Resource config validation failed/);
222
+ });
223
+
224
+ it("should work with valid resource config", async () => {
225
+ const configSchema = mockSchema.object<{
226
+ host: string;
227
+ port: number;
228
+ }>({
229
+ host: "string",
230
+ port: "number",
231
+ });
232
+
233
+ const databaseResource = defineResource({
234
+ id: "resource.database.valid",
235
+ configSchema: configSchema,
236
+ init: async (config) => {
237
+ return {
238
+ connect: () => `Connected to ${(config as any).host}:${(config as any).port}`,
239
+ };
240
+ },
241
+ });
242
+
243
+ const app = defineResource({
244
+ id: "app",
245
+ register: [
246
+ databaseResource.with({
247
+ host: "localhost",
248
+ port: 5432,
249
+ }),
250
+ ],
251
+ dependencies: { database: databaseResource },
252
+ init: async (_, { database }) => {
253
+ const result = database.connect();
254
+ expect(result).toBe("Connected to localhost:5432");
255
+ return result;
256
+ },
257
+ });
258
+
259
+ await run(app);
260
+ });
261
+ });
262
+
263
+ describe("Event Payload Validation", () => {
264
+ it("should validate event payload when emitted", async () => {
265
+ const payloadSchema = new MockValidationSchema((input: unknown) => {
266
+ if (typeof input !== "object" || input === null) {
267
+ throw new Error("Expected object");
268
+ }
269
+ const obj = input as any;
270
+ if (typeof obj.message !== "string") {
271
+ throw new Error("message must be string");
272
+ }
273
+ return obj;
274
+ });
275
+
276
+ const testEvent = defineEvent({
277
+ id: "event.test",
278
+ payloadSchema: payloadSchema,
279
+ });
280
+
281
+ let receivedMessage = "";
282
+ const listenerTask = defineTask({
283
+ id: "task.listener",
284
+ on: testEvent,
285
+ run: async (event) => {
286
+ receivedMessage = (event.data as any).message;
287
+ },
288
+ });
289
+
290
+ const app = defineResource({
291
+ id: "app",
292
+ register: [testEvent, listenerTask],
293
+ dependencies: { testEvent },
294
+ init: async (_, { testEvent }) => {
295
+ // This should work with valid payload
296
+ await testEvent({ message: "Hello World" });
297
+ expect(receivedMessage).toBe("Hello World");
298
+
299
+ // This should throw with invalid payload
300
+ await expect(testEvent({ invalidField: 123 } as any)).rejects.toThrow(/Event payload validation failed/);
301
+ },
302
+ });
303
+
304
+ await run(app);
305
+ });
306
+ });
307
+
308
+ describe("Middleware Config Validation", () => {
309
+ it("should validate middleware config when .with() is called (fail fast)", async () => {
310
+ const configSchema = new MockValidationSchema((input: unknown) => {
311
+ if (typeof input !== "object" || input === null) {
312
+ throw new Error("Expected object");
313
+ }
314
+ const obj = input as any;
315
+ if (typeof obj.timeout !== "number" || obj.timeout <= 0) {
316
+ throw new Error("timeout must be positive number");
317
+ }
318
+ return obj;
319
+ });
320
+
321
+ const timingMiddleware = defineMiddleware({
322
+ id: "middleware.timing",
323
+ configSchema: configSchema,
324
+ run: async ({ next }, _, config) => {
325
+ return next();
326
+ },
327
+ });
328
+
329
+ // This should throw immediately when .with() is called
330
+ expect(() => {
331
+ timingMiddleware.with({
332
+ timeout: -5, // Invalid: negative timeout
333
+ });
334
+ }).toThrow(/Middleware config validation failed/);
335
+ });
336
+
337
+ it("should work with valid middleware config", async () => {
338
+ const configSchema = mockSchema.object<{
339
+ timeout: number;
340
+ }>({
341
+ timeout: "number",
342
+ });
343
+
344
+ const timingMiddleware = defineMiddleware({
345
+ id: "middleware.timing.valid",
346
+ configSchema: configSchema,
347
+ run: async ({ next }, _, config) => {
348
+ const start = Date.now();
349
+ const result = await next();
350
+ const duration = Date.now() - start;
351
+ expect(typeof (config as any).timeout).toBe("number");
352
+ return result;
353
+ },
354
+ });
355
+
356
+ const testTask = defineTask({
357
+ id: "task.test",
358
+ middleware: [timingMiddleware.with({ timeout: 5000 })],
359
+ run: async () => {
360
+ return "success";
361
+ },
362
+ });
363
+
364
+ const app = defineResource({
365
+ id: "app",
366
+ register: [timingMiddleware, testTask],
367
+ dependencies: { testTask },
368
+ init: async (_, { testTask }) => {
369
+ const result = await testTask();
370
+ expect(result).toBe("success");
371
+ return result;
372
+ },
373
+ });
374
+
375
+ await run(app);
376
+ });
377
+ });
378
+
379
+ describe("No Validation (Backward Compatibility)", () => {
380
+ it("should work without any validation schemas", async () => {
381
+ const task = defineTask({
382
+ id: "task.noValidation",
383
+ run: async (input: any) => {
384
+ return `Received: ${JSON.stringify(input)}`;
385
+ },
386
+ });
387
+
388
+ const resource = defineResource({
389
+ id: "resource.noValidation",
390
+ init: async (config: any) => {
391
+ return { config };
392
+ },
393
+ });
394
+
395
+ const event = defineEvent<any>({
396
+ id: "event.noValidation",
397
+ });
398
+
399
+ const middleware = defineMiddleware({
400
+ id: "middleware.noValidation",
401
+ run: async ({ next }) => next(),
402
+ });
403
+
404
+ const app = defineResource({
405
+ id: "app",
406
+ register: [
407
+ task,
408
+ resource.with({ anything: "goes" }),
409
+ event,
410
+ middleware,
411
+ ],
412
+ dependencies: { task, resource, event },
413
+ init: async (_, { task, resource, event }) => {
414
+ const taskResult = await task({ any: "data" });
415
+ expect(taskResult).toBe('Received: {"any":"data"}');
416
+
417
+ expect(resource.config.anything).toBe("goes");
418
+
419
+ await event({ any: "payload" }); // Should work without validation
420
+
421
+ return "success";
422
+ },
423
+ });
424
+
425
+ await run(app);
426
+ });
427
+ });
428
+ });
package/src/define.ts CHANGED
@@ -33,8 +33,10 @@ import {
33
33
  symbolResourceWithConfig,
34
34
  symbolResource,
35
35
  symbolMiddleware,
36
+ ITaskMeta,
37
+ IResourceMeta,
36
38
  } from "./defs";
37
- import { Errors } from "./errors";
39
+ import { MiddlewareAlreadyGlobalError, ValidationError } from "./errors";
38
40
  import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
39
41
 
40
42
  // Helper function to get the caller file
@@ -43,10 +45,11 @@ export function defineTask<
43
45
  Input = undefined,
44
46
  Output extends Promise<any> = any,
45
47
  Deps extends DependencyMapType = any,
46
- TOn extends "*" | IEventDefinition | undefined = undefined
48
+ TOn extends "*" | IEventDefinition | undefined = undefined,
49
+ TMeta extends ITaskMeta = any
47
50
  >(
48
- taskConfig: ITaskDefinition<Input, Output, Deps, TOn>
49
- ): ITask<Input, Output, Deps, TOn> {
51
+ taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>
52
+ ): ITask<Input, Output, Deps, TOn, TMeta> {
50
53
  /**
51
54
  * Creates a task definition.
52
55
  * - Generates an anonymous id based on file path when `id` is omitted
@@ -65,6 +68,7 @@ export function defineTask<
65
68
  run: taskConfig.run,
66
69
  on: taskConfig.on,
67
70
  listenerOrder: taskConfig.listenerOrder,
71
+ inputSchema: taskConfig.inputSchema,
68
72
  events: {
69
73
  beforeRun: {
70
74
  ...defineEvent({
@@ -91,19 +95,28 @@ export function defineTask<
91
95
  [symbolFilePath]: getCallerFile(),
92
96
  },
93
97
  },
94
- meta: taskConfig.meta || {},
98
+ meta: taskConfig.meta || ({} as TMeta),
95
99
  // autorun,
96
100
  };
97
101
  }
98
102
 
99
103
  export function defineResource<
100
104
  TConfig = void,
101
- TValue = any,
105
+ TValue extends Promise<any> = Promise<any>,
102
106
  TDeps extends DependencyMapType = {},
103
- TPrivate = any
107
+ TPrivate = any,
108
+ TMeta extends IResourceMeta = any
104
109
  >(
105
- constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate>
106
- ): IResource<TConfig, TValue, TDeps, TPrivate> {
110
+ constConfig: IResourceDefinition<
111
+ TConfig,
112
+ TValue,
113
+ TDeps,
114
+ TPrivate,
115
+ any,
116
+ any,
117
+ TMeta
118
+ >
119
+ ): IResource<TConfig, TValue, TDeps, TPrivate, TMeta> {
107
120
  /**
108
121
  * Creates a resource definition.
109
122
  * - Generates anonymous id when omitted (resource or index flavor)
@@ -129,7 +142,17 @@ export function defineResource<
129
142
  overrides: constConfig.overrides || [],
130
143
  init: constConfig.init,
131
144
  context: constConfig.context,
145
+ configSchema: constConfig.configSchema,
132
146
  with: function (config: TConfig) {
147
+ // Validate config with schema if provided (fail fast)
148
+ if (this.configSchema) {
149
+ try {
150
+ config = this.configSchema.parse(config);
151
+ } catch (error) {
152
+ throw new ValidationError("Resource config", this.id, error instanceof Error ? error : new Error(String(error)));
153
+ }
154
+ }
155
+
133
156
  return {
134
157
  [symbolResourceWithConfig]: true,
135
158
  id: this.id,
@@ -164,7 +187,7 @@ export function defineResource<
164
187
  [symbolFilePath]: filePath,
165
188
  },
166
189
  },
167
- meta: constConfig.meta || {},
190
+ meta: (constConfig.meta || {}) as TMeta,
168
191
  middleware: constConfig.middleware || [],
169
192
  };
170
193
  }
@@ -182,7 +205,7 @@ export function defineIndex<
182
205
  ? T[K]["resource"]
183
206
  : T[K];
184
207
  } & DependencyMapType
185
- >(items: T): IResource<void, DependencyValuesType<D>, D> {
208
+ >(items: T): IResource<void, Promise<DependencyValuesType<D>>, D> {
186
209
  // Build dependency map from given items; unwrap `.with()` to the base resource
187
210
  const dependencies = {} as D;
188
211
  const register: RegisterableItems[] = [];
@@ -263,6 +286,15 @@ export function defineMiddleware<
263
286
  return {
264
287
  ...object,
265
288
  with: (config: TConfig) => {
289
+ // Validate config with schema if provided (fail fast)
290
+ if (object.configSchema) {
291
+ try {
292
+ config = object.configSchema.parse(config);
293
+ } catch (error) {
294
+ throw new ValidationError("Middleware config", object.id, error instanceof Error ? error : new Error(String(error)));
295
+ }
296
+ }
297
+
266
298
  return {
267
299
  ...object,
268
300
  [symbolMiddlewareConfigured]: true,
@@ -280,7 +312,7 @@ export function defineMiddleware<
280
312
  [symbolMiddlewareEverywhereTasks]: tasks,
281
313
  [symbolMiddlewareEverywhereResources]: resources,
282
314
  everywhere() {
283
- throw Errors.middlewareAlreadyGlobal(object.id);
315
+ throw new MiddlewareAlreadyGlobalError(object.id);
284
316
  },
285
317
  };
286
318
  },
@@ -343,9 +375,9 @@ export function defineOverride(
343
375
  * - `.with(config)` to create configured instances
344
376
  * - `.extract(tags)` to extract this tag from a list of tags
345
377
  */
346
- export function defineTag<TConfig = void>(
347
- definition: ITagDefinition<TConfig>
348
- ): ITag<TConfig> {
378
+ export function defineTag<TConfig = void, TEnforceContract = void>(
379
+ definition: ITagDefinition<TConfig, TEnforceContract>
380
+ ): ITag<TConfig, TEnforceContract> {
349
381
  const id = definition.id;
350
382
 
351
383
  return {
@@ -0,0 +1,91 @@
1
+ // HasContracts<Meta> → true if contracts present, else false
2
+
3
+ import { ITag, ITagWithConfig } from "./defs";
4
+ import { IMeta } from "./defs";
5
+
6
+ // Keep these param names aligned with your defs.ts: ITag<TConfig, TEnforceContract>
7
+ type NonVoid<T> = [T] extends [void] ? never : T;
8
+
9
+ type ExtractReturnFromTag<T> = T extends ITagWithConfig<any, infer R>
10
+ ? NonVoid<R>
11
+ : T extends ITag<any, infer R>
12
+ ? NonVoid<R>
13
+ : never;
14
+
15
+ type IsTuple<T extends readonly unknown[]> = number extends T["length"]
16
+ ? false
17
+ : true;
18
+
19
+ type FilterContracts<
20
+ TTags extends readonly unknown[],
21
+ Acc extends readonly unknown[] = []
22
+ > = TTags extends readonly [infer H, ...infer R]
23
+ ? ExtractReturnFromTag<H> extends never
24
+ ? FilterContracts<R, Acc>
25
+ : FilterContracts<R, [...Acc, ExtractReturnFromTag<H>]>
26
+ : Acc;
27
+
28
+ export type ExtractContractsFromTags<TTags extends readonly unknown[]> =
29
+ IsTuple<TTags> extends true
30
+ ? FilterContracts<TTags>
31
+ : Array<ExtractReturnFromTag<TTags[number]>>;
32
+
33
+ export type ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta extends IMeta> =
34
+ TMeta extends { tags?: infer TTags }
35
+ ? TTags extends readonly unknown[]
36
+ ? ExtractContractsFromTags<TTags>
37
+ : []
38
+ : [];
39
+
40
+ type IsNeverTuple<T extends readonly unknown[]> = T extends [] ? true : false;
41
+
42
+ export type HasContracts<T extends IMeta> =
43
+ ExtractTagsWithNonVoidReturnTypeFromMeta<T> extends never[] ? false : true; // HasContracts and enforcement
44
+
45
+ // Ensure a response type satisfies ALL contracts (intersection)
46
+ type UnionToIntersection<U> = (
47
+ U extends any ? (arg: U) => void : never
48
+ ) extends (arg: infer I) => void
49
+ ? I
50
+ : never;
51
+
52
+ type ContractsUnion<TMeta extends IMeta> =
53
+ ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta> extends readonly (infer U)[]
54
+ ? U
55
+ : never;
56
+
57
+ type ContractsIntersection<TMeta extends IMeta> = UnionToIntersection<
58
+ ContractsUnion<TMeta>
59
+ >;
60
+
61
+ /**
62
+ * Pretty-print helper to expand intersections for better IDE display.
63
+ */
64
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
65
+
66
+ /**
67
+ * Verbose compile-time error surfaced when a value does not satisfy
68
+ * the intersection of all tag-enforced contracts.
69
+ *
70
+ * Intersected with `never` in call sites when desired to ensure assignment
71
+ * fails while still surfacing a readable shape in tooltips.
72
+ */
73
+ export type ContractViolationError<TMeta extends IMeta, TActual> = {
74
+ message: "Value does not satisfy all tag contracts";
75
+ expected: Simplify<ContractsIntersection<TMeta>>;
76
+ received: TActual;
77
+ };
78
+
79
+ export type EnsureResponseSatisfiesContracts<TMeta extends IMeta, TResponse> = [
80
+ ContractsUnion<TMeta>
81
+ ] extends [never]
82
+ ? TResponse // no contracts, allow as-is
83
+ : TResponse extends Promise<infer U>
84
+ ? Promise<
85
+ U extends ContractsIntersection<TMeta>
86
+ ? U
87
+ : ContractViolationError<TMeta, U>
88
+ >
89
+ : TResponse extends ContractsIntersection<TMeta>
90
+ ? TResponse
91
+ : ContractViolationError<TMeta, TResponse>;