@qlever-llc/trellis 0.10.5 → 0.10.7

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 (85) hide show
  1. package/esm/generated-sdk/auth/client.d.ts +137 -76
  2. package/esm/generated-sdk/auth/client.d.ts.map +1 -1
  3. package/esm/generated-sdk/auth/mod.d.ts +1 -1
  4. package/esm/generated-sdk/auth/mod.d.ts.map +1 -1
  5. package/esm/generated-sdk/health/client.d.ts +27 -4
  6. package/esm/generated-sdk/health/client.d.ts.map +1 -1
  7. package/esm/generated-sdk/health/mod.d.ts +1 -1
  8. package/esm/generated-sdk/health/mod.d.ts.map +1 -1
  9. package/esm/generated-sdk/jobs/client.d.ts +38 -15
  10. package/esm/generated-sdk/jobs/client.d.ts.map +1 -1
  11. package/esm/generated-sdk/jobs/mod.d.ts +1 -1
  12. package/esm/generated-sdk/jobs/mod.d.ts.map +1 -1
  13. package/esm/generated-sdk/state/client.d.ts +36 -13
  14. package/esm/generated-sdk/state/client.d.ts.map +1 -1
  15. package/esm/generated-sdk/state/mod.d.ts +1 -1
  16. package/esm/generated-sdk/state/mod.d.ts.map +1 -1
  17. package/esm/generated-sdk/trellis-core/client.d.ts +17 -7
  18. package/esm/generated-sdk/trellis-core/client.d.ts.map +1 -1
  19. package/esm/generated-sdk/trellis-core/mod.d.ts +1 -1
  20. package/esm/generated-sdk/trellis-core/mod.d.ts.map +1 -1
  21. package/esm/index.d.ts +1 -1
  22. package/esm/index.d.ts.map +1 -1
  23. package/esm/server/service.d.ts +80 -2
  24. package/esm/server/service.d.ts.map +1 -1
  25. package/esm/server/service.js +100 -3
  26. package/esm/service/deno.d.ts +1 -1
  27. package/esm/service/deno.d.ts.map +1 -1
  28. package/esm/service/mod.d.ts +1 -1
  29. package/esm/service/mod.d.ts.map +1 -1
  30. package/esm/service/node.d.ts +1 -1
  31. package/esm/service/node.d.ts.map +1 -1
  32. package/esm/trellis.d.ts +17 -2
  33. package/esm/trellis.d.ts.map +1 -1
  34. package/esm/trellis.js +39 -3
  35. package/package.json +2 -2
  36. package/script/generated-sdk/auth/client.d.ts +137 -76
  37. package/script/generated-sdk/auth/client.d.ts.map +1 -1
  38. package/script/generated-sdk/auth/mod.d.ts +1 -1
  39. package/script/generated-sdk/auth/mod.d.ts.map +1 -1
  40. package/script/generated-sdk/health/client.d.ts +27 -4
  41. package/script/generated-sdk/health/client.d.ts.map +1 -1
  42. package/script/generated-sdk/health/mod.d.ts +1 -1
  43. package/script/generated-sdk/health/mod.d.ts.map +1 -1
  44. package/script/generated-sdk/jobs/client.d.ts +38 -15
  45. package/script/generated-sdk/jobs/client.d.ts.map +1 -1
  46. package/script/generated-sdk/jobs/mod.d.ts +1 -1
  47. package/script/generated-sdk/jobs/mod.d.ts.map +1 -1
  48. package/script/generated-sdk/state/client.d.ts +36 -13
  49. package/script/generated-sdk/state/client.d.ts.map +1 -1
  50. package/script/generated-sdk/state/mod.d.ts +1 -1
  51. package/script/generated-sdk/state/mod.d.ts.map +1 -1
  52. package/script/generated-sdk/trellis-core/client.d.ts +17 -7
  53. package/script/generated-sdk/trellis-core/client.d.ts.map +1 -1
  54. package/script/generated-sdk/trellis-core/mod.d.ts +1 -1
  55. package/script/generated-sdk/trellis-core/mod.d.ts.map +1 -1
  56. package/script/index.d.ts +1 -1
  57. package/script/index.d.ts.map +1 -1
  58. package/script/server/service.d.ts +80 -2
  59. package/script/server/service.d.ts.map +1 -1
  60. package/script/server/service.js +99 -2
  61. package/script/service/deno.d.ts +1 -1
  62. package/script/service/deno.d.ts.map +1 -1
  63. package/script/service/mod.d.ts +1 -1
  64. package/script/service/mod.d.ts.map +1 -1
  65. package/script/service/node.d.ts +1 -1
  66. package/script/service/node.d.ts.map +1 -1
  67. package/script/trellis.d.ts +17 -2
  68. package/script/trellis.d.ts.map +1 -1
  69. package/script/trellis.js +39 -3
  70. package/src/index.ts +1 -0
  71. package/src/sdk/_generated/auth/client.ts +100 -72
  72. package/src/sdk/_generated/auth/mod.ts +1 -1
  73. package/src/sdk/_generated/core/client.ts +25 -8
  74. package/src/sdk/_generated/core/mod.ts +1 -1
  75. package/src/sdk/_generated/health/client.ts +26 -5
  76. package/src/sdk/_generated/health/mod.ts +1 -1
  77. package/src/sdk/_generated/jobs/client.ts +35 -14
  78. package/src/sdk/_generated/jobs/mod.ts +1 -1
  79. package/src/sdk/_generated/state/client.ts +33 -12
  80. package/src/sdk/_generated/state/mod.ts +1 -1
  81. package/src/server/service.ts +485 -2
  82. package/src/service/deno.ts +1 -0
  83. package/src/service/mod.ts +1 -0
  84. package/src/service/node.ts +1 -0
  85. package/src/trellis.ts +80 -5
