@getstrata/core 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.0.4
4
+
5
+ - Place cache rollout guidance beside outbox docs without overlapping API additions
6
+
3
7
  ## 2.0.3
4
8
 
5
9
  - Persist transactional events with leased tenant-aware SQL outbox workers
@@ -21,6 +21,8 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
21
21
  protected readonly table: TableDefinition<TEntity, PrimaryKey>;
22
22
  protected readonly connection: DatabaseConnection;
23
23
  constructor(table: TableDefinition<TEntity, PrimaryKey>, connection?: DatabaseConnection);
24
+ private assertTransactionalEvent;
25
+ private runModelMutation;
24
26
  count(options?: ExtendedQueryOptions<TEntity>): Promise<number>;
25
27
  findAll(options?: ExtendedQueryOptions<TEntity>): Promise<TEntity[]>;
26
28
  paginate(options: {
@@ -1,6 +1,10 @@
1
1
  type EventListener = (payload: unknown) => void | Promise<void>;
2
2
  declare class EventBus {
3
3
  constructor();
4
+ private readonly transactionalListeners;
5
+ listenTransactional(event: string, listener: EventListener): () => void;
6
+ hasTransactionalListeners(event: string): boolean;
7
+ dispatchTransactional(event: string, payload: unknown): Promise<void>;
4
8
  private readonly listeners;
5
9
  on(event: string, listener: EventListener): () => void;
6
10
  listen(event: string, listener: EventListener): () => void;
@@ -30,6 +30,7 @@ declare class SqlOutbox {
30
30
  private readonly maxPayloadBytes;
31
31
  private readonly tenantResolver;
32
32
  constructor(options: SqlOutboxOptions);
33
+ hasListener(name: string, event?: string): boolean;
33
34
  publish(name: string, payload: unknown, options?: {
34
35
  id?: string;
35
36
  version?: number;
@@ -1,6 +1,45 @@
1
+ /** JSON Schema keywords remain extensible; metadata cannot contain executable values. */
2
+ type OpenApiJson = null | boolean | number | string | OpenApiJson[] | {
3
+ [key: string]: OpenApiJson | undefined;
4
+ };
5
+ type OpenApiSchema = boolean | {
6
+ [keyword: string]: OpenApiJson | undefined;
7
+ };
8
+ interface OpenApiParameter {
9
+ name: string;
10
+ in: "header" | "query" | "path" | "cookie";
11
+ required?: boolean;
12
+ description?: string;
13
+ schema: OpenApiSchema;
14
+ }
15
+ interface OpenApiMediaType {
16
+ schema?: OpenApiSchema;
17
+ example?: OpenApiJson;
18
+ }
19
+ interface OpenApiResponse {
20
+ description: string;
21
+ content?: Record<string, OpenApiMediaType>;
22
+ }
23
+ interface OpenApiOperation {
24
+ summary?: string;
25
+ description?: string;
26
+ operationId?: string;
27
+ tags?: string[];
28
+ deprecated?: boolean;
29
+ parameters?: OpenApiParameter[];
30
+ requestBody?: {
31
+ required?: boolean;
32
+ description?: string;
33
+ content: Record<string, OpenApiMediaType>;
34
+ };
35
+ responses?: Record<string, OpenApiResponse>;
36
+ security?: Array<Record<string, string[]>>;
37
+ }
38
+ type OpenApiRouteMap = Record<string, Partial<Record<"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS", OpenApiOperation>>>;
1
39
  interface RegisteredRoute {
2
40
  method: string;
3
41
  path: string;
4
42
  middleware: string[];
43
+ openApi?: OpenApiOperation;
5
44
  }
6
- export type { RegisteredRoute };
45
+ export type { OpenApiJson, OpenApiMediaType, OpenApiOperation, OpenApiParameter, OpenApiResponse, OpenApiRouteMap, OpenApiSchema, RegisteredRoute, };
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -155,6 +155,24 @@ var init_tenantContext = __esm(() => {
155
155
  // ../../src/core/events/eventBus.ts
156
156
  class EventBus {
157
157
  constructor() {}
158
+ transactionalListeners = new Map;
159
+ listenTransactional(event, listener) {
160
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
161
+ handlers.add(listener);
162
+ this.transactionalListeners.set(event, handlers);
163
+ return () => {
164
+ handlers.delete(listener);
165
+ if (!handlers.size)
166
+ this.transactionalListeners.delete(event);
167
+ };
168
+ }
169
+ hasTransactionalListeners(event) {
170
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
171
+ }
172
+ async dispatchTransactional(event, payload) {
173
+ for (const listener of this.transactionalListeners.get(event) ?? [])
174
+ await listener(payload);
175
+ }
158
176
  listeners = new Map;
159
177
  on(event, listener) {
160
178
  return this.listen(event, listener);
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -168,7 +168,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
168
168
  "GET /scim/v2/Groups": "SCIM list groups (departments)"
169
169
  };
170
170
  function toOpenApiPath(path) {
171
- return path.replace(/:([A-Za-z_]+)/g, "{$1}");
171
+ return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
172
172
  }
173
173
  function toRelativeApiPath(path) {
174
174
  const prefix = apiPrefix();
@@ -196,6 +196,16 @@ function requiresBearerAuth(path, method) {
196
196
  }
197
197
  return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || relative === "/user" || relative.startsWith("/integrations/") || relative.startsWith("/audit-logs") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
198
198
  }
199
+ var DEFAULT_RESPONSES = {
200
+ "200": { description: "OK" },
201
+ "201": { description: "Created" },
202
+ "204": { description: "No Content" },
203
+ "400": { description: "Bad Request" },
204
+ "401": { description: "Unauthorized" },
205
+ "403": { description: "Forbidden" },
206
+ "404": { description: "Not Found" },
207
+ "422": { description: "Validation Error" }
208
+ };
199
209
  function generateOpenApiSpec(routes) {
200
210
  const paths = {};
201
211
  for (const route of routes) {
@@ -206,16 +216,8 @@ function generateOpenApiSpec(routes) {
206
216
  paths[openApiPath][method] = {
207
217
  summary: description,
208
218
  ...requiresBearerAuth(route.path, route.method) ? { security: [{ bearerAuth: [] }] } : {},
209
- responses: {
210
- "200": { description: "OK" },
211
- "201": { description: "Created" },
212
- "204": { description: "No Content" },
213
- "400": { description: "Bad Request" },
214
- "401": { description: "Unauthorized" },
215
- "403": { description: "Forbidden" },
216
- "404": { description: "Not Found" },
217
- "422": { description: "Validation Error" }
218
- }
219
+ ...route.openApi,
220
+ responses: route.openApi?.responses ?? structuredClone(DEFAULT_RESPONSES)
219
221
  };
220
222
  }
221
223
  return {
@@ -297,10 +299,38 @@ function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
297
299
  " }",
298
300
  ""
299
301
  ];
302
+ const contracts = Object.fromEntries(Object.entries(spec.paths).map(([path, methods]) => [
303
+ path,
304
+ Object.fromEntries(Object.entries(methods).filter(([, value]) => {
305
+ const operation = value;
306
+ return operation.parameters || operation.requestBody || operation.operationId || JSON.stringify(operation.responses) !== JSON.stringify(DEFAULT_RESPONSES);
307
+ }))
308
+ ]).filter(([, methods]) => Object.keys(methods).length));
309
+ if (Object.keys(contracts).length)
310
+ lines.splice(1, 0, ` static readonly operationContracts = ${JSON.stringify(contracts)} as const;`);
300
311
  for (const [path, methods] of Object.entries(spec.paths)) {
301
312
  const requestPath = toRequestPath(path, prefix);
302
- for (const method of Object.keys(methods)) {
313
+ for (const [method, operation] of Object.entries(methods)) {
314
+ const metadata = operation;
315
+ const headers = metadata.parameters?.filter((parameter) => parameter.in === "header") ?? [];
303
316
  const functionName = toMethodName(method, path, prefix);
317
+ const pathParameters = metadata.parameters?.filter((parameter) => parameter.in === "path") ?? [];
318
+ const queryParameters = metadata.parameters?.filter((parameter) => parameter.in === "query") ?? [];
319
+ const groups = [
320
+ { field: "operationHeaders", parameters: headers },
321
+ { field: "operationPath", parameters: pathParameters },
322
+ { field: "operationQuery", parameters: queryParameters }
323
+ ].filter((group) => group.parameters.length);
324
+ if (groups.length) {
325
+ const type = groups.map((group) => `${group.field}${group.parameters.some((parameter) => parameter.required) ? "" : "?"}: { ${group.parameters.map((parameter) => `${JSON.stringify(parameter.name)}${parameter.required ? "" : "?"}: ${parameter.schema?.type === "integer" || parameter.schema?.type === "number" ? "number" : parameter.schema?.type === "boolean" ? "boolean" : "string"}`).join("; ")} }`).join("; ");
326
+ const required = groups.some((group) => group.parameters.some((parameter) => parameter.required));
327
+ lines.push(` async ${functionName}(init: RequestInit & { ${type} }${required ? "" : " = {}"}): Promise<Response> {`, " const headers = new Headers(init.headers);", ` let path = ${JSON.stringify(requestPath)};`, ...groups.flatMap((group) => group.parameters.filter((parameter) => parameter.required).map((parameter) => ` if (init.${group.field}?.[${JSON.stringify(parameter.name)}] === undefined) throw new TypeError(${JSON.stringify(`Required operation ${group.field === "operationHeaders" ? "header" : group.field === "operationPath" ? "path parameter" : "query parameter"}: ${parameter.name}`)});`)), ...headers.map((parameter) => ` if (init.operationHeaders?.[${JSON.stringify(parameter.name)}] !== undefined) headers.set(${JSON.stringify(parameter.name)}, String(init.operationHeaders[${JSON.stringify(parameter.name)}]));`), ...pathParameters.map((parameter) => ` if (init.operationPath?.[${JSON.stringify(parameter.name)}] !== undefined) path = path.replaceAll(${JSON.stringify(`{${parameter.name}}`)}, encodeURIComponent(String(init.operationPath[${JSON.stringify(parameter.name)}])));`), ...queryParameters.length ? [
328
+ " const query = new URLSearchParams();",
329
+ ...queryParameters.map((parameter) => ` if (init.operationQuery?.[${JSON.stringify(parameter.name)}] !== undefined) query.set(${JSON.stringify(parameter.name)}, String(init.operationQuery[${JSON.stringify(parameter.name)}]));`),
330
+ " if (query.size) path += '?' + query.toString();"
331
+ ] : [], ` const { ${groups.map((group) => `${group.field}: _${group.field}`).join(", ")}, ...requestInit } = init;`, ` return await this.request(path, { ...requestInit, headers, method: ${JSON.stringify(method.toUpperCase())} });`, " }", "");
332
+ continue;
333
+ }
304
334
  lines.push(` async ${functionName}(init: RequestInit = {}): Promise<Response> {`, ` return await this.request("${requestPath}", { ...init, method: "${method.toUpperCase()}" });`, " }", "");
305
335
  }
306
336
  }
@@ -13,6 +13,16 @@ function validateOpenApiSpec(spec) {
13
13
  errors.push(`Path must start with '/': ${path}`);
14
14
  }
15
15
  for (const [method, operation] of Object.entries(methods)) {
16
+ const details = operation;
17
+ const seen = new Set;
18
+ for (const parameter of details.parameters ?? []) {
19
+ const identity = `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`;
20
+ if (seen.has(identity))
21
+ errors.push(`Duplicate parameter ${parameter.name} on ${method.toUpperCase()} ${path}.`);
22
+ seen.add(identity);
23
+ if (parameter.in === "path" && (!parameter.required || !path.includes(`{${parameter.name}}`)))
24
+ errors.push(`Path parameter ${parameter.name} must be required and match ${path}.`);
25
+ }
16
26
  if (!("responses" in operation)) {
17
27
  errors.push(`Operation ${method.toUpperCase()} ${path} is missing responses.`);
18
28
  }
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -133,6 +133,24 @@ var init_tenantContext = __esm(() => {
133
133
  // ../../src/core/events/eventBus.ts
134
134
  class EventBus {
135
135
  constructor() {}
136
+ transactionalListeners = new Map;
137
+ listenTransactional(event, listener) {
138
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
139
+ handlers.add(listener);
140
+ this.transactionalListeners.set(event, handlers);
141
+ return () => {
142
+ handlers.delete(listener);
143
+ if (!handlers.size)
144
+ this.transactionalListeners.delete(event);
145
+ };
146
+ }
147
+ hasTransactionalListeners(event) {
148
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
149
+ }
150
+ async dispatchTransactional(event, payload) {
151
+ for (const listener of this.transactionalListeners.get(event) ?? [])
152
+ await listener(payload);
153
+ }
136
154
  listeners = new Map;
137
155
  on(event, listener) {
138
156
  return this.listen(event, listener);
package/dist/index.js CHANGED
@@ -535,6 +535,24 @@ var init_transactionControl = __esm(() => {
535
535
  // ../../src/core/events/eventBus.ts
536
536
  class EventBus {
537
537
  constructor() {}
538
+ transactionalListeners = new Map;
539
+ listenTransactional(event, listener) {
540
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
541
+ handlers.add(listener);
542
+ this.transactionalListeners.set(event, handlers);
543
+ return () => {
544
+ handlers.delete(listener);
545
+ if (!handlers.size)
546
+ this.transactionalListeners.delete(event);
547
+ };
548
+ }
549
+ hasTransactionalListeners(event) {
550
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
551
+ }
552
+ async dispatchTransactional(event, payload) {
553
+ for (const listener of this.transactionalListeners.get(event) ?? [])
554
+ await listener(payload);
555
+ }
538
556
  listeners = new Map;
539
557
  on(event, listener) {
540
558
  return this.listen(event, listener);
@@ -621,6 +639,7 @@ async function dispatchQueuedModelEvents(queued) {
621
639
  }
622
640
  }
623
641
  async function dispatchModelEvent(event, payload) {
642
+ await eventBus.dispatchTransactional(event, payload);
624
643
  const state = deferredModelEvents.getStore();
625
644
  const frame = state?.frames[state.frames.length - 1];
626
645
  if (frame) {
@@ -3810,6 +3829,9 @@ class ConfigStore {
3810
3829
  return this.values.has(key);
3811
3830
  }
3812
3831
  }
3832
+ // ../../src/core/database/baseRepository.ts
3833
+ init_eventBus();
3834
+
3813
3835
  // ../../src/core/events/index.ts
3814
3836
  init_deferredModelEvents();
3815
3837
  init_eventBus();
@@ -3828,6 +3850,9 @@ function buildPaginationMeta(input) {
3828
3850
  };
3829
3851
  }
3830
3852
 
3853
+ // ../../src/core/database/baseRepository.ts
3854
+ init_connectionContext();
3855
+
3831
3856
  // ../../src/core/database/query.ts
3832
3857
  init_dialect();
3833
3858
  function quoteIdentifier(identifier) {
@@ -4952,6 +4977,9 @@ class RepositoryQuery {
4952
4977
  }
4953
4978
 
4954
4979
  // ../../src/core/database/baseRepository.ts
4980
+ init_transaction();
4981
+ init_transactionControl();
4982
+
4955
4983
  class BaseRepository {
4956
4984
  table;
4957
4985
  connection;
@@ -4959,6 +4987,17 @@ class BaseRepository {
4959
4987
  this.table = table;
4960
4988
  this.connection = connection;
4961
4989
  }
4990
+ assertTransactionalEvent(action) {
4991
+ if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
4992
+ return;
4993
+ if (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))
4994
+ throw new Error("Transactional model listeners require runInTransaction and the framework-bound repository connection before writing.");
4995
+ }
4996
+ async runModelMutation(action, operation) {
4997
+ if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
4998
+ return operation();
4999
+ return runInTransaction(operation);
5000
+ }
4962
5001
  async count(options = {}) {
4963
5002
  const { whereNodes, where, ...rest } = options;
4964
5003
  return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
@@ -5116,7 +5155,8 @@ class BaseRepository {
5116
5155
  return await this.incrementById(id, column, -amount, extra);
5117
5156
  }
5118
5157
  async create(values) {
5119
- return await withDatabaseErrorHandling(async () => {
5158
+ this.assertTransactionalEvent("created");
5159
+ return await this.runModelMutation("created", () => withDatabaseErrorHandling(async () => {
5120
5160
  const { text, params } = buildInsertQuery(this.table, values);
5121
5161
  const [record] = await this.connection.unsafe(text, params);
5122
5162
  if (!record) {
@@ -5125,10 +5165,11 @@ class BaseRepository {
5125
5165
  const entity = record;
5126
5166
  await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
5127
5167
  return entity;
5128
- });
5168
+ }));
5129
5169
  }
5130
5170
  async updateById(id, changes) {
5131
- return await withDatabaseErrorHandling(async () => {
5171
+ this.assertTransactionalEvent("updated");
5172
+ return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
5132
5173
  const { text, params } = buildUpdateQuery(this.table, id, changes);
5133
5174
  const [record] = await this.connection.unsafe(text, params);
5134
5175
  const entity = record ?? null;
@@ -5136,7 +5177,7 @@ class BaseRepository {
5136
5177
  await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
5137
5178
  }
5138
5179
  return entity;
5139
- });
5180
+ }));
5140
5181
  }
5141
5182
  async updateByIdOrThrow(id, changes, errorFactory) {
5142
5183
  const record = await this.updateById(id, changes);
@@ -5152,7 +5193,8 @@ class BaseRepository {
5152
5193
  return await this.forceDeleteById(id);
5153
5194
  }
5154
5195
  async softDeleteById(id) {
5155
- return await withDatabaseErrorHandling(async () => {
5196
+ this.assertTransactionalEvent("deleted");
5197
+ return await this.runModelMutation("deleted", () => withDatabaseErrorHandling(async () => {
5156
5198
  const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
5157
5199
  const [record] = await this.connection.unsafe(text, params);
5158
5200
  if (!record) {
@@ -5160,10 +5202,11 @@ class BaseRepository {
5160
5202
  }
5161
5203
  await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
5162
5204
  return true;
5163
- });
5205
+ }));
5164
5206
  }
5165
5207
  async forceDeleteById(id) {
5166
- return await withDatabaseErrorHandling(async () => {
5208
+ this.assertTransactionalEvent("force-deleted");
5209
+ return await this.runModelMutation("force-deleted", () => withDatabaseErrorHandling(async () => {
5167
5210
  const { text, params } = buildDeleteByIdQuery(this.table, id);
5168
5211
  const [row] = await this.connection.unsafe(text, params);
5169
5212
  if (!row) {
@@ -5173,10 +5216,11 @@ class BaseRepository {
5173
5216
  id
5174
5217
  });
5175
5218
  return true;
5176
- });
5219
+ }));
5177
5220
  }
5178
5221
  async restoreById(id) {
5179
- return await withDatabaseErrorHandling(async () => {
5222
+ this.assertTransactionalEvent("restored");
5223
+ return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
5180
5224
  const { text, params } = buildRestoreByIdQuery(this.table, id);
5181
5225
  const [record] = await this.connection.unsafe(text, params);
5182
5226
  if (!record) {
@@ -5185,7 +5229,7 @@ class BaseRepository {
5185
5229
  const entity = record;
5186
5230
  await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
5187
5231
  return entity;
5188
- });
5232
+ }));
5189
5233
  }
5190
5234
  withConnection(connection) {
5191
5235
  const clone = Object.create(Object.getPrototypeOf(this));
@@ -8417,6 +8461,10 @@ class SqlOutbox {
8417
8461
  this.listeners.set(listener.name, listener);
8418
8462
  }
8419
8463
  }
8464
+ hasListener(name, event) {
8465
+ const listener = this.listeners.get(name);
8466
+ return listener !== undefined && (event === undefined || listener.event === event);
8467
+ }
8420
8468
  async publish(name, payload, options = {}) {
8421
8469
  if (!hasActiveTransaction())
8422
8470
  throw new Error("Durable publication requires an active business transaction.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",