@structure-ai/eventsourcing-nisshi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,3 +80,9 @@ The sidecar and broker storage may share one PostgreSQL server, but use separate
80
80
  ## Tests
81
81
 
82
82
  `bun test` skips broker suites unless `NISSHI_URL` is set; PostgreSQL-sidecar scenarios additionally require `DATABASE_URL`. CI installs a pinned Nisshi release, starts PostgreSQL, and runs both sidecar suites. The wire quirks this client encodes (trailing `throttle_time_ms` in Produce, metadata-triggered topic auto-creation, empty-topic high watermark of 1, whole-batch fetch granularity) are pinned by tests in `test/protocol.test.ts`.
83
+
84
+ ## Persistence failures
85
+
86
+ The event store and sidecar adapters expose `PersistenceError` for SQL, broker and envelope failures, retaining the operation and diagnostic cause. Version races remain `ConcurrencyConflict`. See the [shared error contract](../eventsourcing/README.md#persistence-failures).
87
+
88
+ `runPendingRelay` retries only connection failures and broker errors marked retriable. SQL and permanent broker failures stop the relay with a typed error; supervise the returned effect. Produce failures wrap `NisshiProduceError` in `PersistenceError`. Pending rows may still be published later, so a failed append is not proof that no events will appear. See [ADR-0020](../../docs/decisions/0020-typed-persistence-and-cause-boundaries.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@structure-ai/eventsourcing-nisshi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Nisshi (Kafka-API broker) event store with a SQL sidecar for optimistic concurrency, snapshots, checkpoints, and inbox dedupe.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "@effect/sql": "^0.52.1",
29
29
  "@effect/sql-sqlite-bun": "^0.53.0",
30
30
  "@effect/sql-pg": "^0.53.0",
31
- "@structure-ai/domain": "0.2.0",
32
- "@structure-ai/eventsourcing": "0.2.0"
31
+ "@structure-ai/domain": "0.3.0",
32
+ "@structure-ai/eventsourcing": "0.3.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^5.9.2",
package/src/EventStore.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as SqlClient from "@effect/sql/SqlClient";
2
- import type { SqlError } from "@effect/sql/SqlError";
3
- import { ConcurrencyConflict } from "@structure-ai/domain";
2
+ import { SqlError } from "@effect/sql/SqlError";
3
+ import { ConcurrencyConflict, PersistenceError } from "@structure-ai/domain";
4
4
  import {
5
5
  type AppendEvent,
6
6
  type AppendResult,
@@ -10,7 +10,7 @@ import {
10
10
  type StoredEvent,
11
11
  type StoredEventMetadata,
12
12
  } from "@structure-ai/eventsourcing";
13
- import { Effect, Layer, Stream } from "effect";
13
+ import { Cause, Effect, Layer, Stream } from "effect";
14
14
  import { decodeWireEvent, encodeWireEvent, validateWireEvent } from "./envelope.js";
15
15
  import { NisshiClient } from "./protocol/client.js";
16
16
  import { NisshiProduceError } from "./protocol/errors.js";
@@ -109,7 +109,7 @@ const make = (
109
109
  streamName: string,
110
110
  expectedVersion: number,
111
111
  events: ReadonlyArray<AppendEvent>,
112
- ): Effect.Effect<AppendResult, ConcurrencyConflict | SqlError> =>
112
+ ): Effect.Effect<AppendResult, ConcurrencyConflict | SqlError | PersistenceError> =>
113
113
  sql
114
114
  .withTransaction(
115
115
  Effect.gen(function* () {
@@ -141,22 +141,27 @@ const make = (
141
141
  }
142
142
  yield* Effect.forEach(
143
143
  events,
144
- (event, index) => {
145
- const wire = {
146
- type: event.type,
147
- schemaVersion: event.schemaVersion,
148
- version: expectedVersion + index + 1,
149
- payload: event.payload,
150
- metadata: event.metadata,
151
- };
152
- if (validate) {
153
- validateWireEvent(wire, streamName);
154
- }
155
- return sql`
144
+ (event, index) =>
145
+ Effect.gen(function* () {
146
+ const wire = {
147
+ type: event.type,
148
+ schemaVersion: event.schemaVersion,
149
+ version: expectedVersion + index + 1,
150
+ payload: event.payload,
151
+ metadata: event.metadata,
152
+ };
153
+ const encoded = yield* Effect.try({
154
+ try: () => {
155
+ if (validate) validateWireEvent(wire, streamName);
156
+ return JSON.stringify(wire);
157
+ },
158
+ catch: (cause) => new PersistenceError({ operation: "nisshi.encode", cause }),
159
+ });
160
+ return yield* sql`
156
161
  INSERT INTO ${sql(tables.pending)} (stream_name, version, topic, record_value)
157
- VALUES (${streamName}, ${expectedVersion + index + 1}, ${topic}, ${JSON.stringify(wire)})
162
+ VALUES (${streamName}, ${expectedVersion + index + 1}, ${topic}, ${encoded})
158
163
  `;
159
- },
164
+ }),
160
165
  { discard: true },
161
166
  );
162
167
  return {
@@ -166,66 +171,95 @@ const make = (
166
171
  }),
167
172
  )
168
173
  .pipe(
169
- Effect.catchIf(
170
- (error): error is SqlError => isUniqueViolation(error),
171
- () =>
172
- Effect.flatMap(Effect.orDie(currentVersion(streamName)), (actual) =>
173
- Effect.fail(conflict(streamName, expectedVersion, actual)),
174
- ),
175
- ),
174
+ Effect.catchAllCause((cause) => {
175
+ if (
176
+ !Cause.isFailType(cause) ||
177
+ !(cause.error instanceof SqlError) ||
178
+ !isUniqueViolation(cause.error)
179
+ )
180
+ return Effect.failCause(cause);
181
+ return Effect.flatMap(currentVersion(streamName), (actual) =>
182
+ Effect.fail(conflict(streamName, expectedVersion, actual)),
183
+ );
184
+ }),
176
185
  );
177
186
 
178
- /** Best-effort reservation rollback: only succeeds if nobody built on top. Never masks the original failure. */
187
+ /** Conditional rollback; its failure is retained alongside the produce failure. */
179
188
  const rollback = (
180
189
  streamName: string,
181
190
  expectedVersion: number,
182
191
  count: number,
183
- ): Effect.Effect<void> =>
184
- sql
185
- .withTransaction(
186
- Effect.gen(function* () {
187
- yield* sql`
192
+ ): Effect.Effect<void, SqlError> =>
193
+ sql.withTransaction(
194
+ Effect.gen(function* () {
195
+ yield* sql`
188
196
  UPDATE ${sql(tables.streams)}
189
197
  SET last_version = ${expectedVersion}
190
198
  WHERE stream_name = ${streamName} AND last_version = ${expectedVersion + count}
191
199
  `;
192
- yield* sql`
200
+ yield* sql`
193
201
  DELETE FROM ${sql(tables.pending)}
194
202
  WHERE stream_name = ${streamName}
195
203
  AND version > ${expectedVersion} AND version <= ${expectedVersion + count}
196
204
  `;
197
- }),
198
- )
199
- .pipe(
200
- Effect.ignore,
201
- Effect.catchAllDefect(() => Effect.void),
202
- );
205
+ }),
206
+ );
203
207
 
204
208
  const append: EventStoreService["append"] = (streamName, expectedVersion, events) =>
205
209
  Effect.gen(function* () {
210
+ const records = yield* Effect.try({
211
+ try: () =>
212
+ events.map((event, index) => ({
213
+ key: new TextEncoder().encode(streamName),
214
+ value: encodeWireEvent({
215
+ type: event.type,
216
+ schemaVersion: event.schemaVersion,
217
+ version: expectedVersion + index + 1,
218
+ payload: event.payload,
219
+ metadata: event.metadata,
220
+ }),
221
+ })),
222
+ catch: (cause) => new PersistenceError({ operation: "nisshi.encode", cause }),
223
+ });
206
224
  const result = yield* reserve(streamName, expectedVersion, events).pipe(
207
- Effect.catchTag("SqlError", (error) => Effect.die(error)),
225
+ Effect.mapError((cause) =>
226
+ cause instanceof SqlError
227
+ ? new PersistenceError({ operation: "nisshi.reserve", cause })
228
+ : cause,
229
+ ),
208
230
  );
209
231
  if (events.length === 0) {
210
232
  return result;
211
233
  }
212
- const records = events.map((event, index) => ({
213
- key: new TextEncoder().encode(streamName),
214
- value: encodeWireEvent({
215
- type: event.type,
216
- schemaVersion: event.schemaVersion,
217
- version: expectedVersion + index + 1,
218
- payload: event.payload,
219
- metadata: event.metadata,
220
- }),
221
- }));
222
234
  yield* client.produce(topic, records).pipe(
223
- Effect.catchAllCause((cause) =>
224
- Effect.gen(function* () {
225
- yield* rollback(streamName, expectedVersion, events.length);
226
- return yield* Effect.die(new NisshiProduceError({ topic, cause }));
227
- }),
228
- ),
235
+ Effect.catchAllCause((cause) => {
236
+ const original = Cause.map(
237
+ cause,
238
+ (error) =>
239
+ new PersistenceError({
240
+ operation: "nisshi.produce",
241
+ cause: new NisshiProduceError({ topic, cause: error }),
242
+ }),
243
+ );
244
+ if (!Cause.isFailType(cause)) return Effect.failCause(original);
245
+ return rollback(streamName, expectedVersion, events.length).pipe(
246
+ Effect.uninterruptible,
247
+ Effect.matchCauseEffect({
248
+ onSuccess: () => Effect.failCause(original),
249
+ onFailure: (cleanup) =>
250
+ Effect.failCause(
251
+ Cause.sequential(
252
+ original,
253
+ Cause.map(
254
+ cleanup,
255
+ (error) =>
256
+ new PersistenceError({ operation: "nisshi.rollback", cause: error }),
257
+ ),
258
+ ),
259
+ ),
260
+ }),
261
+ );
262
+ }),
229
263
  );
230
264
  // Confirm: events are durable in the topic. A failure here leaves the
231
265
  // pending rows for the relay; a re-produce duplicates are tolerated
@@ -235,8 +269,13 @@ const make = (
235
269
  WHERE stream_name = ${streamName}
236
270
  AND version > ${expectedVersion} AND version <= ${expectedVersion + events.length}
237
271
  `.pipe(
238
- Effect.catchTag("SqlError", (error) =>
239
- Effect.logWarning(`pending confirm failed: ${error.message}`),
272
+ Effect.mapError((cause) => new PersistenceError({ operation: "nisshi.confirm", cause })),
273
+ Effect.catchAllCause((cause) =>
274
+ Cause.isFailType(cause)
275
+ ? Effect.logWarning(
276
+ "Nisshi pending confirmation failed; relay will recover pending rows",
277
+ )
278
+ : Effect.failCause(cause),
240
279
  ),
241
280
  );
242
281
  return result;
@@ -250,12 +289,18 @@ const make = (
250
289
  readonly value: Uint8Array;
251
290
  }>;
252
291
  }
253
- const readTopic = (fromOffset: bigint): Effect.Effect<TopicRecords> =>
292
+ const readTopic = (fromOffset: bigint): Effect.Effect<TopicRecords, PersistenceError> =>
254
293
  Effect.gen(function* () {
255
294
  const out: { offset: bigint; streamName: string; value: Uint8Array }[] = [];
256
295
  let offset = fromOffset;
257
296
  for (;;) {
258
- const page = yield* client.fetch(topic, offset, maxBytes).pipe(Effect.orDie);
297
+ const page = yield* client
298
+ .fetch(topic, offset, maxBytes)
299
+ .pipe(
300
+ Effect.mapError(
301
+ (cause) => new PersistenceError({ operation: "nisshi.fetch", cause }),
302
+ ),
303
+ );
259
304
  if (page.records.length === 0) {
260
305
  return { records: out };
261
306
  }
@@ -278,24 +323,29 @@ const make = (
278
323
  append,
279
324
  read: (streamName, readOptions) =>
280
325
  Stream.unwrap(
281
- Effect.map(readTopic(0n), ({ records }) => {
282
- const fromVersion = readOptions?.fromVersion ?? 1;
283
- const seen = new Set<number>();
284
- const events: StoredEvent[] = [];
285
- for (const record of records) {
286
- if (record.streamName !== streamName) {
287
- continue;
288
- }
289
- const envelope = decodeWireEvent(record.value, streamName);
290
- if (envelope.version < fromVersion || seen.has(envelope.version)) {
291
- continue;
326
+ Effect.flatMap(readTopic(0n), ({ records }) =>
327
+ Effect.gen(function* () {
328
+ const fromVersion = readOptions?.fromVersion ?? 1;
329
+ const seen = new Set<number>();
330
+ const events: StoredEvent[] = [];
331
+ for (const record of records) {
332
+ if (record.streamName !== streamName) {
333
+ continue;
334
+ }
335
+ const envelope = yield* Effect.try({
336
+ try: () => decodeWireEvent(record.value, streamName),
337
+ catch: (cause) => new PersistenceError({ operation: "nisshi.decode", cause }),
338
+ });
339
+ if (envelope.version < fromVersion || seen.has(envelope.version)) {
340
+ continue;
341
+ }
342
+ seen.add(envelope.version);
343
+ events.push(storedEvent(record.offset, streamName, envelope));
292
344
  }
293
- seen.add(envelope.version);
294
- events.push(storedEvent(record.offset, streamName, envelope));
295
- }
296
- events.sort((a, b) => a.version - b.version);
297
- return Stream.fromIterable(events);
298
- }),
345
+ events.sort((a, b) => a.version - b.version);
346
+ return Stream.fromIterable(events);
347
+ }),
348
+ ),
299
349
  ),
300
350
  readAll: (readOptions) =>
301
351
  Stream.unwrap(
@@ -315,7 +365,13 @@ const make = (
315
365
  if (limit !== undefined && events.length >= limit) {
316
366
  break;
317
367
  }
318
- const page = yield* client.fetch(topic, offset, maxBytes).pipe(Effect.orDie);
368
+ const page = yield* client
369
+ .fetch(topic, offset, maxBytes)
370
+ .pipe(
371
+ Effect.mapError(
372
+ (cause) => new PersistenceError({ operation: "nisshi.fetch", cause }),
373
+ ),
374
+ );
319
375
  if (page.records.length === 0) {
320
376
  break;
321
377
  }
@@ -324,7 +380,10 @@ const make = (
324
380
  const event = storedEvent(
325
381
  record.offset,
326
382
  streamName,
327
- decodeWireEvent(record.value, streamName),
383
+ yield* Effect.try({
384
+ try: () => decodeWireEvent(record.value, streamName),
385
+ catch: (cause) => new PersistenceError({ operation: "nisshi.decode", cause }),
386
+ }),
328
387
  );
329
388
  if (keep(event)) {
330
389
  events.push(event);
package/src/Stores.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as SqlClient from "@effect/sql/SqlClient";
2
+ import { PersistenceError } from "@structure-ai/domain";
2
3
  import {
3
4
  CheckpointStore,
4
5
  type CheckpointStoreService,
@@ -29,26 +30,44 @@ export const snapshotStoreLayer = (
29
30
  const sql = yield* SqlClient.SqlClient;
30
31
  const service: SnapshotStoreService = {
31
32
  load: (streamName) =>
32
- Effect.map(
33
+ Effect.flatMap(
33
34
  sql<SnapshotRow>`
34
35
  SELECT state, version FROM ${sql(tables.snapshots)} WHERE stream_name = ${streamName}
35
- `.pipe(Effect.orDie),
36
- (rows): Option.Option<{ state: unknown; version: number }> => {
37
- const row = rows[0];
38
- return row === undefined
39
- ? Option.none()
40
- : Option.some({
41
- state: JSON.parse(row.state) as unknown,
42
- version: Number(row.version),
43
- });
44
- },
36
+ `.pipe(
37
+ Effect.mapError(
38
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
39
+ ),
40
+ ),
41
+ (rows) =>
42
+ Effect.try({
43
+ try: (): Option.Option<{ state: unknown; version: number }> => {
44
+ const row = rows[0];
45
+ return row === undefined
46
+ ? Option.none()
47
+ : Option.some({
48
+ state: JSON.parse(row.state) as unknown,
49
+ version: Number(row.version),
50
+ });
51
+ },
52
+ catch: (cause) => new PersistenceError({ operation: "snapshot.decode", cause }),
53
+ }),
45
54
  ),
46
55
  save: (streamName, snapshot) =>
47
- Effect.asVoid(sql`
56
+ Effect.gen(function* () {
57
+ const encoded = yield* Effect.try({
58
+ try: () => JSON.stringify(snapshot.state ?? null),
59
+ catch: (cause) => new PersistenceError({ operation: "snapshot.encode", cause }),
60
+ });
61
+ return yield* Effect.asVoid(sql`
48
62
  INSERT INTO ${sql(tables.snapshots)} (stream_name, state, version)
49
- VALUES (${streamName}, ${JSON.stringify(snapshot.state ?? null)}, ${snapshot.version})
63
+ VALUES (${streamName}, ${encoded}, ${snapshot.version})
50
64
  ON CONFLICT (stream_name) DO UPDATE SET state = excluded.state, version = excluded.version
51
- `).pipe(Effect.orDie),
65
+ `).pipe(
66
+ Effect.mapError(
67
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
68
+ ),
69
+ );
70
+ }),
52
71
  };
53
72
  return SnapshotStore.of(service);
54
73
  }),
@@ -69,7 +88,11 @@ export const checkpointStoreLayer = (
69
88
  Effect.map(
70
89
  sql<{ readonly position: number | bigint | string | null }>`
71
90
  SELECT position FROM ${sql(tables.checkpoints)} WHERE name = ${name}
72
- `.pipe(Effect.orDie),
91
+ `.pipe(
92
+ Effect.mapError(
93
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
94
+ ),
95
+ ),
73
96
  (rows) => toBigInt(rows[0]?.position),
74
97
  ),
75
98
  save: (name, position) =>
@@ -77,7 +100,11 @@ export const checkpointStoreLayer = (
77
100
  INSERT INTO ${sql(tables.checkpoints)} (name, position)
78
101
  VALUES (${name}, ${position})
79
102
  ON CONFLICT (name) DO UPDATE SET position = excluded.position
80
- `).pipe(Effect.orDie),
103
+ `).pipe(
104
+ Effect.mapError(
105
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
106
+ ),
107
+ ),
81
108
  };
82
109
  return CheckpointStore.of(service);
83
110
  }),
@@ -99,7 +126,11 @@ export const inboxLayer = (
99
126
  sql<{ readonly one: number }>`
100
127
  SELECT 1 AS one FROM ${sql(tables.inbox)}
101
128
  WHERE consumer_id = ${consumerId} AND message_id = ${messageId}
102
- `.pipe(Effect.orDie),
129
+ `.pipe(
130
+ Effect.mapError(
131
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
132
+ ),
133
+ ),
103
134
  (rows) => rows.length > 0,
104
135
  ),
105
136
  markProcessed: (consumerId, messageId) =>
@@ -107,7 +138,11 @@ export const inboxLayer = (
107
138
  INSERT INTO ${sql(tables.inbox)} (consumer_id, message_id)
108
139
  VALUES (${consumerId}, ${messageId})
109
140
  ON CONFLICT DO NOTHING
110
- `).pipe(Effect.orDie),
141
+ `).pipe(
142
+ Effect.mapError(
143
+ (cause) => new PersistenceError({ operation: "nisshi-sidecar", cause }),
144
+ ),
145
+ ),
111
146
  };
112
147
  return Inbox.of(service);
113
148
  }),
package/src/layer.ts CHANGED
@@ -2,6 +2,7 @@ import type * as SqlClient from "@effect/sql/SqlClient";
2
2
  import type { SqlError } from "@effect/sql/SqlError";
3
3
  import { PgClient } from "@effect/sql-pg";
4
4
  import { SqliteClient } from "@effect/sql-sqlite-bun";
5
+ import { PersistenceError } from "@structure-ai/domain";
5
6
  import type {
6
7
  CheckpointStore,
7
8
  EventStore,
@@ -82,7 +83,7 @@ export const layerPg = (
82
83
  options: NisshiPgConfig,
83
84
  ): Layer.Layer<
84
85
  StoreServices | PgClient.PgClient | SqlClient.SqlClient | NisshiClient,
85
- SqlError | NisshiConnectionError
86
+ SqlError | NisshiConnectionError | PersistenceError
86
87
  > => {
87
88
  const client = nisshiClientLayer({
88
89
  brokerUrl: options.brokerUrl,
@@ -101,7 +102,13 @@ export const layerPg = (
101
102
  ? client
102
103
  : Layer.effectDiscard(
103
104
  Effect.flatMap(NisshiClient, (c) =>
104
- c.ensureTopic(options.topic ?? "events").pipe(Effect.orDie),
105
+ c
106
+ .ensureTopic(options.topic ?? "events")
107
+ .pipe(
108
+ Effect.mapError(
109
+ (cause) => new PersistenceError({ operation: "nisshi.ensure-topic", cause }),
110
+ ),
111
+ ),
105
112
  ),
106
113
  ).pipe(Layer.provideMerge(client));
107
114
  return storesLayer(options).pipe(Layer.provideMerge(migrated), Layer.provideMerge(ensured));
@@ -116,7 +123,7 @@ export const layer = (
116
123
  options: NisshiAdaptersConfig,
117
124
  ): Layer.Layer<
118
125
  StoreServices | SqliteClient.SqliteClient | SqlClient.SqlClient | NisshiClient,
119
- ConfigError | SqlError | NisshiConnectionError
126
+ ConfigError | SqlError | NisshiConnectionError | PersistenceError
120
127
  > => {
121
128
  const client = nisshiClientLayer({
122
129
  brokerUrl: options.brokerUrl,
@@ -130,7 +137,13 @@ export const layer = (
130
137
  ? client
131
138
  : Layer.effectDiscard(
132
139
  Effect.flatMap(NisshiClient, (c) =>
133
- c.ensureTopic(options.topic ?? "events").pipe(Effect.orDie),
140
+ c
141
+ .ensureTopic(options.topic ?? "events")
142
+ .pipe(
143
+ Effect.mapError(
144
+ (cause) => new PersistenceError({ operation: "nisshi.ensure-topic", cause }),
145
+ ),
146
+ ),
134
147
  ),
135
148
  ).pipe(Layer.provideMerge(client));
136
149
  return storesLayer(options).pipe(Layer.provideMerge(migrated), Layer.provideMerge(ensured));
package/src/relay.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as SqlClient from "@effect/sql/SqlClient";
2
2
  import type { SqlError } from "@effect/sql/SqlError";
3
- import { Effect } from "effect";
3
+ import { Cause, Effect } from "effect";
4
4
  import { NisshiClient } from "./protocol/client.js";
5
5
  import type { NisshiApiError, NisshiConnectionError } from "./protocol/errors.js";
6
6
  import { type SidecarOptions, sidecarTables } from "./sidecar.js";
@@ -64,13 +64,24 @@ export const drainPending = (
64
64
  /** Runs `drainPending` forever, sleeping `pollInterval` between passes. */
65
65
  export const runPendingRelay = (
66
66
  options?: RelayOptions & { readonly pollInterval?: number },
67
- ): Effect.Effect<never, never, SqlClient.SqlClient | NisshiClient> =>
67
+ ): Effect.Effect<
68
+ never,
69
+ NisshiApiError | NisshiConnectionError | SqlError,
70
+ SqlClient.SqlClient | NisshiClient
71
+ > =>
68
72
  Effect.gen(function* () {
69
73
  const interval = options?.pollInterval ?? 500;
70
74
  for (;;) {
71
75
  yield* drainPending(options).pipe(
72
- Effect.ignore,
73
- Effect.catchAllDefect(() => Effect.void),
76
+ Effect.catchAllCause((cause) => {
77
+ if (
78
+ Cause.isFailType(cause) &&
79
+ (cause.error._tag === "NisshiConnectionError" ||
80
+ (cause.error._tag === "NisshiApiError" && cause.error.retriable))
81
+ )
82
+ return Effect.logWarning("Nisshi relay will retry a transient broker failure");
83
+ return Effect.failCause(cause);
84
+ }),
74
85
  );
75
86
  yield* Effect.sleep(interval);
76
87
  }