@@ -40,16 +40,24 @@ import {
40
40
  CONTRACT_JOBS_METADATA,
41
41
  CONTRACT_KV_METADATA,
42
42
  } from "../contract_support/mod.js";
43
- import { AsyncResult, type BaseError, isErr, Result } from "@qlever-llc/result";
43
+ import {
44
+ AsyncResult,
45
+ type BaseError,
46
+ isErr,
47
+ type MaybeAsync,
48
+ Result,
49
+ } from "@qlever-llc/result";
44
50
  import { Type } from "typebox";
45
51
  import { Value } from "typebox/value";
46
52
  import { type HealthCheckFn, ServiceHealth } from "./health.js";
47
53
  import { mountStandardHealthRpc } from "./health_rpc.js";
48
- import type { RPCDesc } from "../contracts.js";
54
+ import type { EventDesc, RPCDesc } from "../contracts.js";
49
55
  import type {
50
56
  AcceptedOperation,
51
57
  ActiveEventFacade,
52
58
  ActiveEventPublishFacade,
59
+ EventListenerContext,
60
+ EventOpts,
53
61
  FeedEventOf,
54
62
  FeedInputOf,
55
63
  FeedRegistration as RootFeedRegistration,
@@ -60,6 +68,7 @@ import type {
60
68
  OperationRegistration as RootOperationRegistration,
61
69
  OperationRuntimeHandle,
62
70
  OperationTransferContextOf,
71
+ PreparedTrellisEvent,
63
72
  RpcHandlerContext,
64
73
  RpcHandlerErrorOf,
65
74
  } from "../trellis.js";
@@ -1097,6 +1106,268 @@ export type JobsFacadeOf<
1097
1106
  >;
1098
1107
  };
1099
1108
 
