effect-cf 0.35.0 → 0.37.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,11 @@
27
27
  "import": "./dist/HyperdrivePg.mjs",
28
28
  "default": "./dist/HyperdrivePg.mjs"
29
29
  },
30
+ "./mcp": {
31
+ "types": "./dist/Mcp.d.mts",
32
+ "import": "./dist/Mcp.mjs",
33
+ "default": "./dist/Mcp.mjs"
34
+ },
30
35
  "./computer-artifacts": {
31
36
  "types": "./dist/ComputerArtifacts.d.mts",
32
37
  "import": "./dist/ComputerArtifacts.mjs",
@@ -75,6 +80,8 @@
75
80
  "@effect/sql-pg": "catalog:",
76
81
  "@effect/sql-sqlite-do": "catalog:",
77
82
  "@effect/vitest": "catalog:",
83
+ "@modelcontextprotocol/client": "^2.0.0",
84
+ "@modelcontextprotocol/server": "^2.0.0",
78
85
  "@platformatic/vfs": "^0.4.0",
79
86
  "effect": "catalog:",
80
87
  "esbuild": "^0.28.1",
@@ -89,6 +96,7 @@
89
96
  "@effect/sql-d1": "^4.0.0-rc.110",
90
97
  "@effect/sql-pg": "^4.0.0-rc.110",
91
98
  "@effect/sql-sqlite-do": "^4.0.0-rc.110",
99
+ "@modelcontextprotocol/server": "^2.0.0",
92
100
  "effect": "^4.0.0-rc.110"
93
101
  },
94
102
  "peerDependenciesMeta": {
@@ -106,6 +114,9 @@
106
114
  },
107
115
  "@effect/sql-pg": {
108
116
  "optional": true
117
+ },
118
+ "@modelcontextprotocol/server": {
119
+ "optional": true
109
120
  }
110
121
  }
111
122
  }
@@ -12,7 +12,7 @@ import {
12
12
  } from "effect";
13
13
 
14
14
  import { DurableObjectState } from "./DurableObjectState";
15
- import type { SqlStorageValue, StorageOperationError } from "./DurableObjectStorage";
15
+ import { type SqlStorageValue, StorageOperationError } from "./DurableObjectStorage";
16
16
  import * as ErrorMessage from "./internal/ErrorMessage";
17
17
 
