Skip to content

Commit fb43775

Browse files
Makisuoclaude
andauthored
Effect v4 (#308)
* init * stuff * stuff * fix * fix * fix * fix schema stufff * fix more stuff * xd * fix * fix * fix errors * fix stuff * fix * fix * fix * fix * fux * fix * fix * fix * stuff * fix * fix patterns * format * bette rlogin stuff * works * fix * fix * fix * fix electrc clauses * fix * feat simlify model stuff * fix: replace Promise.withResolvers with manual construction for CI compat Plus pending Effect v4 migration changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fc83aff commit fb43775

678 files changed

Lines changed: 17460 additions & 21334 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.context/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ This directory contains git subtrees of library documentation and examples for r
77
The following repositories are included as git subtrees:
88

99
- **Effect** (`.context/effect/`)
10-
- Repository: https://github.com/Effect-TS/effect
10+
- Repository: https://github.com/Effect-TS/effect-smol
1111
- Branch: main
12-
- Purpose: Effect-TS functional programming library documentation and examples
12+
- Purpose: Effect v4 (effect-smol) functional programming library documentation and examples
1313

1414
- **Effect Atom** (`.context/effect-atom/`)
1515
- Repository: https://github.com/tim-smart/effect-atom
@@ -39,7 +39,7 @@ git subtree pull --prefix=.context/tanstack-db tanstack-db-subtree main --squash
3939
Note: The git remotes should already be configured. If not, add them first:
4040

4141
```bash
42-
git remote add effect-subtree https://github.com/Effect-TS/effect
42+
git remote add effect-subtree https://github.com/Effect-TS/effect-smol
4343
git remote add effect-atom-subtree https://github.com/tim-smart/effect-atom
4444
git remote add tanstack-db-subtree https://github.com/TanStack/db
4545
```

CLAUDE.md

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -208,25 +208,28 @@ Without both changes, Electric sync requests for the new table will be rejected
208208

209209
## Effect-TS Best Practices
210210

211-
> **Skill Available**: Run `/effect-best-practices` for comprehensive Effect-TS patterns. The skill auto-activates when writing Effect.Service, Schema.TaggedError, Layer composition, or effect-atom code.
211+
> **Skill Available**: Run `/effect-best-practices` for comprehensive Effect-TS patterns. The skill auto-activates when writing ServiceMap.Service, Schema.TaggedError, Layer composition, or effect-atom code.
212212
213-
### Always Use `Effect.Service` Instead of `Context.Tag`
213+
### Always Use `ServiceMap.Service` Instead of `Context.Tag`
214214

215-
**ALWAYS** prefer `Effect.Service` over `Context.Tag` for defining services. Effect.Service provides built-in `Default` layer, automatic accessors, and proper dependency declaration.
215+
**ALWAYS** prefer `ServiceMap.Service` (from `effect`) over `Context.Tag` for defining services. `ServiceMap.Service` with a `make` option stores the constructor effect on the class. You must define the layer explicitly using `Layer.effect`.
216216

217217
```typescript
218-
// ✅ CORRECT - Use Effect.Service
219-
export class MyService extends Effect.Service<MyService>()("MyService", {
220-
accessors: true,
221-
effect: Effect.gen(function* () {
218+
// ✅ CORRECT - Use ServiceMap.Service with make and explicit layer
219+
import { ServiceMap, Effect, Layer } from "effect"
220+
221+
export class MyService extends ServiceMap.Service<MyService>()("MyService", {
222+
make: Effect.gen(function* () {
222223
// ... implementation
223224
return {
224225
/* methods */
225226
}
226227
}),
227-
}) {}
228+
}) {
229+
static readonly layer = Layer.effect(this, this.make)
230+
}
228231

229-
// Usage: MyService.Default, yield* MyService
232+
// Usage: MyService.layer, yield* MyService
230233
```
231234

232235
```typescript
@@ -237,7 +240,7 @@ export class MyService extends Context.Tag("MyService")<
237240
/* shape */
238241
}
239242
>() {
240-
static Default = Layer.effect(
243+
static layer = Layer.effect(
241244
this,
242245
Effect.gen(function* () {
243246
/* ... */
@@ -246,50 +249,58 @@ export class MyService extends Context.Tag("MyService")<
246249
}
247250
```
248251

252+
```typescript
253+
// ❌ WRONG - Don't use v3 Effect.Service pattern
254+
export class MyService extends Effect.Service<MyService>()("MyService", {
255+
accessors: true,
256+
effect: Effect.gen(function* () {
257+
/* ... */
258+
}),
259+
}) {}
260+
```
261+
249262
**When Context.Tag is acceptable:**
250263

251264
- Infrastructure layers with runtime injection (e.g., Cloudflare KV namespace, worker bindings)
252265
- Factory patterns where the resource is provided externally at runtime
253266

254-
### Use `dependencies` Array in Effect.Service
267+
### Wire Dependencies with `Layer.provide`
255268

256-
**ALWAYS** declare service dependencies in the `dependencies` array when using `Effect.Service`. This ensures proper layer composition and avoids "leaked dependencies" that require manual `Layer.provide` calls at the usage site.
269+
Wire service dependencies using `Layer.provide` on the layer. The v3 `dependencies` array no longer exists.
257270

258271
```typescript
259-
// ✅ CORRECT - Dependencies declared in the service
260-
export class MyService extends Effect.Service<MyService>()("MyService", {
261-
accessors: true,
262-
dependencies: [DatabaseService.Default, CacheService.Default],
263-
effect: Effect.gen(function* () {
272+
// ✅ CORRECT - Dependencies wired via Layer.provide on the layer
273+
export class MyService extends ServiceMap.Service<MyService>()("MyService", {
274+
make: Effect.gen(function* () {
264275
const db = yield* DatabaseService
265276
const cache = yield* CacheService
266277
// ... implementation
278+
return {
279+
/* methods */
280+
}
267281
}),
268-
}) {}
282+
}) {
283+
static readonly layer = Layer.effect(this, this.make).pipe(
284+
Layer.provide(DatabaseService.layer),
285+
Layer.provide(CacheService.layer),
286+
)
287+
}
269288

270-
// Usage is simple - MyService.Default includes all dependencies
271-
const MainLive = Layer.mergeAll(MyService.Default, OtherService.Default)
289+
// Usage is simple - MyService.layer includes all dependencies
290+
const MainLive = Layer.mergeAll(MyService.layer, OtherService.layer)
272291
```
273292

274293
```typescript
275-
// ❌ WRONG - Dependencies leaked to usage site
294+
// ❌ WRONG - v3 dependencies array no longer exists
276295
export class MyService extends Effect.Service<MyService>()("MyService", {
277-
accessors: true,
296+
dependencies: [DatabaseService.Default, CacheService.Default],
278297
effect: Effect.gen(function* () {
279-
const db = yield* DatabaseService
280-
const cache = yield* CacheService
281-
// ... implementation
298+
/* ... */
282299
}),
283300
}) {}
284-
285-
// Now every usage site must manually wire dependencies
286-
const MainLive = Layer.mergeAll(
287-
MyService.Default.pipe(Layer.provide(DatabaseService.Default), Layer.provide(CacheService.Default)),
288-
OtherService.Default,
289-
)
290301
```
291302

292-
**When it's acceptable to omit dependencies:**
303+
**When it's acceptable to omit `Layer.provide`:**
293304

294305
- Infrastructure layers that are globally provided (e.g., Redis, Database) may be intentionally "leaked" to be provided once at the application root
295306
- When a dependency is explicitly meant to be provided by the consumer
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{"timestamp":"2026-03-16T14:12:48.118Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-0000-0000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
2+
{"timestamp":"2026-03-16T14:39:47.254Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
3+
{"timestamp":"2026-03-16T14:39:52.824Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
4+
{"timestamp":"2026-03-16T14:39:57.213Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"4921e0de-a39a-4490-9568-c7f81d1fa558","expected":"synced","actual":"synced"}
5+
{"timestamp":"2026-03-16T14:39:57.997Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"}
6+
{"timestamp":"2026-03-16T14:48:20.247Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"}
7+
{"timestamp":"2026-03-16T14:48:28.056Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"58d534a7-35af-470d-8a28-89220a0988a3","expected":"synced","actual":"synced"}
8+
{"timestamp":"2026-03-16T14:48:30.436Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"}

apps/backend/package.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,9 @@
2424
"rebuild-channel-access": "bun run scripts/rebuild-channel-access.ts"
2525
},
2626
"dependencies": {
27-
"@effect/cluster": "catalog:effect",
28-
"@effect/experimental": "catalog:effect",
2927
"@effect/opentelemetry": "catalog:effect",
30-
"@effect/platform": "catalog:effect",
3128
"@effect/platform-bun": "catalog:effect",
3229
"@effect/platform-node": "catalog:effect",
33-
"@effect/rpc": "catalog:effect",
34-
"@effect/sql": "catalog:effect",
3530
"@effect/sql-pg": "catalog:effect",
3631
"@hazel/auth": "workspace:*",
3732
"@hazel/backend-core": "workspace:*",
@@ -41,14 +36,13 @@
4136
"@hazel/integrations": "workspace:*",
4237
"@hazel/schema": "workspace:*",
4338
"@workos-inc/node": "^7.77.0",
44-
"dfx": "^0.127.0",
39+
"dfx": "^1.0.10",
4540
"drizzle-orm": "^0.45.1",
4641
"effect": "catalog:effect",
4742
"jose": "^6.1.3",
4843
"pg": "^8.16.3"
4944
},
5045
"devDependencies": {
51-
"@effect/language-service": "catalog:effect",
5246
"@testcontainers/postgresql": "^10.18.0",
5347
"@types/bun": "1.3.9",
5448
"drizzle-kit": "^0.31.8",

apps/backend/scripts/create-bot.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import { Database, schema } from "@hazel/db"
1717
import type { BotId, BotInstallationId, OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema"
18-
import { Effect, Logger, LogLevel } from "effect"
18+
import { Effect, References } from "effect"
1919
import { randomUUID } from "crypto"
2020
import { DatabaseLive } from "../src/services/database"
2121

@@ -138,7 +138,7 @@ const createBotScript = Effect.gen(function* () {
138138
// Run the script with proper Effect runtime
139139
const runnable = createBotScript.pipe(
140140
Effect.provide(DatabaseLive),
141-
Effect.provide(Logger.minimumLogLevel(LogLevel.Info)),
141+
Effect.provideService(References.MinimumLogLevel, "Info"),
142142
)
143143

144144
Effect.runPromise(runnable).catch((error) => {

apps/backend/scripts/rebuild-channel-access.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bun
22

33
import { and, Database, eq, isNull, ne, schema } from "@hazel/db"
4-
import { Effect, Layer, Logger, LogLevel } from "effect"
4+
import { Effect, Layer, References } from "effect"
55
import { ChannelAccessSyncService } from "../src/services/channel-access-sync"
66
import { DatabaseLive } from "../src/services/database"
77

@@ -21,7 +21,7 @@ const rebuildChannelAccess = Effect.gen(function* () {
2121

2222
yield* Effect.forEach(
2323
activeNonThreadChannels,
24-
(channel) => ChannelAccessSyncService.syncChannel(channel.id),
24+
(channel) => ChannelAccessSyncService.use((service) => service.syncChannel(channel.id)),
2525
{ concurrency: 20 },
2626
)
2727

@@ -32,9 +32,13 @@ const rebuildChannelAccess = Effect.gen(function* () {
3232
.where(and(isNull(schema.channelsTable.deletedAt), eq(schema.channelsTable.type, "thread"))),
3333
)
3434

35-
yield* Effect.forEach(threadChannels, (channel) => ChannelAccessSyncService.syncChannel(channel.id), {
36-
concurrency: 20,
37-
})
35+
yield* Effect.forEach(
36+
threadChannels,
37+
(channel) => ChannelAccessSyncService.use((service) => service.syncChannel(channel.id)),
38+
{
39+
concurrency: 20,
40+
},
41+
)
3842

3943
const countResult = yield* db.execute(
4044
(client) => client.$client`SELECT COUNT(*)::int AS count FROM channel_access`,
@@ -46,13 +50,13 @@ const rebuildChannelAccess = Effect.gen(function* () {
4650
)
4751
})
4852

49-
const ChannelAccessSyncLive = ChannelAccessSyncService.Default.pipe(Layer.provideMerge(DatabaseLive))
53+
const ChannelAccessSyncLive = ChannelAccessSyncService.layer.pipe(Layer.provideMerge(DatabaseLive))
5054

5155
Effect.runPromise(
5256
rebuildChannelAccess.pipe(
5357
Effect.provide(ChannelAccessSyncLive),
5458
Effect.provide(DatabaseLive),
55-
Effect.provide(Logger.minimumLogLevel(LogLevel.Info)),
59+
Effect.provideService(References.MinimumLogLevel, "Info"),
5660
),
5761
).catch((error) => {
5862
console.error("Failed to rebuild channel_access", error)

apps/backend/scripts/reset-all.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Database } from "@hazel/db"
44
import { WorkOSClient } from "@hazel/backend-core"
5-
import { Effect, Logger, LogLevel } from "effect"
5+
import { Effect, References } from "effect"
66
import { DatabaseLive } from "../src/services/database"
77

88
// Parse command line arguments
@@ -257,8 +257,8 @@ const resetScript = Effect.gen(function* () {
257257
// Run the script with proper Effect runtime
258258
const runnable = resetScript.pipe(
259259
Effect.provide(DatabaseLive),
260-
Effect.provide(WorkOSClient.Default),
261-
Effect.provide(Logger.minimumLogLevel(LogLevel.Info)),
260+
Effect.provide(WorkOSClient.layer),
261+
Effect.provideService(References.MinimumLogLevel, "Info"),
262262
)
263263

264264
Effect.runPromise(runnable).catch((error) => {

apps/backend/scripts/seed-internal-bots.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Database, schema } from "@hazel/db"
1515
import type { BotId, UserId } from "@hazel/schema"
1616
import { createHash, randomUUID } from "crypto"
1717
import { eq } from "drizzle-orm"
18-
import { Effect, Logger, LogLevel } from "effect"
18+
import { Effect, References } from "effect"
1919
import { DatabaseLive } from "../src/services/database"
2020

2121
// Fixed namespace for deterministic UUID generation (DNS namespace UUID)
@@ -217,7 +217,7 @@ const seedInternalBots = Effect.gen(function* () {
217217
// Run the script
218218
const runnable = seedInternalBots.pipe(
219219
Effect.provide(DatabaseLive),
220-
Effect.provide(Logger.minimumLogLevel(LogLevel.Info)),
220+
Effect.provideService(References.MinimumLogLevel, "Info"),
221221
)
222222

223223
Effect.runPromise(runnable).catch((error) => {

apps/backend/scripts/setup.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
WorkOSClient,
1010
WorkOSSync,
1111
} from "@hazel/backend-core"
12-
import { Effect, Layer, Logger, LogLevel } from "effect"
12+
import { Effect, Layer, References } from "effect"
1313
import { DatabaseLive } from "../src/services/database"
1414

1515
// ANSI color codes
@@ -110,20 +110,20 @@ const setupScript = Effect.gen(function* () {
110110
// Run the script
111111
// Build layers with proper dependency wiring
112112
const RepoLive = Layer.mergeAll(
113-
UserRepo.Default,
114-
OrganizationRepo.Default,
115-
OrganizationMemberRepo.Default,
116-
InvitationRepo.Default,
113+
UserRepo.layer,
114+
OrganizationRepo.layer,
115+
OrganizationMemberRepo.layer,
116+
InvitationRepo.layer,
117117
).pipe(Layer.provideMerge(DatabaseLive))
118118

119-
const MainLive = Layer.mergeAll(WorkOSSync.Default, WorkOSClient.Default).pipe(
119+
const MainLive = Layer.mergeAll(WorkOSSync.layer, WorkOSClient.layer).pipe(
120120
Layer.provideMerge(RepoLive),
121121
Layer.provideMerge(DatabaseLive),
122122
)
123123

124124
const runnable = setupScript.pipe(
125125
Effect.provide(MainLive),
126-
Effect.provide(Logger.minimumLogLevel(LogLevel.Info)),
126+
Effect.provideService(References.MinimumLogLevel, "Info"),
127127
)
128128

129129
Effect.runPromise(runnable).catch((error) => {

apps/backend/scripts/test-mock-data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const runTests = Effect.gen(function* () {
168168
yield* Console.error("\n⚠️ Some tests failed")
169169
}
170170
}).pipe(
171-
Effect.catchAll((error) =>
171+
Effect.catch((error) =>
172172
Effect.gen(function* () {
173173
yield* Console.error("\n❌ Test suite error:", error)
174174
yield* Console.error("\n💡 Make sure the backend is running on port 3003")

0 commit comments

Comments
 (0)