1109
+ type ServiceEventName<TA extends TrellisAPI> = keyof TA["events"] & string;
1110
+ type ServiceEventOf<
1111
+ TA extends TrellisAPI,
1112
+ E extends ServiceEventName<TA>,
1113
+ > = TA["events"][E] extends EventDesc<infer TEvent> ? InferSchemaType<TEvent>
1114
+ : never;
1115
+ type ServiceEventPayloadOf<
1116
+ TA extends TrellisAPI,
1117
+ E extends ServiceEventName<TA>,
1118
+ > = Omit<ServiceEventOf<TA, E>, "header">;
1119
+
1120
+ type BoundEventHandleFn<
1121
+ TEventApi extends TrellisAPI,
1122
+ TTrellisApi extends TrellisAPI,
1123
+ E extends ServiceEventName<TEventApi>,
1124
+ TKv extends ContractKvMetadata,
1125
+ TJobs extends ContractJobsMetadata,
1126
+ TDeps,
1127
+ > = (args: {
1128
+ event: ServiceEventOf<TEventApi, E>;
1129
+ context: EventListenerContext;
1130
+ client: Trellis<TTrellisApi, TKv, TJobs>;
1131
+ deps: TDeps;
1132
+ }) => MaybeAsync<void, BaseError>;
1133
+
1134
+ type BoundActiveEventFacade<
1135
+ TEventApi extends TrellisAPI,
1136
+ TTrellisApi extends TrellisAPI,
1137
+ TKv extends ContractKvMetadata,
1138
+ TJobs extends ContractJobsMetadata,
1139
+ TDeps,
1140
+ > = {
1141
+ readonly [TGroup in SurfaceGroupName<ServiceEventName<TEventApi>>]: {
1142
+ readonly [
1143
+ E in SurfaceKeysForGroup<
1144
+ ServiceEventName<TEventApi>,
1145
+ TGroup
1146
+ > as SurfaceLeafName<E>
1147
+ ]: {
1148
+ prepare(
1149
+ event: ServiceEventPayloadOf<TEventApi, E>,
1150
+ ): Result<
1151
+ PreparedTrellisEvent<ServiceEventPayloadOf<TEventApi, E>>,
1152
+ ValidationError | UnexpectedError
1153
+ >;
1154
+ publish(
1155
+ event: ServiceEventPayloadOf<TEventApi, E>,
1156
+ ): AsyncResult<void, ValidationError | UnexpectedError>;
1157
+ listen(
1158
+ handler: BoundEventHandleFn<
1159
+ TEventApi,
1160
+ TTrellisApi,
1161
+ E,
1162
+ TKv,
1163
+ TJobs,
1164
+ TDeps
1165
+ >,
1166
+ subjectData?: Record<string, unknown>,
1167
+ opts?: EventOpts,
1168
+ ): AsyncResult<void, ValidationError | UnexpectedError>;
1169
+ };
1170
+ };
1171
+ };
1172
+
1173
+ type BoundRpcHandleFn<
1174
+ TOwnedApi extends TrellisAPI,
1175
+ TTrellisApi extends TrellisAPI,
1176
+ M extends RpcMethodName<TOwnedApi>,
1177
+ TKv extends ContractKvMetadata,
1178
+ TJobs extends ContractJobsMetadata,
1179
+ TDeps,
1180
+ > = (args: {
1181
+ input: RpcMethodInput<TOwnedApi, M>;
1182
+ context: RpcHandlerContext;
1183
+ client: Trellis<TTrellisApi, TKv, TJobs>;
1184
+ deps: TDeps;
1185
+ }) =>
1186
+ | Promise<
1187
+ Result<RpcMethodOutput<TOwnedApi, M>, RpcHandlerErrorOf<TOwnedApi, M>>
1188
+ >
1189
+ | Result<RpcMethodOutput<TOwnedApi, M>, RpcHandlerErrorOf<TOwnedApi, M>>;
1190
+
1191
+ type BoundFeedHandleFn<
1192
+ TOwnedApi extends TrellisAPI,
1193
+ TTrellisApi extends TrellisAPI,
1194
+ F extends keyof TOwnedApi["feeds"] & string,
1195
+ TKv extends ContractKvMetadata,
1196
+ TJobs extends ContractJobsMetadata,
1197
+ TDeps,
1198
+ > = (context: {
1199
+ input: FeedInputOf<TOwnedApi, F>;
1200
+ caller: unknown;
1201
+ signal: AbortSignal;
1202
+ emit(
1203
+ event: FeedEventOf<TOwnedApi, F>,
1204
+ ): AsyncResult<void, ValidationError | UnexpectedError>;
1205
+ client: Trellis<TTrellisApi, TKv, TJobs>;
1206
+ deps: TDeps;
1207
+ }) => unknown | Promise<unknown>;
1208
+
1209
+ type BoundOperationHandleFn<
1210
+ TOwnedApi extends TrellisAPI,
1211
+ TTrellisApi extends TrellisAPI,
1212
+ O extends keyof TOwnedApi["operations"] & string,
1213
+ TKv extends ContractKvMetadata,
1214
+ TJobs extends ContractJobsMetadata,
1215
+ TDeps,
1216
+ > =
1217
+ & ((
1218
+ handler: (
1219
+ context:
1220
+ & OperationHandlerContext<
1221
+ InferSchemaType<TOwnedApi["operations"][O]["input"]>,
1222
+ OperationProgressOf<TOwnedApi, O>,
1223
+ OperationOutputOf<TOwnedApi, O>,
1224
+ OperationTransferContextOf<TOwnedApi, O>
1225
+ >
1226
+ & {
1227
+ client: Trellis<TTrellisApi, TKv, TJobs>;
1228
+ deps: TDeps;
1229
+ },
1230
+ ) => unknown | Promise<unknown>,
1231
+ ) => Promise<void>)
1232
+ & Pick<
1233
+ OperationHandleFn<TOwnedApi, TTrellisApi, O, TKv, TJobs>,
1234
+ "accept" | "control"
1235
+ >;
1236
+
1237
+ type BoundTypedServiceHandleFacade<
1238
+ TOwnedApi extends TrellisAPI,
1239
+ TTrellisApi extends TrellisAPI,
1240
+ TKv extends ContractKvMetadata,
1241
+ TJobs extends ContractJobsMetadata,
1242
+ TDeps,
1243
+ > = {
1244
+ readonly rpc: {
1245
+ readonly [TGroup in SurfaceGroupName<RpcMethodName<TOwnedApi>>]: {
1246
+ readonly [
1247
+ M in SurfaceKeysForGroup<
1248
+ RpcMethodName<TOwnedApi>,
1249
+ TGroup
1250
+ > as SurfaceLeafName<M>
1251
+ ]: (
1252
+ handler: BoundRpcHandleFn<TOwnedApi, TTrellisApi, M, TKv, TJobs, TDeps>,
1253
+ ) => Promise<void>;
1254
+ };
1255
+ };
1256
+ readonly feed: {
1257
+ readonly [TGroup in SurfaceGroupName<keyof TOwnedApi["feeds"] & string>]: {
1258
+ readonly [
1259
+ F in SurfaceKeysForGroup<
1260
+ keyof TOwnedApi["feeds"] & string,
1261
+ TGroup
1262
+ > as SurfaceLeafName<F>
1263
+ ]: (
1264
+ handler: BoundFeedHandleFn<
1265
+ TOwnedApi,
1266
+ TTrellisApi,
1267
+ F,
1268
+ TKv,
1269
+ TJobs,
1270
+ TDeps
1271
+ >,
1272
+ ) => Promise<void>;
1273
+ };
1274
+ };
1275
+ readonly operation: {
1276
+ readonly [
1277
+ TGroup in SurfaceGroupName<keyof TOwnedApi["operations"] & string>
1278
+ ]: {
1279
+ readonly [
1280
+ O in SurfaceKeysForGroup<
1281
+ keyof TOwnedApi["operations"] & string,
1282
+ TGroup
1283
+ > as SurfaceLeafName<O>
1284
+ ]: BoundOperationHandleFn<TOwnedApi, TTrellisApi, O, TKv, TJobs, TDeps>;
1285
+ };
1286
+ };
1287
+ };
1288
+
1289
+ type BoundJobQueue<
1290
+ TPayload,
1291
+ TResult,
1292
+ TTrellisApi extends TrellisAPI,
1293
+ TKv extends ContractKvMetadata,
1294
+ TJobs extends ContractJobsMetadata,
1295
+ TDeps,
1296
+ > = {
1297
+ create(payload: TPayload): AsyncResult<JobRef<TPayload, TResult>, BaseError>;
1298
+ handle(
1299
+ handler: (args: {
1300
+ job: PublicActiveJob<TPayload, TResult>;
1301
+ client: Trellis<TTrellisApi, TKv, TJobs>;
1302
+ deps: TDeps;
1303
+ }) => Promise<Result<TResult, BaseError>>,
1304
+ ): void;
1305
+ };
1306
+
1307
+ type BoundJobsFacadeOf<
1308
+ TJobs extends ContractJobsMetadata,
1309
+ TTrellisApi extends TrellisAPI,
1310
+ TKv extends ContractKvMetadata,
1311
+ TDeps,
1312
+ > = {
1313
+ [K in keyof TJobs]: BoundJobQueue<
1314
+ TJobs[K]["payload"],
1315
+ TJobs[K]["result"],
1316
+ TTrellisApi,
1317
+ TKv,
1318
+ TJobs,
1319
+ TDeps
1320
+ >;
1321
+ };
1322
+
1323
+ /** Service wrapper returned by `TrellisService.with(deps)`. */
1324
+ export type BoundTrellisService<
1325
+ TOwnedApi extends TrellisAPI = TrellisAPI,
1326
+ TTrellisApi extends TrellisAPI = TOwnedApi,
1327
+ TJobs extends ContractJobsMetadata = {},
1328
+ TKv extends ContractKvMetadata = ContractKvMetadata,
1329
+ TDeps = unknown,
1330
+ > =
1331
+ & Pick<
1332
+ TrellisService<TOwnedApi, TTrellisApi, TJobs, TKv>,
1333
+ | "name"
1334
+ | "auth"
1335
+ | "nc"
1336
+ | "kv"
1337
+ | "store"
1338
+ | "health"
1339
+ | "connection"
1340
+ | "createTransfer"
1341
+ | "completeOperation"
1342
+ | "wait"
1343
+ | "stop"
1344
+ >
1345
+ & {
1346
+ readonly event: BoundActiveEventFacade<
1347
+ TTrellisApi,
1348
+ TTrellisApi,
1349
+ TKv,
1350
+ TJobs,
1351
+ TDeps
1352
+ >;
1353
+ readonly jobs: BoundJobsFacadeOf<TJobs, TTrellisApi, TKv, TDeps>;
1354
+ readonly handle: BoundTypedServiceHandleFacade<
1355
+ TOwnedApi,
1356
+ TTrellisApi,
1357
+ TKv,
1358
+ TJobs,
1359
+ TDeps
1360
+ >;
1361
+ /** Returns a new bound wrapper that injects the provided dependencies. */
1362
+ with<TNextDeps>(deps: TNextDeps): BoundTrellisService<
1363
+ TOwnedApi,
1364
+ TTrellisApi,
1365
+ TJobs,
1366
+ TKv,
1367
+ TNextDeps
1368
+ >;
1369
+ };
1370
+
1100
1371
  const MANAGED_JOB_WORKERS = Symbol("trellis.managedJobWorkers");
