@hasna/events 0.1.13 → 0.1.14

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/dist/index.js CHANGED
@@ -98,11 +98,15 @@ function channelMatchesEvent(channel, event) {
98
98
 
99
99
  // src/storage.ts
100
100
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
101
+ import { Buffer as Buffer2 } from "buffer";
101
102
  import { existsSync } from "fs";
102
103
  import { homedir } from "os";
103
104
  import { join } from "path";
104
105
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
105
106
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
107
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
108
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
109
+ var MAX_EVENT_PAGE_LIMIT = 1000;
106
110
  function getEventsDataDir(override) {
107
111
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
108
112
  }
@@ -116,11 +120,13 @@ function getActiveEventsDirEnv() {
116
120
 
117
121
  class JsonEventsStore {
118
122
  dataDir;
123
+ runtime;
119
124
  channelsPath;
120
125
  eventsPath;
121
126
  deliveriesPath;
122
127
  constructor(dataDir = getEventsDataDir()) {
123
128
  this.dataDir = dataDir;
129
+ this.runtime = localJsonRuntime(dataDir);
124
130
  this.channelsPath = join(dataDir, "channels.json");
125
131
  this.eventsPath = join(dataDir, "events.json");
126
132
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -168,13 +174,58 @@ class JsonEventsStore {
168
174
  await this.writeJson(this.eventsPath, events);
169
175
  return event;
170
176
  }
171
- async listEvents() {
177
+ async appendEventOnce(event, options = {}) {
172
178
  await this.init();
173
- return this.readJson(this.eventsPath, []);
179
+ const events = await this.readJson(this.eventsPath, []);
180
+ const dedupe = options.dedupe !== false;
181
+ if (dedupe) {
182
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
183
+ if (existing) {
184
+ return {
185
+ event: existing,
186
+ stored: false,
187
+ deduped: true,
188
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
189
+ };
190
+ }
191
+ }
192
+ events.push(event);
193
+ await this.writeJson(this.eventsPath, events);
194
+ return {
195
+ event,
196
+ stored: true,
197
+ deduped: false,
198
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
199
+ };
200
+ }
201
+ async listEvents(options = {}) {
202
+ await this.init();
203
+ const events = await this.readJson(this.eventsPath, []);
204
+ return queryEvents(events, options);
205
+ }
206
+ async listEventsPage(options = {}) {
207
+ await this.init();
208
+ const events = await this.readJson(this.eventsPath, []);
209
+ const queried = queryEvents(events, {
210
+ eventId: options.eventId,
211
+ source: options.source,
212
+ type: options.type
213
+ });
214
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
215
+ const limit = normalizeEventPageLimit(options.limit);
216
+ const pageEvents = queried.slice(offset, offset + limit);
217
+ const nextOffset = offset + pageEvents.length;
218
+ const hasMore = nextOffset < queried.length;
219
+ return {
220
+ events: pageEvents,
221
+ cursor: options.cursor,
222
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
223
+ hasMore
224
+ };
174
225
  }
175
226
  async findEventByIdentity(identity) {
176
227
  const events = await this.listEvents();
177
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
228
+ return findEventByIdentity(events, identity);
178
229
  }
179
230
  async appendDelivery(result) {
180
231
  await this.init();
@@ -225,6 +276,83 @@ class JsonEventsStore {
225
276
  });
226
277
  }
227
278
  }
279
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
280
+ return {
281
+ mode: "local-files",
282
+ name: "json-events-store",
283
+ remote: false,
284
+ localFiles: true,
285
+ localSqlite: false,
286
+ postgres: false,
287
+ s3: false,
288
+ aws: false,
289
+ durable: true,
290
+ idempotency: "best-effort-local",
291
+ replayCursors: true,
292
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
293
+ };
294
+ }
295
+ function encodeLocalJsonEventCursor(offset, options = {}) {
296
+ if (!Number.isInteger(offset) || offset < 0)
297
+ throw new Error(`Invalid event cursor offset: ${offset}`);
298
+ const payload = {
299
+ offset,
300
+ eventId: options.eventId,
301
+ source: options.source,
302
+ type: options.type
303
+ };
304
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
305
+ }
306
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
307
+ if (!cursor)
308
+ return 0;
309
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
310
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
311
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
312
+ let payload;
313
+ try {
314
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
315
+ } catch {
316
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
317
+ }
318
+ const offset = payload.offset;
319
+ if (!Number.isInteger(offset) || offset < 0)
320
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
321
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
322
+ assertCursorFilter("source", payload.source, options.source);
323
+ assertCursorFilter("type", payload.type, options.type);
324
+ return offset;
325
+ }
326
+ function normalizeEventPageLimit(limit) {
327
+ if (limit === undefined)
328
+ return DEFAULT_EVENT_PAGE_LIMIT;
329
+ if (!Number.isInteger(limit) || limit < 1)
330
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
331
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
332
+ }
333
+ function queryEvents(events, options) {
334
+ let rows = events;
335
+ if (options.eventId)
336
+ rows = rows.filter((event) => event.id === options.eventId);
337
+ if (options.source)
338
+ rows = rows.filter((event) => event.source === options.source);
339
+ if (options.type)
340
+ rows = rows.filter((event) => event.type === options.type);
341
+ if (options.cursor) {
342
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
343
+ rows = rows.slice(offset);
344
+ }
345
+ if (options.limit !== undefined)
346
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
347
+ return rows;
348
+ }
349
+ function assertCursorFilter(name, cursorValue, optionValue) {
350
+ if (cursorValue !== optionValue)
351
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
352
+ }
353
+ function findEventByIdentity(events, identity) {
354
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
355
+ }
228
356
  async function getEventsStatus(dataDir) {
229
357
  const store = new JsonEventsStore(dataDir);
230
358
  await store.init();
@@ -241,6 +369,7 @@ async function getEventsStatus(dataDir) {
241
369
  service: "events",
242
370
  schemaVersion: "1.0",
243
371
  dataDir: store.dataDir,
372
+ storage: store.runtime,
244
373
  env: {
245
374
  primary: HASNA_EVENTS_DIR_ENV,
246
375
  fallback: HASNA_EVENTS_HOME_ENV,
@@ -457,6 +586,182 @@ function createDeliveryResult(event, channel, attempts) {
457
586
  completedAt: attempts.at(-1)?.completedAt ?? now()
458
587
  };
459
588
  }
589
+
590
+ // src/catalog.ts
591
+ class EventValidationError extends Error {
592
+ eventType;
593
+ issues;
594
+ constructor(eventType, issues) {
595
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
596
+ super(`Event validation failed for type "${eventType}": ${detail}`);
597
+ this.name = "EventValidationError";
598
+ this.eventType = eventType;
599
+ this.issues = issues;
600
+ }
601
+ }
602
+
603
+ class EventTypeCatalog {
604
+ definitions = new Map;
605
+ register(definition) {
606
+ this.definitions.set(definition.type, definition);
607
+ return this;
608
+ }
609
+ unregister(type) {
610
+ return this.definitions.delete(type);
611
+ }
612
+ has(type) {
613
+ return this.definitions.has(type);
614
+ }
615
+ get(type) {
616
+ return this.definitions.get(type);
617
+ }
618
+ list() {
619
+ return [...this.definitions.values()];
620
+ }
621
+ validateEvent(event) {
622
+ const definition = this.definitions.get(event.type);
623
+ if (!definition)
624
+ return { ok: true };
625
+ return definition.validate(event.data, event);
626
+ }
627
+ assertEventValid(event) {
628
+ const result = this.validateEvent(event);
629
+ if (!result.ok) {
630
+ throw new EventValidationError(event.type, result.issues);
631
+ }
632
+ }
633
+ }
634
+ var defaultEventTypeCatalog = new EventTypeCatalog;
635
+ var DISTRIBUTION_EVENT_TYPES = {
636
+ releasePublished: "release.published",
637
+ rolloutStarted: "release.rollout.started",
638
+ rolloutCompleted: "release.rollout.completed",
639
+ rolloutFailed: "release.rollout.failed",
640
+ appInstalled: "app.installed",
641
+ announcementSent: "announcement.sent",
642
+ feedbackCreated: "feedback.created",
643
+ feedbackTriaged: "feedback.triaged"
644
+ };
645
+ var DISTRIBUTION_EVENT_CONTRACT_SCHEMAS = {
646
+ "release.published": "hasna.release.v1",
647
+ "release.rollout.started": "hasna.rollout_record.v1",
648
+ "release.rollout.completed": "hasna.rollout_record.v1",
649
+ "release.rollout.failed": "hasna.rollout_record.v1",
650
+ "app.installed": "hasna.rollout_record.v1",
651
+ "announcement.sent": "hasna.announcement.v1",
652
+ "feedback.created": "hasna.feedback.v1",
653
+ "feedback.triaged": "hasna.feedback.v1"
654
+ };
655
+ var PUBLISH_PATHS = ["skill", "ci", "backfilled"];
656
+ var ROLLOUT_ACTIONS = ["install", "update", "rollback", "freeze-blocked"];
657
+ function requireString(data, key, issues) {
658
+ const value = data[key];
659
+ if (typeof value !== "string" || value.trim().length === 0) {
660
+ issues.push({ path: key, message: "must be a non-empty string" });
661
+ }
662
+ }
663
+ function optionalString(data, key, issues) {
664
+ const value = data[key];
665
+ if (value !== undefined && (typeof value !== "string" || value.trim().length === 0)) {
666
+ issues.push({ path: key, message: "must be a non-empty string when present" });
667
+ }
668
+ }
669
+ function optionalEnum(data, key, allowed, issues) {
670
+ const value = data[key];
671
+ if (value !== undefined && (typeof value !== "string" || !allowed.includes(value))) {
672
+ issues.push({ path: key, message: `must be one of: ${allowed.join(", ")}` });
673
+ }
674
+ }
675
+ function optionalStringArray(data, key, issues) {
676
+ const value = data[key];
677
+ if (value === undefined)
678
+ return;
679
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
680
+ issues.push({ path: key, message: "must be an array of non-empty strings when present" });
681
+ }
682
+ }
683
+ function toResult(issues) {
684
+ return issues.length === 0 ? { ok: true } : { ok: false, issues };
685
+ }
686
+ var validateReleasePublishedData = (data) => {
687
+ const issues = [];
688
+ requireString(data, "appId", issues);
689
+ requireString(data, "package", issues);
690
+ requireString(data, "version", issues);
691
+ optionalString(data, "gitSha", issues);
692
+ optionalString(data, "publishedAt", issues);
693
+ optionalEnum(data, "publishPath", PUBLISH_PATHS, issues);
694
+ return toResult(issues);
695
+ };
696
+ var validateRolloutData = (data, event) => {
697
+ const issues = [];
698
+ requireString(data, "appId", issues);
699
+ requireString(data, "package", issues);
700
+ requireString(data, "version", issues);
701
+ requireString(data, "machine", issues);
702
+ optionalEnum(data, "action", ROLLOUT_ACTIONS, issues);
703
+ if (event.type === "release.rollout.completed" || event.type === "release.rollout.failed") {
704
+ requireString(data, "result", issues);
705
+ }
706
+ return toResult(issues);
707
+ };
708
+ var validateAppInstalledData = (data) => {
709
+ const issues = [];
710
+ requireString(data, "appId", issues);
711
+ requireString(data, "package", issues);
712
+ requireString(data, "version", issues);
713
+ requireString(data, "machine", issues);
714
+ return toResult(issues);
715
+ };
716
+ var validateAnnouncementSentData = (data) => {
717
+ const issues = [];
718
+ requireString(data, "campaignId", issues);
719
+ optionalString(data, "appId", issues);
720
+ optionalString(data, "audienceId", issues);
721
+ optionalString(data, "releaseId", issues);
722
+ optionalStringArray(data, "channels", issues);
723
+ return toResult(issues);
724
+ };
725
+ var validateFeedbackCreatedData = (data) => {
726
+ const issues = [];
727
+ requireString(data, "feedbackId", issues);
728
+ optionalString(data, "appId", issues);
729
+ optionalString(data, "source", issues);
730
+ optionalString(data, "summary", issues);
731
+ return toResult(issues);
732
+ };
733
+ var validateFeedbackTriagedData = (data) => {
734
+ const issues = [];
735
+ requireString(data, "feedbackId", issues);
736
+ requireString(data, "disposition", issues);
737
+ optionalString(data, "appId", issues);
738
+ optionalString(data, "triagedBy", issues);
739
+ return toResult(issues);
740
+ };
741
+ function createDistributionEventDefinitions() {
742
+ const bind = (type, validate, description) => ({
743
+ type,
744
+ contractSchemaId: DISTRIBUTION_EVENT_CONTRACT_SCHEMAS[type],
745
+ description,
746
+ validate
747
+ });
748
+ return [
749
+ bind("release.published", validateReleasePublishedData, "A package version was published"),
750
+ bind("release.rollout.started", validateRolloutData, "A rollout of a release to a machine started"),
751
+ bind("release.rollout.completed", validateRolloutData, "A rollout of a release to a machine completed"),
752
+ bind("release.rollout.failed", validateRolloutData, "A rollout of a release to a machine failed"),
753
+ bind("app.installed", validateAppInstalledData, "An app was installed on a machine"),
754
+ bind("announcement.sent", validateAnnouncementSentData, "An announcement campaign was sent"),
755
+ bind("feedback.created", validateFeedbackCreatedData, "User or agent feedback was captured"),
756
+ bind("feedback.triaged", validateFeedbackTriagedData, "Captured feedback was triaged")
757
+ ];
758
+ }
759
+ function registerDistributionEventTypes(catalog = defaultEventTypeCatalog) {
760
+ for (const definition of createDistributionEventDefinitions()) {
761
+ catalog.register(definition);
762
+ }
763
+ return catalog;
764
+ }
460
765
  // src/index.ts
461
766
  import { randomUUID as randomUUID2 } from "crypto";
462
767
  function createEvent(input) {
@@ -479,10 +784,14 @@ class EventsClient {
479
784
  store;
480
785
  redactors;
481
786
  transportOptions;
787
+ catalog;
788
+ validateCatalogTypes;
482
789
  constructor(options = {}) {
483
790
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
484
791
  this.redactors = options.redactors ?? [];
485
792
  this.transportOptions = { fetchImpl: options.fetchImpl };
793
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
794
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
486
795
  }
487
796
  async addChannel(input) {
488
797
  const timestamp = new Date().toISOString();
@@ -500,18 +809,40 @@ class EventsClient {
500
809
  }
501
810
  async emit(input, options = {}) {
502
811
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
503
- if (options.dedupe !== false) {
504
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
505
- if (existing) {
506
- return { event: existing, deliveries: [], deduped: true };
507
- }
812
+ if (options.validate ?? this.validateCatalogTypes) {
813
+ this.catalog.assertEventValid(event);
814
+ }
815
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
816
+ if (append.deduped) {
817
+ return { event: append.event, deliveries: [], deduped: true };
508
818
  }
509
- await this.store.appendEvent(event);
510
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
511
- return { event, deliveries, deduped: false };
819
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
820
+ return { event: append.event, deliveries, deduped: false };
512
821
  }
513
- async listEvents() {
514
- return this.store.listEvents();
822
+ async listEvents(options = {}) {
823
+ if (Object.keys(options).length === 0)
824
+ return this.store.listEvents();
825
+ return queryClientEvents(await this.store.listEvents(), options);
826
+ }
827
+ async listEventsPage(options = {}) {
828
+ if (this.store.listEventsPage)
829
+ return this.store.listEventsPage(options);
830
+ const events = queryClientEvents(await this.store.listEvents(), {
831
+ eventId: options.eventId,
832
+ source: options.source,
833
+ type: options.type
834
+ });
835
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
836
+ const limit = normalizeEventPageLimit(options.limit);
837
+ const pageEvents = events.slice(offset, offset + limit);
838
+ const nextOffset = offset + pageEvents.length;
839
+ const hasMore = nextOffset < events.length;
840
+ return {
841
+ events: pageEvents,
842
+ cursor: options.cursor,
843
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
844
+ hasMore
845
+ };
515
846
  }
516
847
  async listDeliveries() {
517
848
  return this.store.listDeliveries();
@@ -579,22 +910,37 @@ class EventsClient {
579
910
  return result;
580
911
  }
581
912
  async replay(options = {}) {
582
- const events = (await this.store.listEvents()).filter((event) => {
583
- if (options.eventId && event.id !== options.eventId)
584
- return false;
585
- if (options.source && event.source !== options.source)
586
- return false;
587
- if (options.type && event.type !== options.type)
588
- return false;
589
- return true;
590
- });
913
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
591
914
  if (options.dryRun)
592
- return { events, deliveries: [] };
915
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
593
916
  const deliveries = [];
594
- for (const event of events) {
917
+ for (const event of page.events) {
595
918
  deliveries.push(...await this.deliver(event));
596
919
  }
597
- return { events, deliveries };
920
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
921
+ }
922
+ async appendEvent(event, options) {
923
+ if (this.store.appendEventOnce) {
924
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
925
+ }
926
+ if (options.dedupe) {
927
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
928
+ if (existing) {
929
+ return {
930
+ event: existing,
931
+ stored: false,
932
+ deduped: true,
933
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
934
+ };
935
+ }
936
+ }
937
+ const stored = await this.store.appendEvent(event);
938
+ return {
939
+ event: stored,
940
+ stored: true,
941
+ deduped: false,
942
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
943
+ };
598
944
  }
599
945
  async applyRedaction(event, channel) {
600
946
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -671,6 +1017,20 @@ function setPath(input, path, replacement) {
671
1017
  if (last && last in cursor)
672
1018
  cursor[last] = replacement;
673
1019
  }
1020
+ function queryClientEvents(events, options) {
1021
+ let rows = events;
1022
+ if (options.eventId)
1023
+ rows = rows.filter((event) => event.id === options.eventId);
1024
+ if (options.source)
1025
+ rows = rows.filter((event) => event.source === options.source);
1026
+ if (options.type)
1027
+ rows = rows.filter((event) => event.type === options.type);
1028
+ if (options.cursor)
1029
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
1030
+ if (options.limit !== undefined)
1031
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
1032
+ return rows;
1033
+ }
674
1034
  function normalizeTime(value) {
675
1035
  if (!value)
676
1036
  return new Date().toISOString();
@@ -686,28 +1046,48 @@ function normalizeRetryPolicy(policy) {
686
1046
  export {
687
1047
  verifyWebhookSignature,
688
1048
  verifyPayloadSignature,
1049
+ validateRolloutData,
1050
+ validateReleasePublishedData,
1051
+ validateFeedbackTriagedData,
1052
+ validateFeedbackCreatedData,
1053
+ validateAppInstalledData,
1054
+ validateAnnouncementSentData,
689
1055
  signPayload,
690
1056
  sanitizeChannelsForOutput,
691
1057
  sanitizeChannelForOutput,
1058
+ registerDistributionEventTypes,
692
1059
  redactSensitiveKeys,
693
1060
  redactPaths,
1061
+ normalizeEventPageLimit,
694
1062
  matchString,
1063
+ localJsonRuntime,
695
1064
  isTimestampWithinTolerance,
696
1065
  getEventsStatus,
697
1066
  getEventsDataDir,
698
1067
  getActiveEventsDirEnv,
699
1068
  eventMatchesFilter,
1069
+ encodeLocalJsonEventCursor,
700
1070
  dispatchWebhook,
701
1071
  dispatchCommand,
702
1072
  dispatchChannel,
1073
+ defaultEventTypeCatalog,
1074
+ decodeLocalJsonEventCursor,
703
1075
  createEvent,
1076
+ createDistributionEventDefinitions,
704
1077
  createDeliveryResult,
705
1078
  channelMatchesEvent,
706
1079
  buildWebhookRequest,
707
1080
  buildSignatureBase,
1081
+ MAX_EVENT_PAGE_LIMIT,
1082
+ LOCAL_JSON_EVENT_CURSOR_PREFIX,
708
1083
  JsonEventsStore,
709
1084
  HASNA_EVENTS_HOME_ENV,
710
1085
  HASNA_EVENTS_DIR_ENV,
711
1086
  EventsClient,
712
- DEFAULT_SIGNATURE_TOLERANCE_MS
1087
+ EventValidationError,
1088
+ EventTypeCatalog,
1089
+ DISTRIBUTION_EVENT_TYPES,
1090
+ DISTRIBUTION_EVENT_CONTRACT_SCHEMAS,
1091
+ DEFAULT_SIGNATURE_TOLERANCE_MS,
1092
+ DEFAULT_EVENT_PAGE_LIMIT
713
1093
  };
package/dist/storage.d.ts CHANGED
@@ -1,17 +1,23 @@
1
- import type { ChannelConfig, DeliveryResult, EventEnvelope, EventsStatus, StoredEventsData } from "./types.js";
1
+ import type { ChannelConfig, DeliveryResult, EventAppendOptions, EventAppendResult, EventEnvelope, EventPage, EventPageOptions, EventsStatus, EventsStoreRuntime, StoredEventsData } from "./types.js";
2
2
  export declare const HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
3
3
  export declare const HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
4
+ export declare const LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
5
+ export declare const DEFAULT_EVENT_PAGE_LIMIT = 100;
6
+ export declare const MAX_EVENT_PAGE_LIMIT = 1000;
4
7
  export declare function getEventsDataDir(override?: string): string;
5
8
  export declare function getActiveEventsDirEnv(): EventsStatus["env"]["active"];
6
9
  export interface EventsStore {
7
10
  dataDir: string;
11
+ runtime?: EventsStoreRuntime;
8
12
  init(): Promise<void>;
9
13
  addChannel(channel: ChannelConfig): Promise<ChannelConfig>;
10
14
  listChannels(): Promise<ChannelConfig[]>;
11
15
  getChannel(id: string): Promise<ChannelConfig | undefined>;
12
16
  removeChannel(id: string): Promise<boolean>;
13
17
  appendEvent(event: EventEnvelope): Promise<EventEnvelope>;
14
- listEvents(): Promise<EventEnvelope[]>;
18
+ appendEventOnce?(event: EventEnvelope, options?: EventAppendOptions): Promise<EventAppendResult>;
19
+ listEvents(options?: EventPageOptions): Promise<EventEnvelope[]>;
20
+ listEventsPage?(options?: EventPageOptions): Promise<EventPage>;
15
21
  findEventByIdentity(identity: {
16
22
  id?: string;
17
23
  dedupeKey?: string;
@@ -21,6 +27,7 @@ export interface EventsStore {
21
27
  }
22
28
  export declare class JsonEventsStore implements EventsStore {
23
29
  dataDir: string;
30
+ runtime: EventsStoreRuntime;
24
31
  private channelsPath;
25
32
  private eventsPath;
26
33
  private deliveriesPath;
@@ -31,7 +38,9 @@ export declare class JsonEventsStore implements EventsStore {
31
38
  getChannel(id: string): Promise<ChannelConfig | undefined>;
32
39
  removeChannel(id: string): Promise<boolean>;
33
40
  appendEvent(event: EventEnvelope): Promise<EventEnvelope>;
34
- listEvents(): Promise<EventEnvelope[]>;
41
+ appendEventOnce(event: EventEnvelope, options?: EventAppendOptions): Promise<EventAppendResult>;
42
+ listEvents(options?: EventPageOptions): Promise<EventEnvelope[]>;
43
+ listEventsPage(options?: EventPageOptions): Promise<EventPage>;
35
44
  findEventByIdentity(identity: {
36
45
  id?: string;
37
46
  dedupeKey?: string;
@@ -43,4 +52,8 @@ export declare class JsonEventsStore implements EventsStore {
43
52
  private readJson;
44
53
  private writeJson;
45
54
  }
55
+ export declare function localJsonRuntime(dataDir?: string): EventsStoreRuntime;
56
+ export declare function encodeLocalJsonEventCursor(offset: number, options?: EventPageOptions): string;
57
+ export declare function decodeLocalJsonEventCursor(cursor: string | undefined, options?: EventPageOptions): number;
58
+ export declare function normalizeEventPageLimit(limit: number | undefined): number;
46
59
  export declare function getEventsStatus(dataDir?: string): Promise<EventsStatus>;