@browserlessapi/inferlo 0.5.0 → 0.7.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 (138) hide show
  1. package/README.md +14 -5
  2. package/dist/ai/ai-sdk.d.ts +48 -0
  3. package/dist/ai/ai-sdk.js +235 -0
  4. package/dist/ai/ai-sdk.js.map +1 -0
  5. package/dist/ai/index.d.ts +44 -3
  6. package/dist/ai/index.js +164 -9
  7. package/dist/ai/index.js.map +1 -1
  8. package/dist/ai/public.d.ts +6 -1
  9. package/dist/ai/public.js +2 -0
  10. package/dist/ai/public.js.map +1 -1
  11. package/dist/ai/ui.d.ts +47 -0
  12. package/dist/ai/ui.js +33 -0
  13. package/dist/ai/ui.js.map +1 -0
  14. package/dist/api-version.d.ts +32 -0
  15. package/dist/api-version.js +45 -0
  16. package/dist/api-version.js.map +1 -0
  17. package/dist/app.d.ts +60 -6
  18. package/dist/app.js +310 -66
  19. package/dist/app.js.map +1 -1
  20. package/dist/astro.d.ts +60 -0
  21. package/dist/astro.js +68 -0
  22. package/dist/astro.js.map +1 -0
  23. package/dist/audit.d.ts +57 -0
  24. package/dist/audit.js +99 -0
  25. package/dist/audit.js.map +1 -0
  26. package/dist/auth.d.ts +1 -0
  27. package/dist/auth.js +1 -0
  28. package/dist/auth.js.map +1 -1
  29. package/dist/azure.d.ts +66 -0
  30. package/dist/azure.js +119 -0
  31. package/dist/azure.js.map +1 -0
  32. package/dist/cache.d.ts +96 -0
  33. package/dist/cache.js +247 -0
  34. package/dist/cache.js.map +1 -0
  35. package/dist/cli/main.js +28 -7
  36. package/dist/cli/main.js.map +1 -1
  37. package/dist/client-ip.d.ts +36 -1
  38. package/dist/client-ip.js +80 -1
  39. package/dist/client-ip.js.map +1 -1
  40. package/dist/db/memory.js +2 -1
  41. package/dist/db/memory.js.map +1 -1
  42. package/dist/db/postgres.js +4 -1
  43. package/dist/db/postgres.js.map +1 -1
  44. package/dist/db/table.d.ts +37 -5
  45. package/dist/db/table.js +96 -15
  46. package/dist/db/table.js.map +1 -1
  47. package/dist/db/types.d.ts +2 -0
  48. package/dist/describe.d.ts +54 -2
  49. package/dist/describe.js +34 -3
  50. package/dist/describe.js.map +1 -1
  51. package/dist/events.d.ts +134 -0
  52. package/dist/events.js +196 -0
  53. package/dist/events.js.map +1 -0
  54. package/dist/fastify.d.ts +10 -2
  55. package/dist/fastify.js +22 -4
  56. package/dist/fastify.js.map +1 -1
  57. package/dist/h3.d.ts +76 -0
  58. package/dist/h3.js +61 -0
  59. package/dist/h3.js.map +1 -0
  60. package/dist/hapi.d.ts +45 -0
  61. package/dist/hapi.js +57 -0
  62. package/dist/hapi.js.map +1 -0
  63. package/dist/index.d.ts +25 -5
  64. package/dist/index.js +9 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/jobs.d.ts +70 -10
  67. package/dist/jobs.js +78 -13
  68. package/dist/jobs.js.map +1 -1
  69. package/dist/koa.d.ts +51 -0
  70. package/dist/koa.js +45 -0
  71. package/dist/koa.js.map +1 -0
  72. package/dist/lambda.d.ts +110 -0
  73. package/dist/lambda.js +169 -0
  74. package/dist/lambda.js.map +1 -0
  75. package/dist/langchain.d.ts +58 -0
  76. package/dist/langchain.js +220 -0
  77. package/dist/langchain.js.map +1 -0
  78. package/dist/mcp.d.ts +1 -1
  79. package/dist/mcp.js +7 -3
  80. package/dist/mcp.js.map +1 -1
  81. package/dist/model.d.ts +23 -6
  82. package/dist/model.js +35 -6
  83. package/dist/model.js.map +1 -1
  84. package/dist/module.d.ts +52 -0
  85. package/dist/module.js +152 -0
  86. package/dist/module.js.map +1 -0
  87. package/dist/nextjs.d.ts +35 -0
  88. package/dist/nextjs.js +33 -0
  89. package/dist/nextjs.js.map +1 -0
  90. package/dist/openapi.d.ts +3 -0
  91. package/dist/openapi.js +12 -3
  92. package/dist/openapi.js.map +1 -1
  93. package/dist/permissions.d.ts +26 -0
  94. package/dist/permissions.js +78 -0
  95. package/dist/permissions.js.map +1 -0
  96. package/dist/rate-limit.d.ts +5 -3
  97. package/dist/rate-limit.js +3 -3
  98. package/dist/rate-limit.js.map +1 -1
  99. package/dist/react-router.d.ts +41 -0
  100. package/dist/react-router.js +42 -0
  101. package/dist/react-router.js.map +1 -0
  102. package/dist/redis-events.d.ts +27 -0
  103. package/dist/redis-events.js +163 -0
  104. package/dist/redis-events.js.map +1 -0
  105. package/dist/redis-jobs.d.ts +24 -0
  106. package/dist/redis-jobs.js +203 -0
  107. package/dist/redis-jobs.js.map +1 -0
  108. package/dist/redis.d.ts +30 -0
  109. package/dist/redis.js +106 -0
  110. package/dist/redis.js.map +1 -0
  111. package/dist/request-matcher.d.ts +4 -0
  112. package/dist/request-matcher.js +14 -0
  113. package/dist/request-matcher.js.map +1 -0
  114. package/dist/response-body.d.ts +6 -0
  115. package/dist/response-body.js +7 -0
  116. package/dist/response-body.js.map +1 -0
  117. package/dist/route.d.ts +50 -2
  118. package/dist/route.js +24 -0
  119. package/dist/route.js.map +1 -1
  120. package/dist/schema.d.ts +2 -2
  121. package/dist/serve.d.ts +26 -2
  122. package/dist/serve.js +111 -10
  123. package/dist/serve.js.map +1 -1
  124. package/dist/service-client.d.ts +91 -0
  125. package/dist/service-client.js +177 -0
  126. package/dist/service-client.js.map +1 -0
  127. package/dist/sessions.d.ts +74 -0
  128. package/dist/sessions.js +172 -0
  129. package/dist/sessions.js.map +1 -0
  130. package/dist/sveltekit.d.ts +72 -0
  131. package/dist/sveltekit.js +68 -0
  132. package/dist/sveltekit.js.map +1 -0
  133. package/dist/tracing.d.ts +122 -0
  134. package/dist/tracing.js +210 -0
  135. package/dist/tracing.js.map +1 -0
  136. package/llms.txt +8 -3
  137. package/package.json +78 -4
  138. package/templates/app/AGENTS.md +10 -6