1101
1372
 
1102
1373
  type ManagedJobWorkers = {
@@ -1147,6 +1418,30 @@ type ServiceEventPublishLeaf = {
1147
1418
  ): ReturnType<HandlerTrellis<TrellisAPI>["publish"]>;
1148
1419
  };
1149
1420
 
1421
+ type ServiceEventLeaf = ServiceEventPublishLeaf & {
1422
+ listen(
1423
+ handler: (
1424
+ event: unknown,
1425
+ context: EventListenerContext,
1426
+ ) => MaybeAsync<void, BaseError>,
1427
+ subjectData?: Record<string, unknown>,
1428
+ opts?: EventOpts,
1429
+ ): AsyncResult<void, ValidationError | UnexpectedError>;
1430
+ };
1431
+
1432
+ type BoundServiceEventLeaf<TDeps> = ServiceEventPublishLeaf & {
1433
+ listen(
1434
+ handler: (args: {
1435
+ event: unknown;
1436
+ context: EventListenerContext;
1437
+ client: unknown;
1438
+ deps: TDeps;
1439
+ }) => MaybeAsync<void, BaseError>,
1440
+ subjectData?: Record<string, unknown>,
1441
+ opts?: EventOpts,
1442
+ ): AsyncResult<void, ValidationError | UnexpectedError>;
1443
+ };
1444
+
1150
1445
  function createServiceEventPublishFacade<TA extends TrellisAPI>(outbound: {
1151
1446
  readonly api: TA;
1152
1447
  prepare(
@@ -2255,6 +2550,47 @@ function createJobsFacade<
2255
2550
  return jobsFacade as ManagedJobsFacade<TJobs, TTrellisApi, TKv>;
2256
2551
  }
2257
2552
 
2553
+ function createBoundJobsFacade<
2554
+ TJobs extends ContractJobsMetadata,
2555
+ TTrellisApi extends TrellisAPI,
2556
+ TKv extends ContractKvMetadata,
2557
+ TDeps,
2558
+ >(args: {
2559
+ jobs: JobsFacadeOf<TJobs, TTrellisApi, TKv>;
2560
+ deps: TDeps;
2561
+ }): BoundJobsFacadeOf<TJobs, TTrellisApi, TKv, TDeps> {
2562
+ const boundJobs: Record<string, unknown> = {};
2563
+ const jobs = args.jobs as Record<
2564
+ string,
2565
+ JobQueue<unknown, unknown, TTrellisApi, TKv, TJobs>
2566
+ >;
2567
+
2568
+ for (const queueType of Object.keys(jobs)) {
2569
+ const queue = jobs[queueType];
2570
+ if (!queue) continue;
2571
+ boundJobs[queueType] = {
2572
+ create: (payload) => queue.create(payload),
2573
+ handle: (handler) =>
2574
+ queue.handle(({ job, client }) =>
2575
+ handler({
2576
+ job,
2577
+ client,
2578
+ deps: args.deps,
2579
+ })
2580
+ ),
2581
+ } satisfies BoundJobQueue<
2582
+ unknown,
2583
+ unknown,
2584
+ TTrellisApi,
2585
+ TKv,
2586
+ TJobs,
2587
+ TDeps
2588
+ >;
2589
+ }
2590
+
2591
+ return boundJobs as BoundJobsFacadeOf<TJobs, TTrellisApi, TKv, TDeps>;
2592
+ }
2593
+
2258
2594
  export class TrellisService<
2259
2595
  TOwnedApi extends TrellisAPI = TrellisAPI,
2260
2596
  TTrellisApi extends TrellisAPI = TOwnedApi,
@@ -2340,6 +2676,77 @@ export class TrellisService<
2340
2676
  this.#stopHealthPublishing = stopHealthPublishing;
2341
2677
  }
2342
2678
 
2679
+ /**
2680
+ * Returns a service wrapper that injects application dependencies into
2681
+ * service-owned handler argument objects as `args.deps`.
2682
+ */
2683
+ with<TDeps>(
2684
+ deps: TDeps,
2685
+ ): BoundTrellisService<TOwnedApi, TTrellisApi, TJobs, TKv, TDeps> {
2686
+ return {
2687
+ name: this.name,
2688
+ auth: this.auth,
2689
+ nc: this.nc,
2690
+ event: this.#createBoundEventFacade(deps),
2691
+ kv: this.kv,
2692
+ store: this.store,
2693
+ jobs: createBoundJobsFacade({ jobs: this.jobs, deps }),
2694
+ health: this.health,
2695
+ handle: this.#createBoundHandleFacade(deps),
2696
+ connection: this.connection,
2697
+ createTransfer: (args) => this.createTransfer(args),
2698
+ completeOperation: (operationId, output) =>
2699
+ this.completeOperation(operationId, output),
2700
+ wait: () => this.wait(),
2701
+ stop: () => this.stop(),
2702
+ with: (nextDeps) => this.with(nextDeps),
2703
+ };
2704
+ }
2705
+
2706
+ #createBoundEventFacade<TDeps>(
2707
+ deps: TDeps,
2708
+ ): BoundActiveEventFacade<TTrellisApi, TTrellisApi, TKv, TJobs, TDeps> {
2709
+ const event = {} as BoundActiveEventFacade<
2710
+ TTrellisApi,
2711
+ TTrellisApi,
2712
+ TKv,
2713
+ TJobs,
2714
+ TDeps
2715
+ >;
2716
+ const source = this.event as Record<
2717
+ string,
2718
+ Record<string, ServiceEventLeaf>
2719
+ >;
2720
+ for (const [groupName, leaves] of Object.entries(source)) {
2721
+ const group: Record<string, BoundServiceEventLeaf<TDeps>> = {};
2722
+ for (const [leafName, leaf] of Object.entries(leaves)) {
2723
+ group[leafName] = {
2724
+ prepare: (payload) => leaf.prepare(payload),
2725
+ publish: (payload) => leaf.publish(payload),
2726
+ listen: (handler, subjectData, opts) =>
2727
+ leaf.listen(
2728
+ (payload, context) =>
2729
+ handler({
2730
+ event: payload,
2731
+ context,
2732
+ client: this.#handlerTrellis,
2733
+ deps,
2734
+ }),
2735
+ subjectData,
2736
+ opts,
2737
+ ),
2738
+ };
2739
+ }
2740
+ Object.defineProperty(event, groupName, {
2741
+ value: group,
2742
+ enumerable: true,
2743
+ configurable: true,
2744
+ });
2745
+ }
2746
+
2747
+ return event;
2748
+ }
2749
+
2343
2750
  #createHandleFacade(): ServiceHandleFacade {
