@qlever-llc/trellis 0.10.13 → 0.10.15

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 (94) hide show
  1. package/esm/auth/browser.js +0 -1
  2. package/esm/auth.js +0 -1
  3. package/esm/browser.d.ts +29 -2
  4. package/esm/browser.d.ts.map +1 -1
  5. package/esm/browser.js +14 -3
  6. package/esm/client_connect.d.ts.map +1 -1
  7. package/esm/client_connect.js +7 -3
  8. package/esm/contract_support/mod.d.ts +5 -10
  9. package/esm/contract_support/mod.d.ts.map +1 -1
  10. package/esm/contract_support/mod.js +41 -31
  11. package/esm/contract_support/protocol.d.ts +8 -13
  12. package/esm/contract_support/protocol.d.ts.map +1 -1
  13. package/esm/contract_support/protocol.js +4 -5
  14. package/esm/contracts.js +0 -1
  15. package/esm/device.d.ts +0 -29
  16. package/esm/device.d.ts.map +1 -1
  17. package/esm/device.js +10 -1
  18. package/esm/errors/index.js +0 -1
  19. package/esm/index.d.ts +3 -4
  20. package/esm/index.d.ts.map +1 -1
  21. package/esm/index.js +3 -4
  22. package/esm/runtime_transport.d.ts.map +1 -1
  23. package/esm/runtime_transport.js +4 -2
  24. package/esm/server/health_rpc.d.ts +4 -2
  25. package/esm/server/health_rpc.d.ts.map +1 -1
  26. package/esm/server/health_rpc.js +6 -1
  27. package/esm/server/service.d.ts +14 -37
  28. package/esm/server/service.d.ts.map +1 -1
  29. package/esm/server/service.js +143 -103
  30. package/esm/server.js +3 -3
  31. package/esm/service/outbox_inbox.d.ts +1 -6
  32. package/esm/service/outbox_inbox.d.ts.map +1 -1
  33. package/esm/service/outbox_inbox.js +0 -21
  34. package/esm/telemetry/env.d.ts.map +1 -1
  35. package/esm/telemetry/env.js +5 -6
  36. package/esm/telemetry/metrics.d.ts +0 -7
  37. package/esm/telemetry/metrics.d.ts.map +1 -1
  38. package/esm/trellis.d.ts +1 -19
  39. package/esm/trellis.d.ts.map +1 -1
  40. package/esm/trellis.js +12 -8
  41. package/package.json +2 -2
  42. package/script/auth/browser.js +0 -1
  43. package/script/auth.js +0 -1
  44. package/script/browser.d.ts +29 -2
  45. package/script/browser.d.ts.map +1 -1
  46. package/script/browser.js +75 -17
  47. package/script/client_connect.d.ts.map +1 -1
  48. package/script/client_connect.js +7 -36
  49. package/script/contract_support/mod.d.ts +5 -10
  50. package/script/contract_support/mod.d.ts.map +1 -1
  51. package/script/contract_support/mod.js +43 -32
  52. package/script/contract_support/protocol.d.ts +8 -13
  53. package/script/contract_support/protocol.d.ts.map +1 -1
  54. package/script/contract_support/protocol.js +5 -6
  55. package/script/contracts.js +0 -1
  56. package/script/device.d.ts +0 -29
  57. package/script/device.d.ts.map +1 -1
  58. package/script/device.js +10 -1
  59. package/script/errors/index.js +0 -1
  60. package/script/index.d.ts +3 -4
  61. package/script/index.d.ts.map +1 -1
  62. package/script/index.js +1 -6
  63. package/script/runtime_transport.d.ts.map +1 -1
  64. package/script/runtime_transport.js +4 -2
  65. package/script/server/health_rpc.d.ts +4 -2
  66. package/script/server/health_rpc.d.ts.map +1 -1
  67. package/script/server/health_rpc.js +6 -1
  68. package/script/server/service.d.ts +14 -37
  69. package/script/server/service.d.ts.map +1 -1
  70. package/script/server/service.js +153 -112
  71. package/script/server.js +3 -3
  72. package/script/service/outbox_inbox.d.ts +1 -6
  73. package/script/service/outbox_inbox.d.ts.map +1 -1
  74. package/script/service/outbox_inbox.js +0 -21
  75. package/script/telemetry/env.d.ts.map +1 -1
  76. package/script/telemetry/env.js +5 -39
  77. package/script/telemetry/metrics.d.ts +0 -7
  78. package/script/telemetry/metrics.d.ts.map +1 -1
  79. package/script/trellis.d.ts +1 -19
  80. package/script/trellis.d.ts.map +1 -1
  81. package/script/trellis.js +12 -8
  82. package/src/browser.ts +200 -2
  83. package/src/client_connect.ts +10 -2
  84. package/src/contract_support/mod.ts +69 -45
  85. package/src/contract_support/protocol.ts +16 -7
  86. package/src/device.ts +10 -2
  87. package/src/index.ts +3 -4
  88. package/src/runtime_transport.ts +6 -1
  89. package/src/server/health_rpc.ts +7 -2
  90. package/src/server/service.ts +186 -126
  91. package/src/server.ts +3 -3
  92. package/src/service/outbox_inbox.ts +1 -28
  93. package/src/telemetry/env.ts +10 -7
  94. package/src/trellis.ts +22 -13