package/dist/hapi.js ADDED
@@ -0,0 +1,57 @@
1
+ import { assertAIClient } from "./ai-client.js";
2
+ import { InferloError } from "./errors.js";
3
+ import { toNodeHandler } from "./serve.js";
4
+ const bad = (message, fix) => new InferloError("IL5025", message, { fix });
5
+ async function register(server, options) {
6
+ if (options === null || typeof options !== "object") {
7
+ throw bad("inferloPlugin options must be an object with ai, app or both.", "Use server.register({ plugin: inferloPlugin, options: { ai } }).");
8
+ }
9
+ if ("prefix" in options) {
10
+ throw bad("inferloPlugin takes the path prefix from hapi, not from its options.", 'Use server.register({ plugin: inferloPlugin, options: { app }, routes: { prefix: "/api" } }).');
11
+ }
12
+ const { app } = options;
13
+ if (app !== undefined && (typeof app !== "object" || app === null || typeof app.fetch !== "function")) {
14
+ throw bad("inferloPlugin's app must be an Inferlo app (createApp(...)).", 'Use server.register({ plugin: inferloPlugin, options: { app: inferloApp }, routes: { prefix: "/api" } }).');
15
+ }
16
+ if (!app && !options.ai) {
17
+ throw bad("inferloPlugin needs ai, app or both.", 'Use options: { ai } for server.ai and request.ai, or options: { app } with routes: { prefix: "/api" } to serve an Inferlo app.');
18
+ }
19
+ const decorated = server.decorations.server.includes("ai");
20
+ // app.ai is the default, unless an earlier registration with { ai } already set server.ai.
21
+ const ai = options.ai ?? (decorated ? undefined : app?.ai);
22
+ if (ai !== undefined) {
23
+ assertAIClient(ai, 'Pass createAI({ providers, models }) from "@browserlessapi/inferlo/ai", or an Inferlo app\'s app.ai: server.register({ plugin: inferloPlugin, options: { ai } }).');
24
+ if (decorated) {
25
+ throw bad("server.ai is already set by an earlier inferloPlugin registration.", "Register inferloPlugin with { ai } once, and with { app } (without ai) for each app you serve.");
26
+ }
27
+ // A value, not a method (older hapi types only describe method decorations).
28
+ server.decorate("server", "ai", ai);
29
+ // A getter, so every request reads the one client.
30
+ server.decorate("request", "ai", () => ai, { apply: true });
31
+ }
32
+ if (!app)
33
+ return;
34
+ const prefix = server.realm.modifiers.route.prefix;
35
+ const handler = toNodeHandler(app, { prefix });
36
+ // Inferlo reads the body stream itself (JSON, forms, its own size limits) and writes the answer, streams
37
+ // included, to the Node response; hapi's own routes stay more specific than this catch-all.
38
+ server.route({
39
+ method: "*",
40
+ path: "/{path*}",
41
+ options: { payload: { output: "stream", parse: false }, description: "Inferlo app" },
42
+ handler: async (request, h) => {
43
+ await handler(request.raw.req, request.raw.res);
44
+ return h.abandon;
45
+ },
46
+ });
47
+ }
48
+ /**
49
+ * hapi plugin. options: { ai } decorates server.ai and request.ai; options: { app } serves a whole Inferlo app
50
+ * (routes, auth, validation, OpenAPI, MCP) under the prefix the plugin is registered with (routes: { prefix }).
51
+ */
52
+ export const inferloPlugin = {
53
+ name: "@browserlessapi/inferlo",
54
+ multiple: true,
55
+ register,
56
+ };
57
+ //# sourceMappingURL=hapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hapi.js","sourceRoot":"","sources":["../src/hapi.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -8,32 +8,52 @@ export type { MetricsConfig } from "./metrics.ts";
8
8
  export type { Deprecation } from "./deprecation.ts";