2344
2751
  const rpc: ServiceHandleFacade["rpc"] = {};
2345
2752
  for (const method of Object.keys(this.#server.api.rpc ?? {})) {
@@ -2405,6 +2812,82 @@ export class TrellisService<
2405
2812
  return { rpc, feed, operation };
2406
2813
  }
2407
2814
 
2815
+ #createBoundHandleFacade<TDeps>(
2816
+ deps: TDeps,
2817
+ ): BoundTypedServiceHandleFacade<TOwnedApi, TTrellisApi, TKv, TJobs, TDeps> {
2818
+ const rpc: ServiceHandleFacade["rpc"] = {};
2819
+ for (const method of Object.keys(this.#server.api.rpc ?? {})) {
2820
+ addSurfaceLeaf(rpc, method, (handler) =>
2821
+ this.#server.mountRuntime(
2822
+ method,
2823
+ async ({ input, context }) =>
2824
+ await Promise.resolve(
2825
+ (handler as (
2826
+ args: unknown,
2827
+ ) =>
2828
+ | Promise<Result<unknown, BaseError>>
2829
+ | Result<unknown, BaseError>)({
2830
+ input,
2831
+ context,
2832
+ client: this.#handlerTrellis,
2833
+ deps,
2834
+ }),
2835
+ ),
2836
+ ));
2837
+ }
2838
+
2839
+ const feed: ServiceHandleFacade["feed"] = {};
2840
+ for (const feedName of Object.keys(this.#server.api.feeds ?? {})) {
2841
+ addSurfaceLeaf(
2842
+ feed,
2843
+ feedName,
2844
+ (handler) =>
2845
+ this.#server.feedHandle(feedName).handle((context) =>
2846
+ (handler as (args: unknown) => unknown | Promise<unknown>)({
2847
+ ...context,
2848
+ client: this.#handlerTrellis,
2849
+ deps,
2850
+ })
2851
+ ),
2852
+ );
2853
+ }
2854
+
2855
+ const operation: Record<
2856
+ string,
2857
+ Record<string, ServiceHandleOperationLeaf>
2858
+ > = {};
2859
+ for (
2860
+ const operationName of Object.keys(this.#server.api.operations ?? {})
2861
+ ) {
2862
+ const registration = this.#operation(
2863
+ operationName as keyof TOwnedApi["operations"] & string,
2864
+ );
2865
+ const leaf = Object.assign(
2866
+ (handler: (context: unknown) => unknown) =>
2867
+ registration.handle((context) =>
2868
+ handler({
2869
+ ...context,
2870
+ client: this.#handlerTrellis,
2871
+ deps,
2872
+ })
2873
+ ),
2874
+ {
2875
+ accept: (args: { sessionKey: string }) => registration.accept(args),
2876
+ control: (operationId: string) => registration.control(operationId),
2877
+ },
2878
+ ) as ServiceHandleOperationLeaf;
2879
+ addSurfaceLeaf(operation, operationName, leaf);
2880
+ }
2881
+
2882
+ return { rpc, feed, operation } as BoundTypedServiceHandleFacade<
2883
+ TOwnedApi,
2884
+ TTrellisApi,
2885
+ TKv,
2886
+ TJobs,
2887
+ TDeps
2888
+ >;
2889
+ }
2890
+
2408
2891
  /**
2409
2892
  * Creates a short-lived receive transfer grant for a caller session.
2410
2893
  */