@@ -5,14 +5,13 @@ import {
5
5
  type NatsConnection,
6
6
  type Subscription,
7
7
  } from "@nats-io/nats-core";
8
+ import type { KVError, StoreError } from "../errors/index.js";
9
+ import { TypedKV } from "../kv.js";
8
10
  import {
9
- type KVError,
10
- type StoreError,
11
11
  type StoreWaitOptions,
12
- TypedKV,
13
12
  TypedStore,
14
13
  TypedStoreEntry,
15
- } from "../index.js";
14
+ } from "../store.js";
16
15
  import { sdk as trellisAuth } from "../sdk/auth.js";
17
16
  import {
18
17
  TrellisServiceRuntime,
@@ -745,7 +744,21 @@ async function fetchServiceBootstrapInfo(args: {
745
744
  }
746
745
  }
747
746
 
748
- export class StoreHandle {
747
+ export abstract class StoreHandle {
748
+ abstract readonly binding: ResourceBindingStore;
749
+
750
+ abstract open(): AsyncResult<TypedStore, StoreError>;
751
+
752
+ /**
753
+ * Waits for a staged object to appear in the bound store and returns its entry.
754
+ */
755
+ abstract waitFor(
756
+ key: string,
757
+ options?: StoreWaitOptions,
758
+ ): AsyncResult<TypedStoreEntry, StoreError>;
759
+ }
760
+
761
+ class InternalStoreHandle extends StoreHandle {
749
762
  readonly binding: ResourceBindingStore;
750
763
  readonly #nc: NatsConnection;
751
764
 
@@ -754,6 +767,7 @@ export class StoreHandle {
754
767
  binding: ResourceBindingStore,
755
768
  token: typeof storeHandleConstructorToken,
756
769
  ) {
770
+ super();
757
771
  if (token !== storeHandleConstructorToken) {
758
772
  throw new TypeError(
759
773
  "StoreHandle instances are created by TrellisService",
@@ -1453,12 +1467,12 @@ export type BoundTrellisService<
1453
1467
  TrellisService<TOwnedApi, TTrellisApi, TJobs, TKv>,
1454
1468
  | "name"
1455
1469
  | "auth"
1456
- | "nc"
1457
1470
  | "kv"
1458
1471
  | "store"
1459
1472
  | "connection"
1460
1473
  | "createTransfer"
1461
1474
  | "completeOperation"
1475
+ | "publishPrepared"
1462
1476
  | "wait"
1463
1477
  | "stop"
1464
1478
  >
@@ -1797,6 +1811,9 @@ export type TrellisServiceInternalConnectArgs<
1797
1811
  contractKv?: TKv;
1798
1812
  };
1799
1813
 
1814
+ /**
1815
+ * @internal Shared by Trellis-owned service bootstrap paths.
1816
+ */
1800
1817
  export async function createConnectedService<
1801
1818
  TOwnedApi extends TrellisAPI,
1802
1819
  TTrellisApi extends TrellisAPI,
@@ -1991,12 +2008,12 @@ export async function createConnectedService<
1991
2008
  stores: Object.fromEntries(
1992
2009
  Object.entries(args.bindings.store ?? {}).map(([alias, binding]) => [
1993
2010
  alias,
1994
- new StoreHandle(args.nc, binding, storeHandleConstructorToken),
2011
+ new InternalStoreHandle(args.nc, binding, storeHandleConstructorToken),
1995
2012
  ]),
1996
2013
  ),
1997
2014
  });
1998
2015
 
1999
- const service = new TrellisService<TOwnedApi, TTrellisApi, TJobs, TKv>(
2016
+ const service = Reflect.construct(TrellisService, [
2000
2017
  args.name,
2001
2018
  args.auth,
2002
2019
  args.nc,
@@ -2011,7 +2028,7 @@ export async function createConnectedService<
2011
2028
  stopHealthPublishing,
2012
2029
  connection,
2013
2030
  trellisServiceConstructorToken,
2014
- );
2031
+ ]) as TrellisService<TOwnedApi, TTrellisApi, TJobs, TKv>;
2015
2032
  handlerResources = {
2016
2033
  kv: service.kv,
2017
2034
  store: service.store,
@@ -2757,6 +2774,143 @@ function createBoundJobsFacade<
2757
2774
  return boundJobs as BoundJobsFacadeOf<TJobs, TTrellisApi, TKv, TDeps>;
2758
2775
  }
2759
2776
 
2777
+ /**
2778
+ * Connects a service with caller-supplied runtime dependencies for tests and
2779
+ * Trellis-owned internals. This helper is intentionally not re-exported from
2780
+ * public package subpaths.
2781
+ *
2782
+ * @internal
2783
+ */
2784
+ export function connectTrellisServiceWithRuntimeDeps<
2785
+ const TContract extends ServiceContract<
2786
+ TrellisAPI,
2787
+ TrellisAPI | undefined,
2788
+ ContractJobsMetadata,
2789
+ ContractKvMetadata
2790
+ >,
2791
+ >(
2792
+ args: TrellisServiceConnectArgs<TContract>,
2793
+ deps: Partial<TrellisServiceRuntimeDeps>,
2794
+ ): AsyncResult<
2795
+ TrellisService<
2796
+ ContractOwnedApi<TContract>,
2797
+ ContractTrellisApi<TContract>,
2798
+ ContractJobsOf<TContract>,
2799
+ ContractKvOf<TContract>
2800
+ >,
2801
+ TransportError | UnexpectedError
2802
+ > {
2803
+ return AsyncResult.from((async () => {
2804
+ try {
2805
+ type TOwnedApi = ContractOwnedApi<TContract>;
2806
+ type TTrellisApi = ContractTrellisApi<TContract>;
2807
+
2808
+ const runtimeDeps = {
2809
+ ...(await loadDefaultServiceRuntimeDeps()),
2810
+ ...deps,
2811
+ } satisfies TrellisServiceRuntimeDeps;
2812
+ if (automaticTelemetryEnabled(args.telemetry)) {
2813
+ runtimeDeps.initTelemetry?.(args.name);
2814
+ }
2815
+ const auth = await createAuth({ sessionKeySeed: args.sessionKeySeed });
2816
+ const bootstrapLog = resolveServiceLogger(args.server?.log);
2817
+ const bootstrap = await fetchServiceBootstrapInfo({
2818
+ trellisUrl: args.trellisUrl,
2819
+ serviceName: args.name,
2820
+ contractId: args.contract.CONTRACT_ID,
2821
+ contractDigest: args.contract.CONTRACT_DIGEST,
2822
+ contract: args.contract.CONTRACT,
2823
+ auth,
2824
+ log: bootstrapLog,
2825
+ });
2826
+ const { authenticator: authTokenAuthenticator, inboxPrefix } = await auth
2827
+ .natsConnectOptions({
2828
+ contractDigest: args.contract.CONTRACT_DIGEST,
2829
+ });
2830
+
2831
+ let nc: NatsConnection;
2832
+ try {
2833
+ nc = await runtimeDeps.connect({
2834
+ servers: selectRuntimeTransportServers(
2835
+ bootstrap.connectInfo.transports,
2836
+ ),
2837
+ maxReconnectAttempts: DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS,
2838
+ waitOnFirstConnect: DEFAULT_SERVICE_RUNTIME_WAIT_ON_FIRST_CONNECT,
2839
+ inboxPrefix,
2840
+ authenticator: [
2841
+ authTokenAuthenticator,
2842
+ jwtAuthenticator(
2843
+ bootstrap.connectInfo.transport.sentinel.jwt,
2844
+ new TextEncoder().encode(
2845
+ bootstrap.connectInfo.transport.sentinel.seed,
2846
+ ),
2847
+ ),
2848
+ ],
2849
+ });
2850
+ } catch (cause) {
2851
+ throw new TransportError({
2852
+ code: "trellis.runtime.connect_failed",
2853
+ message: "Trellis could not open the service runtime connection.",
2854
+ hint:
2855
+ "Retry the connection. If it keeps failing, check Trellis transport availability.",
2856
+ cause,
2857
+ context: {
2858
+ trellisUrl: args.trellisUrl,
2859
+ contractId: args.contract.CONTRACT_ID,
2860
+ contractDigest: args.contract.CONTRACT_DIGEST,
2861
+ },
2862
+ });
2863
+ }
2864
+
2865
+ try {
2866
+ const server = args.contract.API.trellis
2867
+ ? {
2868
+ ...(args.server ?? {}),
2869
+ api: args.contract.API.owned,
2870
+ trellisApi: args.contract.API.trellis as TTrellisApi,
2871
+ }
2872
+ : {
2873
+ ...(args.server ?? {}),
2874
+ api: args.contract.API.owned,
2875
+ };
2876
+
2877
+ return Result.ok(
2878
+ await createConnectedService<
2879
+ TOwnedApi,
2880
+ TTrellisApi,
2881
+ ContractJobsOf<TContract>,
2882
+ ContractKvOf<TContract>
2883
+ >({
2884
+ name: args.name,
2885
+ auth,
2886
+ nc,
2887
+ contractId: args.contract.CONTRACT_ID,
2888
+ contractDigest: args.contract.CONTRACT_DIGEST,
2889
+ contractJobs:
2890
+ (args.contract[CONTRACT_JOBS_METADATA] ?? {}) as ContractJobsOf<
2891
+ TContract
2892
+ >,
2893
+ contractKv:
2894
+ (args.contract[CONTRACT_KV_METADATA] ?? {}) as ContractKvOf<
2895
+ TContract
2896
+ >,
2897
+ contractEventConsumers: args.contract.CONTRACT.eventConsumers,
2898
+ server,
2899
+ bindings: bootstrap.binding.resources,
2900
+ }),
2901
+ );
2902
+ } catch (cause) {
2903
+ await closeFailedServiceBootstrapConnection(nc);
2904
+ throw cause;
2905
+ }
2906
+ } catch (cause) {
2907
+ return Result.err(
2908
+ cause instanceof TransportError ? cause : toUnexpectedError(cause),
2909
+ );
2910
+ }
2911
+ })());
2912
+ }
2913
+
2760
2914
  export class TrellisService<
2761
2915
  TOwnedApi extends TrellisAPI = TrellisAPI,
2762
2916
  TTrellisApi extends TrellisAPI = TOwnedApi,
@@ -2765,8 +2919,8 @@ export class TrellisService<
2765
2919
  > {
2766
2920
  readonly name: string;
2767
2921
  readonly auth: SessionAuth;
2768
- readonly nc: NatsConnection;
2769
2922
  readonly #server: TrellisServiceRuntimeFor<TOwnedApi & TTrellisApi>;
2923
+ readonly #nc: NatsConnection;
2770
2924
  readonly #handlerTrellis: Trellis<TTrellisApi, TKv, TJobs>;
2771
2925
  /** Event lifecycle surface for service startup listeners and publishers. */
2772
2926
  readonly event: ActiveEventFacade<TTrellisApi>;
@@ -2783,7 +2937,7 @@ export class TrellisService<
2783
2937
  #waitPromise?: Promise<void>;
2784
2938
  #stopPromise?: Promise<void>;
2785
2939
 
2786
- constructor(
2940
+ private constructor(
2787
2941
  name: string,
2788
2942
  auth: SessionAuth,
2789
2943
  nc: NatsConnection,
@@ -2806,7 +2960,7 @@ export class TrellisService<
2806
2960
 
2807
2961
  this.name = name;
2808
2962
  this.auth = auth;
2809
- this.nc = nc;
2963
+ this.#nc = nc;
2810
2964
  this.#server = server;
2811
2965
  Object.defineProperty(this, "server", {
2812
2966
  value: server,
@@ -2818,7 +2972,10 @@ export class TrellisService<
2818
2972
  this.store = Object.fromEntries(
2819
2973
  Object.entries(storeBindings).map((
2820
2974
  [alias, binding],
2821
- ) => [alias, new StoreHandle(nc, binding, storeHandleConstructorToken)]),
2975
+ ) => [
2976
+ alias,
2977
+ new InternalStoreHandle(nc, binding, storeHandleConstructorToken),
2978
+ ]),
2822
2979
  );
2823
2980
  this.#operationTransfer = operationTransfer;
2824
2981
  const jobs = createJobsFacade<TJobs, TTrellisApi, TKv>({
@@ -2854,7 +3011,6 @@ export class TrellisService<
2854
3011
  return {
2855
3012
  name: this.name,
2856
3013
  auth: this.auth,
2857
- nc: this.nc,
2858
3014
  event: this.#createBoundEventFacade(deps),
2859
3015
  kv: this.kv,
2860
3016
  store: this.store,
@@ -2865,12 +3021,20 @@ export class TrellisService<
2865
3021
  createTransfer: (args) => this.createTransfer(args),
2866
3022
  completeOperation: (operationId, output) =>
2867
3023
  this.completeOperation(operationId, output),
3024
+ publishPrepared: (event) => this.publishPrepared(event),
2868
3025
  wait: () => this.wait(),
2869
3026
  stop: () => this.stop(),
2870
3027
  with: (nextDeps) => this.with(nextDeps),
2871
3028
  };
2872
3029
  }
2873
3030
 
3031
+ /** Publishes a prepared event through the service runtime connection. */
3032
+ publishPrepared(
3033
+ event: PreparedTrellisEvent,
3034
+ ): AsyncResult<void, UnexpectedError> {
3035
+ return this.#handlerTrellis.publishPrepared(event);
3036
+ }
3037
+
2874
3038
  #createBoundHealth<TDeps>(deps: TDeps): BoundServiceHealth<TDeps> {
2875
3039
  const health = this.health;
2876
3040
  return {
@@ -3147,7 +3311,6 @@ export class TrellisService<
3147
3311
  >,
3148
3312
  >(
3149
3313
  args: TrellisServiceConnectArgs<TContract>,
3150
- deps?: Partial<TrellisServiceRuntimeDeps>,
3151
3314
  ): AsyncResult<
3152
3315
  TrellisService<
3153
3316
  ContractOwnedApi<TContract>,
@@ -3157,123 +3320,14 @@ export class TrellisService<
3157
3320
  >,
3158
3321
  TransportError | UnexpectedError
3159
3322
  > {
3160
- return AsyncResult.from((async () => {
3161
- try {
3162
- type TOwnedApi = ContractOwnedApi<TContract>;
3163
- type TTrellisApi = ContractTrellisApi<TContract>;
3164
-
3165
- const runtimeDeps = {
3166
- ...(await loadDefaultServiceRuntimeDeps()),
3167
- ...deps,
3168
- } satisfies TrellisServiceRuntimeDeps;
3169
- if (automaticTelemetryEnabled(args.telemetry)) {
3170
- runtimeDeps.initTelemetry?.(args.name);
3171
- }
3172
- const auth = await createAuth({ sessionKeySeed: args.sessionKeySeed });
3173
- const bootstrapLog = resolveServiceLogger(args.server?.log);
3174
- const bootstrap = await fetchServiceBootstrapInfo({
3175
- trellisUrl: args.trellisUrl,
3176
- serviceName: args.name,
3177
- contractId: args.contract.CONTRACT_ID,
3178
- contractDigest: args.contract.CONTRACT_DIGEST,
3179
- contract: args.contract.CONTRACT,
3180
- auth,
3181
- log: bootstrapLog,
3182
- });
3183
- const { authenticator: authTokenAuthenticator, inboxPrefix } =
3184
- await auth
3185
- .natsConnectOptions({
3186
- contractDigest: args.contract.CONTRACT_DIGEST,
3187
- });
3188
-
3189
- let nc: NatsConnection;
3190
- try {
3191
- nc = await runtimeDeps.connect({
3192
- servers: selectRuntimeTransportServers(
3193
- bootstrap.connectInfo.transports,
3194
- ),
3195
- maxReconnectAttempts: DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS,
3196
- waitOnFirstConnect: DEFAULT_SERVICE_RUNTIME_WAIT_ON_FIRST_CONNECT,
3197
- inboxPrefix,
3198
- authenticator: [
3199
- authTokenAuthenticator,
3200
- jwtAuthenticator(
3201
- bootstrap.connectInfo.transport.sentinel.jwt,
3202
- new TextEncoder().encode(
3203
- bootstrap.connectInfo.transport.sentinel.seed,
3204
- ),
3205
- ),
3206
- ],
3207
- });
3208
- } catch (cause) {
3209
- throw new TransportError({
3210
- code: "trellis.runtime.connect_failed",
3211
- message: "Trellis could not open the service runtime connection.",
3212
- hint:
3213
- "Retry the connection. If it keeps failing, check Trellis transport availability.",
3214
- cause,
3215
- context: {
3216
- trellisUrl: args.trellisUrl,
3217
- contractId: args.contract.CONTRACT_ID,
3218
- contractDigest: args.contract.CONTRACT_DIGEST,
3219
- },
3220
- });
3221
- }
3222
-
3223
- try {
3224
- const server = args.contract.API.trellis
3225
- ? {
3226
- ...(args.server ?? {}),
3227
- api: args.contract.API.owned,
3228
- trellisApi: args.contract.API.trellis as TTrellisApi,
3229
- }
3230
- : {
3231
- ...(args.server ?? {}),
3232
- api: args.contract.API.owned,
3233
- };
3234
-
3235
- return Result.ok(
3236
- await createConnectedService<
3237
- TOwnedApi,
3238
- TTrellisApi,
3239
- ContractJobsOf<TContract>,
3240
- ContractKvOf<TContract>
3241
- >({
3242
- name: args.name,
3243
- auth,
3244
- nc,
3245
- contractId: args.contract.CONTRACT_ID,
3246
- contractDigest: args.contract.CONTRACT_DIGEST,
3247
- contractJobs:
3248
- (args.contract[CONTRACT_JOBS_METADATA] ?? {}) as ContractJobsOf<
3249
- TContract
3250
- >,
3251
- contractKv:
3252
- (args.contract[CONTRACT_KV_METADATA] ?? {}) as ContractKvOf<
3253
- TContract
3254
- >,
3255
- contractEventConsumers: args.contract.CONTRACT.eventConsumers,
3256
- server,
3257
- bindings: bootstrap.binding.resources,
3258
- }),
3259
- );
3260
- } catch (cause) {
3261
- await closeFailedServiceBootstrapConnection(nc);
3262
- throw cause;
3263
- }
3264
- } catch (cause) {
3265
- return Result.err(
3266
- cause instanceof TransportError ? cause : toUnexpectedError(cause),
3267
- );
3268
- }
3269
- })());
3323
+ return connectTrellisServiceWithRuntimeDeps(args, {});
3270
3324
  }
3271
3325
 
3272
3326
  async wait(): Promise<void> {
3273
3327
  this.#waitPromise ??= (async () => {
3274
3328
  try {
3275
3329
  await this.#managedJobWorkers.start().orThrow();
3276
- const closed = await this.nc.closed();
3330
+ const closed = await this.#nc.closed();
3277
3331
  if (closed instanceof Error) {
3278
3332
  throw closed;
3279
3333
  }
@@ -3299,6 +3353,12 @@ export class TrellisService<
3299
3353
  await this.#operationTransfer.stop();
3300
3354
  } finally {
3301
3355
  await this.#server.stop();
3356
+ this.connection.setStatus({
3357
+ kind: this.connection.status.kind,
3358
+ phase: "closed",
3359
+ observedAt: new Date(),
3360
+ transport: { name: "nats" },
3361
+ });
3302
3362
  }
3303
3363
  }
3304
3364
  }
package/src/server.ts CHANGED
@@ -1799,12 +1799,12 @@ export class TrellisServiceRuntime extends Trellis<TrellisAPI, TrellisMode> {
1799
1799
 
1800
1800
  async stop(): Promise<void> {
1801
1801
  this.#stopPromise ??= (async () => {
1802
- if (this.natsConnection.isClosed()) {
1802
+ if (this.nats.isClosed()) {
1803
1803
  return;
1804
1804
  }
1805
1805
 
1806
1806
  try {
1807
- await this.natsConnection.drain();
1807
+ await this.nats.drain();
1808
1808
  } catch (cause) {
1809
1809
  if (
1810
1810
  !(cause instanceof Error) ||
@@ -1813,7 +1813,7 @@ export class TrellisServiceRuntime extends Trellis<TrellisAPI, TrellisMode> {
1813
1813
  throw cause;
1814
1814
  }
1815
1815
 
1816
- await this.natsConnection.closed().catch(() => undefined);
1816
+ await this.nats.closed().catch(() => undefined);
1817
1817
  }
1818
1818
  })();
1819
1819
 
@@ -1,4 +1,3 @@
1
- import type { NatsConnection } from "@nats-io/nats-core";
2
1
  import { type AsyncResult, type BaseError, isErr } from "@qlever-llc/result";
3
2
  import { type StaticDecode, Type } from "typebox";
4
3
  import type { PreparedTrellisEvent, Trellis } from "../trellis.js";
@@ -387,19 +386,6 @@ export type KvOutboxRecord = StaticDecode<typeof KvOutboxRecordSchema>;
387
386
  export class NatsKvOutboxRepository implements OutboxRepository {
388
387
  constructor(readonly kv: OutboxKvStore) {}
389
388
 
390
- /** Opens or creates the KV bucket used for durable outbox records. */
391
- static async open(
392
- nats: NatsConnection,
393
- bucket = "trellis_outbox",
394
- ): Promise<NatsKvOutboxRepository> {
395
- const opened = await TypedKV.open(nats, bucket, KvOutboxRecordSchema, {
396
- history: 1,
397
- });
398
- const value = opened.take();
399
- if (isErr(value)) throw value.error;
400
- return new NatsKvOutboxRepository(value);
401
- }
402
-
403
389
  async enqueue(event: PreparedTrellisEvent): Promise<OutboxMessage> {
404
390
  const now = new Date().toISOString();
405
391
  const record: KvOutboxRecord = {
@@ -500,20 +486,7 @@ export class NatsKvOutboxRepository implements OutboxRepository {
500
486
 
501
487
  /** Durable NATS KV inbox repository for event-id duplicate suppression. */
502
488
  export class NatsKvInboxRepository implements InboxRepository {
503
- private constructor(readonly kv: TypedKV<typeof KvInboxRecordSchema>) {}
504
-
505
- /** Opens or creates the KV bucket used for durable inbox records. */
506
- static async open(
507
- nats: NatsConnection,
508
- bucket = "trellis_inbox",
509
- ): Promise<NatsKvInboxRepository> {
510
- const opened = await TypedKV.open(nats, bucket, KvInboxRecordSchema, {
511
- history: 1,
512
- });
513
- const value = opened.take();
514
- if (isErr(value)) throw value.error;
515
- return new NatsKvInboxRepository(value);
516
- }
489
+ constructor(readonly kv: TypedKV<typeof KvInboxRecordSchema>) {}
517
490
 
518
491
  async record(messageId: string, now: Date = new Date()): Promise<boolean> {
519
492
  // Durable NATS KV dedupe is useful for event handlers without SQL state, but
@@ -9,19 +9,22 @@ type ProcessLike = {
9
9
  env?: Record<string, string | undefined>;
10
10
  };
11
11
 
12
+ type EnvironmentGlobalThis = typeof dntShim.dntGlobalThis & {
13
+ Deno?: DenoLike;
14
+ process?: ProcessLike;
15
+ };
16
+
12
17
  // Shared telemetry code needs environment access without assuming Deno or Node.
13
18
  export function getEnv(key: string): string | undefined {
14
- const deno = dntShim.dntGlobalThis as typeof dntShim.dntGlobalThis & { Deno?: DenoLike };
15
- if (deno.Deno?.env?.get) {
19
+ const load = new Function("return globalThis") as () => EnvironmentGlobalThis;
20
+ const environmentGlobal = load();
21
+ if (environmentGlobal.Deno?.env?.get) {
16
22
  try {
17
- return deno.Deno.env.get(key);
23
+ return environmentGlobal.Deno.env.get(key);
18
24
  } catch {
19
25
  return undefined;
20
26
  }
21
27
  }
22
28
 
23
- const processGlobal = dntShim.dntGlobalThis as typeof dntShim.dntGlobalThis & {
24
- process?: ProcessLike;
25
- };
26
- return processGlobal.process?.env?.[key];
29
+ return environmentGlobal.process?.env?.[key];
27
30
  }
package/src/trellis.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  CONTRACT_JOBS_METADATA,
23
23
  CONTRACT_KV_METADATA,
24
24
  CONTRACT_STATE_METADATA,
25
- type ContractEventConsumers,
26
25
  type ContractJobsMetadata,
27
26
  type ContractKvMetadata,
28
27
  type EventConsumerResourceBinding,
@@ -1117,10 +1116,28 @@ function createEventListenerContext(args: {
1117
1116
  }
1118
1117
 
1119
1118
  type RuntimeEventConsumers = {
1120
- metadata?: ContractEventConsumers;
1119
+ metadata?: RuntimeEventConsumerGroups;
1121
1120
  bindings?: Record<string, EventConsumerResourceBinding>;
1122
1121
  };
1123
1122
 
1123
+ type RuntimeEventConsumerGroup = {
1124
+ uses?: Readonly<Record<string, readonly string[]>>;
1125
+ self?: readonly string[];
1126
+ };
1127
+
1128
+ type RuntimeEventConsumerGroups = Readonly<
1129
+ Record<string, RuntimeEventConsumerGroup>
1130
+ >;
1131
+
1132
+ function eventConsumerGroupEvents(group: RuntimeEventConsumerGroup): string[] {
1133
+ const events = new Set<string>();
1134
+ for (const groupEvents of Object.values(group.uses ?? {})) {
1135
+ for (const event of groupEvents) events.add(event);
1136
+ }
1137
+ for (const event of group.self ?? []) events.add(event);
1138
+ return [...events].sort();
1139
+ }
1140
+
1124
1141
  type TrellisInternalOpts<TA extends AnyTrellisAPI> = TrellisOpts<TA> & {
1125
1142
  eventConsumers?: RuntimeEventConsumers;
1126
1143
  };
@@ -1472,7 +1489,6 @@ export type ClientTrellis<
1472
1489
  readonly api: TA;
1473
1490
  readonly state: StateFacade<TState>;
1474
1491
  readonly connection: TrellisConnection;
1475
- readonly natsConnection: NatsConnection;
1476
1492
  readonly rpc: ActiveRpcFacade<TA>;
1477
1493
  readonly event: ActiveEventFacade<TA>;
1478
1494
  readonly feed: ActiveFeedFacade<TA>;
@@ -2077,13 +2093,6 @@ export class Trellis<
2077
2093
  this.operation = this.#createOperationFacade();
2078
2094
  }
2079
2095
 
2080
- /**
2081
- * Returns the underlying NATS connection.
2082
- */
2083
- get natsConnection(): NatsConnection {
2084
- return this.nats;
2085
- }
2086
-
2087
2096
  #createStateFacade(state: TState | undefined): StateFacade<TState> {
2088
2097
  const stores = (state ?? {}) as RuntimeStateStores;
2089
2098
  const facade = Object.fromEntries(
@@ -4215,7 +4224,7 @@ export class Trellis<
4215
4224
  const bindings = this.#eventConsumers.bindings ?? {};
4216
4225
  const groups = Object.entries(metadata ?? {})
4217
4226
  .filter(([, group]) =>
4218
- group.events.some((entry) => entry.event === String(event))
4227
+ eventConsumerGroupEvents(group).includes(String(event))
4219
4228
  )
4220
4229
  .map(([group]) => group);
4221
4230
 
@@ -4339,9 +4348,9 @@ export class Trellis<
4339
4348
  ): boolean {
4340
4349
  const metadata = this.#eventConsumers.metadata?.[group];
4341
4350
  if (!metadata) return false;
4342
- return metadata.events.every((entry) =>
4351
+ return eventConsumerGroupEvents(metadata).every((event) =>
4343
4352
  loop.registrations.some((registration) =>
4344
- String(registration.event) === entry.event
4353
+ String(registration.event) === event
4345
4354
  )
4346
4355
  );
4347
4356
  }