9
9
  export type { App, AppConfig, AuthConfig, CorsConfig, Middleware, MiddlewareContext, RequestOptions, RouteSource, Logger, HealthConfig, HealthCheck, CheckResult, ReadinessReport, CloseOptions } from "./app.ts";
10
10
  export type { SecureHeadersConfig } from "./secure-headers.ts";
11
+ export { module, isModule } from "./module.ts";
12
+ export type { Module, ModuleOptions, AnyModule, ModuleModels } from "./module.ts";
13
+ export { apiVersion } from "./api-version.ts";
14
+ export type { ApiVersionOptions } from "./api-version.ts";
11
15
  export { route, isRoute } from "./route.ts";
12
- export type { Register, Context, RouteDef, RouteOptions, Auth, Method, ToolOptions, AppUser, AppDb } from "./route.ts";
16
+ export type { Register, Context, RouteDef, RouteInput, RouteOptions, Auth, Method, ToolOptions, AppUser, AppDb, AppPermission } from "./route.ts";
13
17
  export { t, Schema, ObjectSchema, ArraySchema, RefSchema, ValidationError, toJSONSchema } from "./schema.ts";
14
18
  export type { Infer, InferInput, Issue, JSONSchema, ParseResult, Shape, AnySchema } from "./schema.ts";
15
19
  export { model, Model, columnName } from "./model.ts";
16
- export type { Row, CreateInput, UpdateInput, AnyModel } from "./model.ts";
20
+ export type { Row, CreateInput, UpdateInput, AnyModel, ModelOptions } from "./model.ts";
17
21
  export { memory } from "./db/memory.ts";
18
22
  export { createDb, Table } from "./db/table.ts";
19
- export type { Db, Where, OrderBy, FindManyOptions, FindOptions, PaginateOptions, Page, Include, Relations, ReverseRelations, WithInclude } from "./db/table.ts";
23
+ export type { Db, PgClient, Where, OrderBy, FindManyOptions, FindOptions, PaginateOptions, Page, Include, Relations, ReverseRelations, WithInclude } from "./db/table.ts";
20
24
  export type { Adapter, Condition, Query, DbRecord } from "./db/types.ts";
21
25
  export { planMigration, applyMigrations } from "./db/migrate.ts";
22
26
  export type { ApplyMigrationsOptions, Migration, MigrationPlan, MigrationSnapshot, MigrationSnapshotColumn, MigrationSnapshotV1, PlanMigrationOptions, } from "./db/migrate.ts";
23
- export { createAI, cosineSimilarity, extractJson, openai, openaiCompatible, anthropic, gemini, ollama, mockProvider, ProviderError } from "./ai/index.ts";
27
+ export { createAI, cosineSimilarity, extractJson, openai, openaiCompatible, anthropic, gemini, ollama, mockProvider, ProviderError, uiChatBody, fromUIMessages } from "./ai/index.ts";
24
28
  export { tool, isTool, ToolBuilder } from "./ai/tool.ts";
29
+ export { aiSdk } from "./ai/ai-sdk.ts";
30
+ export type { AISdkProvider, AISdkLanguageModel, AISdkEmbeddingModel, AISdkOptions } from "./ai/ai-sdk.ts";
25
31
  export type { ToolDef, FunctionToolOptions, ToolContext } from "./ai/tool.ts";
26
- export type { AI, AIConfig, GenerateOptions, GenerateResult, StreamResult, Usage, Pricing, Provider, ProviderRequest, ProviderResponse, Image, ImageMediaType, Message, MockReply, Step, ToolCall, ToolCallResult, ToolSpec, } from "./ai/index.ts";
32
+ export type { AI, AIConfig, GenerateOptions, GenerateResult, StreamResult, StreamResponseInit, UIChatMessage, Usage, Pricing, Provider, ProviderRequest, ProviderResponse, Image, ImageMediaType, Message, MockReply, Step, ToolCall, ToolCallResult, ToolSpec, } from "./ai/index.ts";
27
33
  export type { RateLimit, RateLimitConfig } from "./rate-limit.ts";
34
+ export type { RedisClient } from "./redis.ts";
28
35
  export { job, isJob, retryDelayMs } from "./jobs.ts";
29
36
  export { cron, isCron } from "./cron.ts";
37
+ export { event, isEvent, on } from "./events.ts";
38
+ export type { AppEvents, EventDef, EventMessage, EventOptions, EventPayload, Events, EventsConfig, EventTransport, OnDef, PublishedEvent, PublishOptions } from "./events.ts";
30
39
  export type { CronDef } from "./cron.ts";
40
+ export type { JobTraceContext } from "./tracing.ts";
31
41
  export type { JobDef, JobOptions, JobContext, JobInput, JobRecord, JobStatus, JobSource, JobQueueConfig, Jobs, AppJobs, EnqueueOptions, RunResult, Worker } from "./jobs.ts";
42
+ export { AuditLog, auditChanges } from "./audit.ts";
43
+ export type { Audit, AuditEntry, AuditChange } from "./audit.ts";
44
+ export type { Cache, CacheConfig, CacheSetOptions, RouteCache } from "./cache.ts";
45
+ export { permissions, isPermissions } from "./permissions.ts";
46
+ export type { Permissions, PermissionRule, PermissionInput } from "./permissions.ts";
32
47
  export { bearer, hashToken, createToken, hashPassword, verifyPassword, safeEqual } from "./auth.ts";
48
+ export { sessions } from "./sessions.ts";
49
+ export type { TracingConfig, OtelApi, OtelSpan } from "./tracing.ts";
50
+ export type { Sessions, SessionsOptions, SessionData, SessionCookieOptions, SessionResponseContext } from "./sessions.ts";
33
51
  export { env, setEnv } from "./env.ts";