@@ -1,5 +1,6 @@
1
1
  import "../_dnt.polyfills.js";
2
2
  export {
3
+ type BoundTrellisService,
3
4
  type JobArgs,
4
5
  type JobHandler,
5
6
  type JobQueue,
@@ -36,6 +36,7 @@ export {
36
36
  HealthRpcSchema,
37
37
  } from "../server/health_schemas.js";
38
38
  export {
39
+ type BoundTrellisService,
39
40
  type JobArgs,
40
41
  type JobHandler,
41
42
  type JobQueue,
@@ -1,5 +1,6 @@
1
1
  import "../_dnt.polyfills.js";
2
2
  export {
3
+ type BoundTrellisService,
3
4
  type JobArgs,
4
5
  type JobHandler,
5
6
  type JobQueue,
package/src/trellis.ts CHANGED
@@ -1009,6 +1009,50 @@ export type EventOpts = {
1009
1009
  signal?: AbortSignal;
1010
1010
  };
1011
1011
 
1012
+ /** Context provided to event listener callbacks. */
1013
+ export type EventListenerContext = {
1014
+ /** Stable event id from the Trellis event header. */
1015
+ id: string;
1016
+ /** Event creation time from the Trellis event header. */
1017
+ time: Date;
1018
+ /** NATS subject that delivered the event. */
1019
+ subject: string;
1020
+ /** Runtime listener mode that delivered the event. */
1021
+ mode: "durable" | "ephemeral";
1022
+ /** Durable event consumer group, when delivered through a group. */
1023
+ group?: string;
1024
+ /** JetStream sequence number, when available. */
1025
+ sequence?: number;
1026
+ };
1027
+
1028
+ function createEventListenerContext(args: {
1029
+ payload: unknown;
1030
+ subject: string;
1031
+ mode: "durable" | "ephemeral";
1032
+ group?: string;
1033
+ message: object;
1034
+ }): EventListenerContext {
1035
+ const header = typeof args.payload === "object" && args.payload !== null
1036
+ ? Reflect.get(args.payload, "header")
1037
+ : undefined;
1038
+ const id = typeof header === "object" && header !== null
1039
+ ? Reflect.get(header, "id")
1040
+ : undefined;
1041
+ const time = typeof header === "object" && header !== null
1042
+ ? Reflect.get(header, "time")
1043
+ : undefined;
1044
+ const sequence = Reflect.get(args.message, "seq");
1045
+
1046
+ return {
1047
+ id: typeof id === "string" ? id : "",
1048
+ time: new Date(typeof time === "string" ? time : 0),
1049
+ subject: args.subject,
1050
+ mode: args.mode,
1051
+ ...(args.group ? { group: args.group } : {}),
1052
+ ...(typeof sequence === "number" ? { sequence } : {}),
1053
+ };
1054
+ }
1055
+
1012
1056
  type RuntimeEventConsumers = {
1013
1057
  metadata?: ContractEventConsumers;
1014
1058
  bindings?: Record<string, EventConsumerResourceBinding>;
@@ -1224,7 +1268,10 @@ export type FeedSurface<
1224
1268
  type MaybePromise<T> = T | Promise<T>;
1225
1269
 
1226
1270
  type EventCallback<TMessage> = {
1227
- bivarianceHack(message: TMessage): MaybeAsync<void, BaseError>;
1271
+ bivarianceHack(
1272
+ message: TMessage,
1273
+ context: EventListenerContext,
1274
+ ): MaybeAsync<void, BaseError>;
1228
1275
  }["bivarianceHack"];
1229
1276
 
1230
1277
  export type RpcHandlerContext = {
@@ -1731,7 +1778,10 @@ export type EventPayload<
1731
1778
  export type EventHandler<
1732
1779
  TContract,
1733
1780
  E extends EventName<TContract>,
1734
- > = (event: EventType<TContract, E>) => MaybeAsync<void, BaseError>;
1781
+ > = (
1782
+ event: EventType<TContract, E>,
1783
+ context: EventListenerContext,
1784
+ ) => MaybeAsync<void, BaseError>;
1735
1785
 
1736
1786
  type DeepRecord<T> = {
1737
1787
  [k: string]: T | DeepRecord<T>;
@@ -3664,7 +3714,15 @@ export class Trellis<
3664
3714
  }
3665
3715
 
3666
3716
  const handlerResult = await AsyncResult.lift(
3667
- fn(m as EventOf<TA, EventsOf<TA>>),
3717
+ fn(
3718
+ m as EventOf<TA, EventsOf<TA>>,
3719
+ createEventListenerContext({
3720
+ payload: m,
3721
+ subject: msg.subject,
3722
+ mode: "ephemeral",
3723
+ message: msg,
3724
+ }),
3725
+ ),
3668
3726
  );
3669
3727
  if (handlerResult.isErr()) {
3670
3728
  this.#log.error(
@@ -3887,7 +3945,15 @@ export class Trellis<
3887
3945
  }
3888
3946
 
3889
3947
  const handlerResult = await AsyncResult.lift(
3890
- fn(m as EventOf<TA, EventsOf<TA>>),
3948
+ fn(
3949
+ m as EventOf<TA, EventsOf<TA>>,
3950
+ createEventListenerContext({
3951
+ payload: m,
3952
+ subject: msg.subject,
3953
+ mode: "durable",
3954
+ message: msg,
3955
+ }),
3956
+ ),
3891
3957
  );
3892
3958
  if (handlerResult.isErr()) {
3893
3959
  this.#log.error(
@@ -3949,7 +4015,16 @@ export class Trellis<
3949
4015
  }
3950
4016
 
3951
4017
  const handlerResult = await AsyncResult.lift(
3952
- registration.fn(eventPayload as EventOf<TA, EventsOf<TA>>),
4018
+ registration.fn(
4019
+ eventPayload as EventOf<TA, EventsOf<TA>>,
4020
+ createEventListenerContext({
4021
+ payload: eventPayload,
4022
+ subject: msg.subject,
4023
+ mode: "durable",
4024
+ group,
4025
+ message: msg,
4026
+ }),
4027
+ ),
3953
4028
  );
3954
4029
  if (handlerResult.isErr()) {
3955
4030
  this.#log.error(