18
18
  const INIT_TABLE_SQL = `
@@ -177,8 +177,30 @@ export type ProcessDueAlarmsHandler<R = never, E = never> = (
177
177
  event: DurableObjectAlarmEvent,
178
178
  ) => Effect.Effect<void, E, R>;
179
179
 
180
+ /**
181
+ * Alarm mutations owned by one transaction callback. Run them in the callback's
182
+ * fiber; forked work and use after the callback ends fail with StorageOperationError.
183
+ */
184
+ export type AlarmTransaction = Pick<AlarmScheduler, "scheduleAlarm" | "cancelAlarm">;
185
+
180
186
  /** Own `storage.setAlarm()` exclusively: a Durable Object has one platform alarm timestamp. */
181
187
  export type AlarmScheduler = {
188
+ /**
189
+ * Commits local application storage and logical alarms in one native SQLite
190
+ * Durable Object transaction, reconciling the native alarm before commit.
191
+ * Use the supplied mutations, not standalone alarm methods or nested transactions.
192
+ * SqlClient queries must use this same Durable Object's storage.
193
+ *
194
+ * Failure, defects and interruption before commit roll back. A lost reply or
195
+ * interruption after commit does not undo committed state. Keep RPC and other
196
+ * external effects outside; atomically pre-arm a later wake before fallible work.
197
+ * Cloudflare's native retries are bounded; composition does not remove the
198
+ * pre-arm requirement or promise infinite retry liveness.
199
+ */
200
+ readonly transaction: <A, E, R>(
201
+ closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R>,
202
+ ) => Effect.Effect<A, E | StorageOperationError, R>;
203
+
182
204
  readonly cancelAlarm: (
183
205
  input: AlarmRef,
184
206
  ) => Effect.Effect<void, InvalidAlarmRefError | StorageOperationError>;
@@ -523,17 +545,9 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
523
545
  const cancelAlarm = Effect.fn("DurableObjectAlarm.cancelAlarm")(function* (input: AlarmRef) {
524
546
  const ref = yield* decodeAlarmRef(input);
525
547
 
526
- yield* state.storage.transaction(() =>
527
- Effect.gen(function* () {
528
- yield* ensureTable(state);
529
- yield* state.storage.sql
530
- .exec(
531
- `DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`,
532
- getScheduledEventId(ref),
533
- )
534
- .pipe(Effect.asVoid);
535
- yield* reconcileAlarm();
536
- }),
548
+ yield* state.storage.sql.exec(
549
+ `DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`,
550
+ getScheduledEventId(ref),
537
551
  );
538
552
  });
539
553
 
@@ -545,27 +559,62 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
545
559
  const payload = yield* encodeStoredPayload(input.payload);
546
560
  const runAt = DateTime.toEpochMillis(input.runAt);
547
561
 
548
- yield* state.storage.transaction(() =>
549
- Effect.gen(function* () {
550
- yield* ensureTable(state);
551
- yield* state.storage.sql
552
- .exec(
553
- `INSERT OR REPLACE INTO effect_cf_scheduled_alarms
562
+ yield* state.storage.sql.exec(
563
+ `INSERT OR REPLACE INTO effect_cf_scheduled_alarms
554
564
  (storage_id, alarm_id, tag, run_at, repeat_every_ms, payload)
555
565
  VALUES (?, ?, ?, ?, ?, ?)`,
556
- getScheduledEventId(ref),
557
- ref.id,
558
- ref.tag,
559
- runAt,
560
- repeatEveryMillis,
561
- payload,
562
- )
563
- .pipe(Effect.asVoid);
564
- yield* reconcileAlarm();
565
- }),
566
+ getScheduledEventId(ref),
567
+ ref.id,
568
+ ref.tag,
569
+ runAt,
570
+ repeatEveryMillis,
571
+ payload,
566
572
  );
567
573
  });
568
574
 
575
+ const transaction: AlarmScheduler["transaction"] = (closure) =>
576
+ state.storage.transaction(() =>
577
+ Effect.withFiber((owner) => {
578
+ let active = true;
579
+
580
+ // A callback-owned handle cannot escape via a returned Effect or a
581
+ // detached fiber and write after reconciliation or rollback.
582
+ const requireActive = <A, E>(effect: Effect.Effect<A, E>) =>
583
+ Effect.withFiber<A, E | StorageOperationError>((fiber) =>
584
+ active && fiber === owner
585
+ ? effect
586
+ : Effect.fail(
587
+ new StorageOperationError({
588
+ operation: "alarm.transaction",
589
+ cause: new Error(
590
+ "Alarm mutations require their active transaction callback",
591
+ ),
592
+ }),
593
+ ),
594
+ );
595
+
596
+ return Effect.gen(function* () {
597
+ yield* ensureTable(state);
598
+ const result = yield* Effect.suspend(() =>
599
+ closure({
600
+ cancelAlarm: (input) => requireActive(cancelAlarm(input)),
601
+ scheduleAlarm: (input) => requireActive(scheduleAlarm(input)),
602
+ }),
603
+ ).pipe(
604
+ Effect.ensuring(
605
+ Effect.sync(() => {
606
+ active = false;
607
+ }),
608
+ ),
609
+ );
610
+
611
+ yield* reconcileAlarm();
612
+
613
+ return result;
614
+ });
615
+ }),
616
+ );
617
+
569
618
  const processDueAlarms = Effect.fn("DurableObjectAlarm.processDueAlarms")(function* <
570
619
  R,
571
620
  E,
@@ -669,9 +718,10 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
669
718
  });
670
719
 
671
720
  return DurableObjectAlarm.of({
672
- cancelAlarm,
721
+ cancelAlarm: (input) => transaction((alarms) => alarms.cancelAlarm(input)),
673
722
  processDueAlarms,
674
- scheduleAlarm,
723
+ scheduleAlarm: (input) => transaction((alarms) => alarms.scheduleAlarm(input)),
724
+ transaction,
675
725
  });
676
726
  }).pipe(Effect.withSpan("DurableObjectAlarm.layer")),
677
727
  );