34
52
  export { config } from "./config.ts";
35
53
  export type { Config, ConfigOptions } from "./config.ts";
36
54
  export { InferloError, httpError, notFound, forbidden, badRequest, docsUrl } from "./errors.ts";
55
+ export { serviceClient, ServiceCallError } from "./service-client.ts";
56
+ export type { ServiceClient, ServiceClientOptions, CallInput, CallOptions, CallContext, CallResult, TypedRoute } from "./service-client.ts";
37
57
  export { serve, toNodeHandler, fromNodeRequest } from "./serve.ts";
38
58
  export type { ServeOptions, Server, NodeRequest, NodeResponse, NodeHandler, NodeHandlerOptions, FromNodeRequestOptions } from "./serve.ts";
39
59
  export { INFERLO_VERSION } from "./describe.ts";
package/dist/index.js CHANGED
@@ -3,20 +3,28 @@
3
3
  * https://github.com/BrowserlessAPI/Inferlo
4
4
  */
5
5
  export { createApp } from "./app.js";
6
+ export { module, isModule } from "./module.js";
7
+ export { apiVersion } from "./api-version.js";
6
8
  export { route, isRoute } from "./route.js";
7
9
  export { t, Schema, ObjectSchema, ArraySchema, RefSchema, ValidationError, toJSONSchema } from "./schema.js";
8
10
  export { model, Model, columnName } from "./model.js";
9
11
  export { memory } from "./db/memory.js";
10
12
  export { createDb, Table } from "./db/table.js";
11
13
  export { planMigration, applyMigrations } from "./db/migrate.js";
12
- export { createAI, cosineSimilarity, extractJson, openai, openaiCompatible, anthropic, gemini, ollama, mockProvider, ProviderError } from "./ai/index.js";
14
+ export { createAI, cosineSimilarity, extractJson, openai, openaiCompatible, anthropic, gemini, ollama, mockProvider, ProviderError, uiChatBody, fromUIMessages } from "./ai/index.js";
13
15
  export { tool, isTool, ToolBuilder } from "./ai/tool.js";
16
+ export { aiSdk } from "./ai/ai-sdk.js";
14
17
  export { job, isJob, retryDelayMs } from "./jobs.js";
15
18
  export { cron, isCron } from "./cron.js";
19
+ export { event, isEvent, on } from "./events.js";
20
+ export { AuditLog, auditChanges } from "./audit.js";
21
+ export { permissions, isPermissions } from "./permissions.js";
16
22
  export { bearer, hashToken, createToken, hashPassword, verifyPassword, safeEqual } from "./auth.js";
23
+ export { sessions } from "./sessions.js";
17
24
  export { env, setEnv } from "./env.js";
18
25
  export { config } from "./config.js";
19
26
  export { InferloError, httpError, notFound, forbidden, badRequest, docsUrl } from "./errors.js";
27
+ export { serviceClient, ServiceCallError } from "./service-client.js";
20
28
  export { serve, toNodeHandler, fromNodeRequest } from "./serve.js";
21
29
  export { INFERLO_VERSION } from "./describe.js";
22
30
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAOrC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG7G,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGtD,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAWjE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC1J,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAwBzD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACpG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAOrC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG7G,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGtD,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAWjE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACtL,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AA4BvC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAKjD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGpD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACpG,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEtE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
package/dist/jobs.d.ts CHANGED
@@ -11,10 +11,14 @@
11
11
  * jobs in this process, app.jobs.runDue() runs them once (tests).
12
12
  */
13
13
  import type { AI } from "./ai/index.ts";
14
+ import type { Audit } from "./audit.ts";
15
+ import type { Cache } from "./cache.ts";
14
16
  import { type CronDef } from "./cron.ts";
15
17
  import type { Adapter } from "./db/types.ts";
18
+ import { type AppEvents, type EventTransport, type Events, type OnDef } from "./events.ts";
16
19
  import type { AppDb } from "./route.ts";
17
20
  import { type AnySchema, type Infer, type InferInput } from "./schema.ts";
21
+ import type { JobTraceContext, JobTracing } from "./tracing.ts";
18
22
  export interface JobOptions<I extends AnySchema> {
19
23
  /** Schema of the job's input. Checked when the job is enqueued and again when it runs. */
20
24
  input: I;
@@ -30,10 +34,24 @@ export interface JobContext<Input> {
30
34
  ai: AI;
31
35
  /** Enqueue follow-up jobs. */
32
36
  jobs: Jobs;
37
+ /** Publish events: await events.publish(orderPaid, { orderId }) queues the jobs subscribed with on(event, job). */
38
+ events: Events;
39
+ /** The app cache, the same as ctx.cache in routes and app.cache. */
40
+ cache: Cache;
33
41
  /** Job id. */
34
42
  id: string;
35
43
  /** 1 on the first run, 2 on the first retry, and so on. */
36
44
  attempt: number;
45
+ /**
46
+ * The tenant the job works for (the tenant of the request or job that queued it, or enqueue's tenantId), or null.
47
+ * ctx.db is scoped to it, and jobs and events queued from this job keep it.
48
+ */
49
+ tenantId: string | null;
50
+ /**
51
+ * Write an audit log entry (needs AuditLog in createApp({ models })). Jobs have no signed-in user: pass the
52
+ * userId of the user who asked for the work, or leave it out for work the system does on its own.
53
+ */
54
+ audit: Audit;
37
55
  }
38
56
  export interface JobDef<Input = any, Output = any> {
39
57
  readonly kind: "inferlo.job";
@@ -48,6 +66,11 @@ export type JobInput<J> = J extends JobDef<infer I, any> ? I : never;
48
66
  export interface EnqueueOptions {
49
67
  /** Run no earlier than this time. Default: now. */
50
68
  runAt?: Date;
69
+ /**
70
+ * The tenant the job works for: its ctx.db is scoped to it (tenant-scoped models). Default: the tenant of the
71
+ * request or job that enqueues it, none from app.jobs. Pass null to queue it without a tenant.
72
+ */
73
+ tenantId?: string | null;
51
74
  }
52
75
  export type JobStatus = "pending" | "running" | "done" | "failed";
53
76
  export interface JobRecord {
@@ -64,6 +87,10 @@ export interface JobRecord {
64
87
  runAt: Date;
65
88
  /** Message of the last failure. */
66
89
  lastError?: string;
90
+ /** With createApp({ tracing }): the trace (W3C traceparent and tracestate) of the request or job that enqueued it, which its runs continue. */
91
+ trace?: JobTraceContext;
92
+ /** The tenant the job works for (multi-tenancy): its ctx.db is scoped to it. */
93
+ tenantId?: string;
67
94
  createdAt: Date;
68
95
  updatedAt: Date;
69
96
  }
@@ -75,8 +102,9 @@ export interface JobQueueConfig {
75
102
  /**
76
103
  * Where queued jobs are kept. "memory" (default): in this process, for development and tests.
77
104
  * "database": in the inferlo_jobs table of the app's Postgres database, shared by every instance.
105
+ * "redis": in the Redis of createApp({ redis }), shared by every instance.
78
106
  */
79
- store?: "memory" | "database";
107
+ store?: "memory" | "database" | "redis";
80
108
  }
81
109
  /** Where queued jobs are kept. */
82
110
  export interface JobStore {
@@ -86,7 +114,7 @@ export interface JobStore {
86
114
  claim(now: Date, limit: number, names: readonly string[]): Promise<JobRecord[]>;
87
115
  update(id: string, patch: Partial<Pick<JobRecord, "status" | "runAt" | "lastError">>): Promise<void>;
88
116
  get(id: string): Promise<JobRecord | null>;
89
- /** Record that a running job is still alive (database store), so it is not picked up again as stuck. */
117
+ /** Record that a running job is still alive (database and Redis stores), so it is not picked up again as stuck. */
90
118
  touch?(id: string): Promise<void>;
91
119
  }
92
120
  export interface RunResult {
@@ -108,14 +136,17 @@ export interface AppJobs extends Jobs {
108
136
  /** The registered crons: cron(schedule, job) values in createApp({ jobs }). */
109
137
  readonly crons: readonly CronDef[];
110
138
  /** Where queued jobs are kept (createApp({ jobQueue: { store } })). */
111
- readonly queue: "memory" | "database";
139
+ readonly queue: "memory" | "database" | "redis";
112
140
  /** A queued job by id, or null. */
113
141
  get(id: string): Promise<JobRecord | null>;
114
142
  /** Run every job that is due now (or at options.now), one after another, including follow-ups they enqueue, then return. For tests and scripts. */
115
143
  runDue(options?: {
116
144
  now?: Date;
117
145
  }): Promise<RunResult>;
118
- /** Run due jobs in this process every pollMs (default 1000) until stop() is called. Also queues each cron's job at every tick after the start. */
146
+ /**
147
+ * Run due jobs in this process every pollMs (default 1000) until stop() is called. Also queues each cron's job at
148
+ * every tick after the start, and with an events transport, the jobs of the events it receives.
149
+ */
119
150
  start(options?: {
120
151
  pollMs?: number;
121
152
  }): Worker;
@@ -134,13 +165,15 @@ export declare class JobBuilder<I extends AnySchema> {
134
165
  /** Define a background job: job("send-email", { input: t.object({ to: t.email() }) }).handle(fn). */
135
166
  export declare function job<I extends AnySchema>(name: string, options: JobOptions<I>): JobBuilder<I>;
136
167
  export declare function isJob(value: unknown): value is JobDef;
137
- /** Anything that contains jobs: a job, a cron, an array, or a module (import * as jobs from "./jobs.ts"). */
138
- export type JobSource = JobDef | CronDef | readonly JobSource[] | {
168
+ /** Anything that contains jobs: a job, a cron, an event subscription (on), an array, or a module (import * as jobs from "./jobs.ts"). */
169
+ export type JobSource = JobDef | CronDef | OnDef | readonly JobSource[] | {
139
170
  readonly [name: string]: unknown;
140
171
  };
141
172
  /** The jobs and crons in createApp({ jobs }). A cron's job is registered with it. */
142
173
  export declare function collectJobs(sources: readonly JobSource[] | undefined): JobDef[];
143
174
  export declare function collectCrons(sources: readonly JobSource[] | undefined): CronDef[];
175
+ /** The event subscriptions, on(event, job), in createApp({ jobs }). */
176
+ export declare function collectSubscriptions(sources: readonly JobSource[] | undefined): OnDef[];
144
177
  /** Jobs kept in this process. They are lost when it stops, so use it for development and tests. */
145
178
  export declare function memoryJobStore(): JobStore;
146
179
  /** A running job whose worker has not reported for this long is treated as stuck (its process stopped) and run again. */
@@ -156,14 +189,24 @@ export declare const STOPPED_MESSAGE = "The worker stopped while running this jo
156
189
  export declare function databaseJobStore(adapter: Adapter, options?: {
157
190
  staleAfterMs?: number;
158
191
  }): JobStore;
159
- /** Pick the job store for createApp({ jobQueue }). Raises IL6011 for an unknown store, IL6012 for "database" without Postgres. */
160
- export declare function jobStoreFor(config: JobQueueConfig | undefined, adapter: Adapter): JobStore;
192
+ /**
193
+ * Pick the job store for createApp({ jobQueue }). Raises IL6011 for an unknown store, IL6012 for "database" without
194
+ * Postgres. "redis" uses the redis option, a function that returns the Redis store (or throws IL6031 without a client).
195
+ */
196
+ export declare function jobStoreFor(config: JobQueueConfig | undefined, adapter: Adapter, redis?: () => JobStore): JobStore;
161
197
  export interface JobRuntime {
162
198
  db: AppDb;
163
199
  ai: AI;
200
+ cache: Cache;
201
+ /** Builds ctx.audit for one run. */
202
+ audit(job: {
203
+ name: string;
204
+ id: string;
205
+ tenantId: string | null;
206
+ }): Audit;
164
207
  store: JobStore;
165
208
  /** Name of the store, shown as app.jobs.queue. Default "memory". */
166
- queue?: "memory" | "database";
209
+ queue?: "memory" | "database" | "redis";
167
210
  /** How often a running job is reported alive to stores that support it. Default HEARTBEAT_MS. */
168
211
  heartbeatMs?: number;
169
212
  logger: {
@@ -172,10 +215,27 @@ export interface JobRuntime {
172
215
  };
173
216
  /** Called after each attempt with its outcome and duration (metrics). */
174
217
  onRun?(name: string, outcome: keyof RunResult, durationMs: number): void;
218
+ /** With createApp({ tracing }): a span per attempt that continues the trace the job was enqueued in. */
219
+ tracing?: JobTracing;
220
+ /** With createApp({ events: { transport } }): carries published events between services. */
221
+ eventTransport?: EventTransport;
175
222
  }
176
223
  /** app.jobs plus what the framework tests reach directly. */
177
224
  export interface InternalJobs extends AppJobs {
178
225
  /** Queue the job of each cron's latest tick in (from, to]. */
179
226
  queueCrons(from: Date, to: Date): Promise<void>;
227
+ /** app.events: publishing queues the jobs subscribed with on(event, job). */
228
+ readonly events: AppEvents;
180
229
  }
181
- export declare function createJobs(list: readonly JobDef[], runtime: JobRuntime, crons?: readonly CronDef[]): InternalJobs;
230
+ export declare function createJobs(list: readonly JobDef[], runtime: JobRuntime, crons?: readonly CronDef[], subscriptions?: readonly OnDef[]): InternalJobs;
231
+ /**
232
+ * ctx.jobs and ctx.events for one tenant: jobs and events queued through them carry the tenant unless the call
233
+ * names another (tenantId) or none (null). Internal: used for requests and jobs with a tenant.
234
+ */
235
+ export declare function withTenant(base: {
236
+ jobs: Jobs;
237
+ events: Events;
238
+ }, tenantId: string): {
239
+ jobs: Jobs;
240
+ events: Events;
241
+ };
package/dist/jobs.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { cronTickId, isCron } from "./cron.js";
2
+ import { checkTenantId } from "./db/table.js";
2
3
  import { InferloError } from "./errors.js";
4
+ import { createEvents, isOn } from "./events.js";
3
5
  import { ValidationError } from "./schema.js";
4
6
  /** Workers started with app.jobs.start() that have not been stopped, so app.close() can stop them. */
5
7
  const workers = new WeakMap();
@@ -61,9 +63,14 @@ export function collectJobs(sources) {
61
63
  export function collectCrons(sources) {
62
64
  return collect(sources).crons;
63
65
  }
66
+ /** The event subscriptions, on(event, job), in createApp({ jobs }). */
67
+ export function collectSubscriptions(sources) {
68
+ return collect(sources).subscriptions;
69
+ }
64
70
  function collect(sources) {
65
71
  const out = [];
66
72
  const crons = [];
73
+ const subscriptions = [];
67
74
  const seen = new Set();
68
75
  const visit = (source) => {
69
76
  if (!source || typeof source !== "object" || seen.has(source))
@@ -75,11 +82,15 @@ function collect(sources) {
75
82
  crons.push(source);
76
83
  visit(source.job);
77
84
  }
85
+ else if (isOn(source)) {
86
+ subscriptions.push(source);
87
+ visit(source.job);
88
+ }
78
89
  else if (Array.isArray(source))
79
90
  source.forEach(visit);
80
91
  else
81
92
  for (const value of Object.values(source))
82
- if (isJob(value) || isCron(value) || Array.isArray(value))
93
+ if (isJob(value) || isCron(value) || isOn(value) || Array.isArray(value))
83
94
  visit(value);
84
95
  };
85
96
  visit(sources);
@@ -89,7 +100,7 @@ function collect(sources) {
89
100
  throw new InferloError("IL6009", `Two jobs are named "${j.name}".`, { fix: "Give each job a unique name." });
90
101
  names.set(j.name, j);
91
102
  }
92
- return { jobs: out, crons };
103
+ return { jobs: out, crons, subscriptions };
93
104
  }
94
105
  /** Jobs kept in this process. They are lost when it stops, so use it for development and tests. */
95
106
  export function memoryJobStore() {
@@ -125,7 +136,7 @@ export const STALE_AFTER_MS = 5 * 60_000;
125
136
  /** How often a worker reports that a running job is still alive. */
126
137
  export const HEARTBEAT_MS = 30_000;
127
138
  export const STOPPED_MESSAGE = "The worker stopped while running this job (the process exited or lost its database connection).";
128
- const JOB_COLUMNS = `"id", "name", "input", "status", "attempts", "max_attempts", "run_at", "last_error", "created_at", "updated_at"`;
139
+ const JOB_COLUMNS = `"id", "name", "input", "status", "attempts", "max_attempts", "run_at", "last_error", "created_at", "updated_at", "traceparent", "tracestate", "tenant_id"`;
129
140
  function fromRow(row) {
130
141
  const record = {
131
142
  id: String(row.id),
@@ -140,6 +151,13 @@ function fromRow(row) {
140
151
  };
141
152
  if (row.last_error !== null && row.last_error !== undefined)
142
153
  record.lastError = String(row.last_error);
154
+ if (typeof row.traceparent === "string") {
155
+ record.trace = { traceparent: row.traceparent };
156
+ if (typeof row.tracestate === "string")
157
+ record.trace.tracestate = row.tracestate;
158
+ }
159
+ if (typeof row.tenant_id === "string")
160
+ record.tenantId = row.tenant_id;
143
161
  return record;
144
162
  }
145
163
  /**
@@ -161,7 +179,10 @@ export function databaseJobStore(adapter, options = {}) {
161
179
  await sql(`CREATE TABLE IF NOT EXISTS "inferlo_jobs" (
162
180
  "id" uuid PRIMARY KEY, "name" text NOT NULL, "input" jsonb NOT NULL, "status" text NOT NULL,
163
181
  "attempts" integer NOT NULL, "max_attempts" integer NOT NULL, "run_at" timestamptz NOT NULL,
164
- "last_error" text, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL)`);
182
+ "last_error" text, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL,
183
+ "traceparent" text, "tracestate" text, "tenant_id" text)`);
184
+ // Tables created by earlier versions get the trace (Inferlo 0.6) and tenant (0.7) columns.
185
+ await sql('ALTER TABLE "inferlo_jobs" ADD COLUMN IF NOT EXISTS "traceparent" text, ADD COLUMN IF NOT EXISTS "tracestate" text, ADD COLUMN IF NOT EXISTS "tenant_id" text');
165
186
  await sql('CREATE INDEX IF NOT EXISTS "inferlo_jobs_due_idx" ON "inferlo_jobs" ("status", "run_at")');
166
187
  }
167
188
  catch {
@@ -179,8 +200,8 @@ export function databaseJobStore(adapter, options = {}) {
179
200
  return {
180
201
  async add(r) {
181
202
  await table();
182
- await sql(`INSERT INTO "inferlo_jobs" (${JOB_COLUMNS}) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT ("id") DO NOTHING`, [
183
- r.id, r.name, JSON.stringify(r.input), r.status, r.attempts, r.maxAttempts, r.runAt, r.lastError ?? null, r.createdAt, r.updatedAt,
203
+ await sql(`INSERT INTO "inferlo_jobs" (${JOB_COLUMNS}) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT ("id") DO NOTHING`, [
204
+ r.id, r.name, JSON.stringify(r.input), r.status, r.attempts, r.maxAttempts, r.runAt, r.lastError ?? null, r.createdAt, r.updatedAt, r.trace?.traceparent ?? null, r.trace?.tracestate ?? null, r.tenantId ?? null,
184
205
  ]);
185
206
  },
186
207
  async claim(now, limit, names) {
@@ -229,16 +250,21 @@ export function databaseJobStore(adapter, options = {}) {
229
250
  },
230
251
  };
231
252
  }
232
- /** Pick the job store for createApp({ jobQueue }). Raises IL6011 for an unknown store, IL6012 for "database" without Postgres. */
233
- export function jobStoreFor(config, adapter) {
253
+ /**
254
+ * Pick the job store for createApp({ jobQueue }). Raises IL6011 for an unknown store, IL6012 for "database" without
255
+ * Postgres. "redis" uses the redis option, a function that returns the Redis store (or throws IL6031 without a client).
256
+ */
257
+ export function jobStoreFor(config, adapter, redis) {
234
258
  const store = config?.store ?? "memory";
235
259
  if (store === "memory")
236
260
  return memoryJobStore();
237
261
  if (store === "database")
238
262
  return databaseJobStore(adapter);
239
- throw new InferloError("IL6011", `Invalid jobQueue store "${String(store)}".`, { fix: 'Use createApp({ jobQueue: { store: "memory" } }) (the default) or { store: "database" }.' });
263
+ if (store === "redis" && redis)
264
+ return redis();
265
+ throw new InferloError("IL6011", `Invalid jobQueue store "${String(store)}".`, { fix: 'Use createApp({ jobQueue: { store: "memory" } }) (the default), { store: "database" } or { store: "redis" } with createApp({ redis }).' });
240
266
  }
241
- export function createJobs(list, runtime, crons = []) {
267
+ export function createJobs(list, runtime, crons = [], subscriptions = []) {
242
268
  const byName = new Map(list.map((j) => [j.name, j]));
243
269
  const names = list.map((j) => j.name);
244
270
  const running = new Set();
@@ -285,9 +311,25 @@ export function createJobs(list, runtime, crons = []) {
285
311
  await runtime.store.add(record(c.job, {}, tick, await cronTickId(c, tick)));
286
312
  }
287
313
  };
314
+ const events = createEvents(subscriptions, {
315
+ async enqueue(def, input, id, from, tenantId) {
316
+ const queued = record(registered(def), input, undefined, id);
317
+ const trace = from ?? runtime.tracing?.capture();
318
+ if (runtime.tracing && trace?.traceparent)
319
+ queued.trace = trace;
320
+ if (tenantId)
321
+ queued.tenantId = tenantId;
322
+ await runtime.store.add(queued);
323
+ return id;
324
+ },
325
+ transport: runtime.eventTransport,
326
+ capture: runtime.tracing ? () => runtime.tracing.capture() : undefined,
327
+ logger: runtime.logger,
328
+ });
288
329
  const jobs = {
289
330
  list,
290
331
  crons,
332
+ events,
291
333
  queueCrons,
292
334
  queue: runtime.queue ?? "memory",
293
335
  async enqueue(j, input, options = {}) {
@@ -295,7 +337,13 @@ export function createJobs(list, runtime, crons = []) {
295
337
  if (options.runAt !== undefined && (!(options.runAt instanceof Date) || Number.isNaN(options.runAt.getTime()))) {
296
338
  throw new InferloError("IL6009", `runAt for job "${def.name}" must be a valid Date.`, { fix: "Pass runAt: new Date(Date.now() + 60_000) or leave it out to run now." });
297
339
  }
340
+ const tenantId = options.tenantId === undefined || options.tenantId === null ? undefined : checkTenantId(options.tenantId);
298
341
  const queued = record(def, input, options.runAt, crypto.randomUUID());
342
+ const trace = runtime.tracing?.capture();
343
+ if (trace?.traceparent)
344
+ queued.trace = trace;
345
+ if (tenantId)
346
+ queued.tenantId = tenantId;
299
347
  await runtime.store.add(queued);
300
348
  return queued;
301
349
  },
@@ -325,6 +373,9 @@ export function createJobs(list, runtime, crons = []) {
325
373
  await queueCrons(cronFrom, now);
326
374
  cronFrom = now;
327
375
  }
376
+ // Events from other services (createApp({ events: { transport } })) queue their jobs first.
377
+ if (events.transport)
378
+ await events.receive().catch((error) => runtime.logger.error("event receive error", error));
328
379
  await jobs.runDue();
329
380
  }
330
381
  catch (error) {
@@ -355,22 +406,26 @@ export function createJobs(list, runtime, crons = []) {
355
406
  workers.set(jobs, running);
356
407
  async function run(record, now) {
357
408
  const started = performance.now();
358
- const outcome = await attempt(record, now);
409
+ const outcome = runtime.tracing ? await runtime.tracing.run(record, (fail) => attempt(record, now, fail)) : await attempt(record, now);
359
410
  runtime.onRun?.(record.name, outcome, performance.now() - started);
360
411
  return outcome;
361
412
  }
362
- async function attempt(record, now) {
413
+ async function attempt(record, now, fail) {
363
414
  const def = byName.get(record.name);
364
415
  const touch = runtime.store.touch?.bind(runtime.store);
365
416
  const heartbeat = touch ? setInterval(() => void touch(record.id).catch(() => { }), runtime.heartbeatMs ?? HEARTBEAT_MS) : undefined;
366
417
  try {
367
418
  if (!def)
368
419
  throw new InferloError("IL6010", `"${record.name}" is not a registered job.`, { fix: "Add the job to createApp({ jobs: [...] }) in the process that runs jobs." });
369
- await def.handler({ input: parse(def, record.input), db: runtime.db, ai: runtime.ai, jobs, id: record.id, attempt: record.attempts });
420
+ const tenantId = record.tenantId ?? null;
421
+ const scoped = tenantId ? withTenant({ jobs, events }, tenantId) : { jobs, events };
422
+ const db = tenantId ? runtime.db.$tenant(tenantId) : runtime.db;
423
+ await def.handler({ input: parse(def, record.input), db, ai: runtime.ai, jobs: scoped.jobs, events: scoped.events, cache: runtime.cache, id: record.id, attempt: record.attempts, tenantId, audit: runtime.audit({ name: record.name, id: record.id, tenantId }) });
370
424
  await runtime.store.update(record.id, { status: "done" });
371
425
  return "done";
372
426
  }
373
427
  catch (error) {
428
+ fail?.(error);
374
429
  const message = error instanceof Error ? error.message : String(error);
375
430
  if (record.attempts < record.maxAttempts) {
376
431
  await runtime.store.update(record.id, { status: "pending", lastError: message, runAt: new Date(now.getTime() + retryDelayMs(record.attempts)) });
@@ -388,4 +443,14 @@ export function createJobs(list, runtime, crons = []) {
388
443
  }
389
444
  return jobs;
390
445
  }
446
+ /**
447
+ * ctx.jobs and ctx.events for one tenant: jobs and events queued through them carry the tenant unless the call
448
+ * names another (tenantId) or none (null). Internal: used for requests and jobs with a tenant.
449
+ */
450
+ export function withTenant(base, tenantId) {
451
+ return {
452
+ jobs: { enqueue: (job, input, options = {}) => base.jobs.enqueue(job, input, { ...options, tenantId: options.tenantId === undefined ? tenantId : options.tenantId }) },
453
+ events: { publish: (event, payload, options = {}) => base.events.publish(event, payload, { ...options, tenantId: options.tenantId === undefined ? tenantId : options.tenantId }) },
454
+ };
455
+ }
391
456
  //# sourceMappingURL=jobs.js.map