diff --git a/.changeset/effect-3-22-update.md b/.changeset/effect-3-22-update.md new file mode 100644 index 00000000..dcc21c03 --- /dev/null +++ b/.changeset/effect-3-22-update.md @@ -0,0 +1,13 @@ +--- +'@codeforbreakfast/eventsourcing-store-postgres': patch +'@codeforbreakfast/eventsourcing-transport-websocket': patch +'@codeforbreakfast/eventsourcing-testing-contracts': patch +--- + +Build and test against Effect 3.22. + +`eventsourcing-store-postgres` now depends on `@effect/sql` 0.52, `@effect/sql-pg` 0.53 and `@effect/experimental` 0.61. `eventsourcing-transport-websocket` now depends on `@effect/platform` 0.97. + +`eventsourcing-store-postgres` fixes lost events on new subscriptions. `subscribe` and `subscribeAll` used to return before Postgres `LISTEN` was active, so an event committed in that gap never reached the subscriber. They now wait until `LISTEN` is active. To detect that, the store sends probe notifications to the channel it is listening on, repeating every 10ms until one comes back. A probe payload starts with `eventstore_listen_probe:`, and every listener on that channel receives it. If your own code listens on the `eventstore_events_*` channels, ignore payloads with that prefix. Older versions of this package log a parse error for each probe they receive. + +In `eventsourcing-testing-contracts`, the test helpers fail with Effect's tagged errors instead of a plain `Error`. `expectError` fails with `NoSuchElementException` when the error does not match the predicate. `waitForConnectionState` and `collectMessages` fail with `TimeoutException` when they time out. The declared error type is still `Error`, so existing code compiles unchanged, and you can now match these failures by `_tag`. diff --git a/bun.lock b/bun.lock index 27e58822..ecd78180 100644 --- a/bun.lock +++ b/bun.lock @@ -11,18 +11,18 @@ "@codeforbreakfast/eslint-effect": "workspace:*", "@commitlint/cli": "20.5.3", "@commitlint/config-conventional": "20.5.3", - "@effect/cluster": "0.55.0", - "@effect/language-service": "0.60.0", - "@effect/platform": "0.93.6", - "@effect/platform-bun": "0.86.0", - "@effect/rpc": "0.72.2", - "@effect/workflow": "0.15.0", + "@effect/cluster": "0.60.2", + "@effect/language-service": "0.87.3", + "@effect/platform": "0.97.2", + "@effect/platform-bun": "0.91.2", + "@effect/rpc": "0.76.2", + "@effect/workflow": "0.19.1", "@eslint-community/eslint-plugin-eslint-comments": "4.8.1", "@types/bun": "1.4.2", "@typescript-eslint/eslint-plugin": "8.70.1", "@typescript-eslint/parser": "8.70.1", "dependency-cruiser": "17.4.3", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "eslint-config-prettier": "10.1.8", "eslint-plugin-compat": "6.2.1", @@ -50,9 +50,9 @@ "@codeforbreakfast/eventsourcing-projections": "workspace:*", "@codeforbreakfast/eventsourcing-store": "workspace:*", "@codeforbreakfast/eventsourcing-store-filesystem": "workspace:*", - "@effect/cli": "0.72.1", - "@effect/platform-bun": "0.86.0", - "effect": "3.19.9", + "@effect/cli": "0.77.2", + "@effect/platform-bun": "0.91.2", + "effect": "3.22.2", }, "devDependencies": { "@types/bun": "latest", @@ -62,7 +62,7 @@ "name": "@codeforbreakfast/bun-test-effect", "version": "0.4.1", "devDependencies": { - "effect": "3.19.9", + "effect": "3.22.2", }, "peerDependencies": { "effect": ">=3.0.0", @@ -73,7 +73,7 @@ "version": "0.8.5", "devDependencies": { "@typescript-eslint/eslint-plugin": "8.70.1", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -93,7 +93,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-store-inmemory": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -111,7 +111,7 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -128,7 +128,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -149,7 +149,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-transport-inmemory": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -168,7 +168,7 @@ "@codeforbreakfast/eventsourcing-store-inmemory": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -185,7 +185,7 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -203,9 +203,9 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", - "@effect/platform-bun": "0.86.0", + "@effect/platform-bun": "0.91.2", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -224,7 +224,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -238,17 +238,17 @@ "version": "0.6.14", "dependencies": { "@codeforbreakfast/eventsourcing-store": "workspace:*", - "@effect/experimental": "0.57.11", - "@effect/sql": "0.48.6", - "@effect/sql-pg": "0.49.7", + "@effect/experimental": "0.61.1", + "@effect/sql": "0.52.1", + "@effect/sql-pg": "0.53.0", "type-fest": "5.10.0", }, "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", - "@effect/platform-bun": "0.86.0", + "@effect/platform-bun": "0.91.2", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -267,7 +267,7 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -283,7 +283,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -302,7 +302,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -315,7 +315,7 @@ "version": "0.5.7", "dependencies": { "@codeforbreakfast/eventsourcing-transport": "workspace:*", - "@effect/platform": "0.93.6", + "@effect/platform": "0.97.2", "type-fest": "5.10.0", }, "devDependencies": { @@ -323,7 +323,7 @@ "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", "bun-types": "1.4.2", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -341,7 +341,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3", }, @@ -467,33 +467,33 @@ "@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.7.0", "", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.4.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw=="], - "@effect/cli": ["@effect/cli@0.72.1", "", { "dependencies": { "ini": "^4.1.3", "toml": "^3.0.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.93.0", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "effect": "^3.19.3" } }, "sha512-HGDMGD23TxFW9tCSX6g+M2u0robikMA0mP0SqeJMj7FWXTdcQ+cQsJE99bxi9iu+5YID7MIrVJMs8TUwXUV2sg=="], + "@effect/cli": ["@effect/cli@0.77.2", "", { "dependencies": { "ini": "^4.1.3", "toml": "^4.3.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.97.2", "@effect/printer": "^0.51.0", "@effect/printer-ansi": "^0.51.0", "effect": "^3.22.2" } }, "sha512-1I6rvAtsyATAFF5nBGeou2qKIkIr3WFEoHNd2+UH8inREblwCLKJb7o3EdbMcG5w3212ZanFVOA2/3KHL22NbQ=="], - "@effect/cluster": ["@effect/cluster@0.55.0", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.93.6", "@effect/rpc": "^0.72.2", "@effect/sql": "^0.48.6", "@effect/workflow": "^0.15.0", "effect": "^3.19.8" } }, "sha512-WeLrGpdtWhr4ap3kcW+G+qYyDqzEa65ypPWsU+sNU2gordENsAkxUQqo8taKr114AOk6WE74q0I7iWABcfGlhQ=="], + "@effect/cluster": ["@effect/cluster@0.60.2", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.97.1", "@effect/rpc": "^0.76.2", "@effect/sql": "^0.52.1", "@effect/workflow": "^0.19.1", "effect": "^3.22.1" } }, "sha512-GEODN5kvFJ0L0XSkANcS8s6lfhjrrA8x/ezYUV/lXerqCHf5xo3ZTPUanGMHu5Byvikng0mZDcyNvke2sGjfSQ=="], - "@effect/experimental": ["@effect/experimental@0.57.11", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.93.6", "effect": "^3.19.9", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-M5uug3Drs/gyTHLfA+XzcIZQGUEV/Jn5yi1POki4oZswhpzNmsVTHl4THpxAordRKwa5lFvTSlsRP684YH7pSw=="], + "@effect/experimental": ["@effect/experimental@0.61.1", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.97.1", "effect": "^3.22.1", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-+P+PgGQeE2fXmsm63YoyENNcQIc7OiCkY4/HLkEdTvq93syfGIpwDfQD8u3xODMHJhZ5ZtRZHH4+UedFO0tLiA=="], - "@effect/language-service": ["@effect/language-service@0.60.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-elJDWHG5Naq3OkilPt9ZRn56JfSA3MhXUIlDx9RWJeScHm96kZ+HkZ3eFBxqROzXwD6Q2DTtFctFwOM0+QLZEA=="], + "@effect/language-service": ["@effect/language-service@0.87.3", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-g1OmRBZF5chbW9QMNoirPo5Bb4zzU2HfP88tR7/VSZEUxHJ1/qVb8AQ0kUh2ggctLH1I715yCwP4gkw56PTjpQ=="], - "@effect/platform": ["@effect/platform@0.93.6", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.8" } }, "sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w=="], + "@effect/platform": ["@effect/platform@0.97.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.10", "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^3.22.2" } }, "sha512-fof9J+pId374/b1WAt0P1H8QmvulzpHTjS6SUHiG20U/sFjkBUS5vGMy1/XfdTAVKX1IY+Z87dFhi/RFDfgBPQ=="], - "@effect/platform-bun": ["@effect/platform-bun@0.86.0", "", { "dependencies": { "@effect/platform-node-shared": "^0.56.0", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.55.0", "@effect/platform": "^0.93.6", "@effect/rpc": "^0.72.2", "@effect/sql": "^0.48.6", "effect": "^3.19.8" } }, "sha512-wg6eyzIxHuOd87rHjR18T7DjOtQ0dz8/cQD/pvZo8xFBSZ0AoZXqkHOKuU34DIRA4tp+BdBP6wjDDQYNQK/agQ=="], + "@effect/platform-bun": ["@effect/platform-bun@0.91.2", "", { "dependencies": { "@effect/platform-node-shared": "^0.61.1", "multipasta": "^0.2.8" }, "peerDependencies": { "@effect/cluster": "^0.60.2", "@effect/platform": "^0.97.1", "@effect/rpc": "^0.76.2", "@effect/sql": "^0.52.1", "effect": "^3.22.1" } }, "sha512-lISuYdx54uX6BIu0AIwvIqG6mbFkT0UlqGWZm7Jn2V6Ni3nCumnaBfMxYrdGWJzkoGIi1xEkvmRRk5Ff7r6NEA=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@0.56.0", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.55.0", "@effect/platform": "^0.93.6", "@effect/rpc": "^0.72.2", "@effect/sql": "^0.48.6", "effect": "^3.19.8" } }, "sha512-0RawLcUCLHVGs4ch1nY26P4xM+U6R03ZR02MgNHMsL0slh8YYlal5PnwD/852rJ59O9prQX3Kq8zs+cGVoLAJw=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@0.61.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.8", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.60.1", "@effect/platform": "^0.97.1", "@effect/rpc": "^0.76.1", "@effect/sql": "^0.52.1", "effect": "^3.22.1" } }, "sha512-MJAJ1Nc43pYJb5ulFtdNID8klNyRLcbQ0zZ7XYRcBAlT/DrCyN4yAD89AzosmUyfOU+LMHdc8LTMt4rKNEMFaA=="], "@effect/printer": ["@effect/printer@0.40.12", "", { "peerDependencies": { "@effect/typeclass": "^0.31.12", "effect": "^3.12.12" } }, "sha512-B5y/9pzdvN4ntlg74jhFE5nhgdTALdPDfaJ/e4JgIjzHAfcT3nKVyZj1CPoTFkNSFOPID5YBMPSzAG14vwSm5w=="], "@effect/printer-ansi": ["@effect/printer-ansi@0.40.12", "", { "dependencies": { "@effect/printer": "^0.40.12" }, "peerDependencies": { "@effect/typeclass": "^0.31.12", "effect": "^3.12.12" } }, "sha512-3dtgF0j6HzUQZlnytVqAldBX3mHUcXFrPtYfbo+E/2m0cN0ggfWOXOUYxB/rCWSeYR0bdGxFReEsxNuydzCQHQ=="], - "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="], + "@effect/rpc": ["@effect/rpc@0.76.2", "", { "dependencies": { "msgpackr": "^1.11.10" }, "peerDependencies": { "@effect/platform": "^0.97.1", "effect": "^3.22.1" } }, "sha512-x/4jvjufr1m3QWAace7G5Xh09DDcaeU7ijvS0Qumkan/hM89n19JClYnlV3IW93RACZInbgdyOVA+4kOQJ+JQQ=="], - "@effect/sql": ["@effect/sql@0.48.6", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.57.9", "@effect/platform": "^0.93.6", "effect": "^3.19.8" } }, "sha512-OBIG/DYFxYTA9EXXhqi6sAcX0YLz8Huu8L+wj3a0aOSRPpHm9HkL9a5lacRPPvrVl31rKcEteGa/lO6n26gIFg=="], + "@effect/sql": ["@effect/sql@0.52.1", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.61.1", "@effect/platform": "^0.97.1", "effect": "^3.22.1" } }, "sha512-dUVPjlUgpga6sDa/MtYipHAqzHx2St41hxI00nXTBt4IBBJaf2lTGaOvlEoBg/q6PHVAgSoKnxe19pWUvabC9g=="], - "@effect/sql-pg": ["@effect/sql-pg@0.49.7", "", { "dependencies": { "pg": "^8.16.3", "pg-connection-string": "2.9.1", "pg-cursor": "^2.15.3", "pg-pool": "^3.10.1", "pg-types": "^4.1.0" }, "peerDependencies": { "@effect/experimental": "^0.57.5", "@effect/platform": "^0.93.4", "@effect/sql": "^0.48.1", "effect": "^3.19.6" } }, "sha512-7yGHEcSTj3jo7EZxC4cEXUno+/pgbEjjY5WLrx0Nfta07Vu6CTplcSOQ5KA24HwkclBidNwfX7LWghc04kAgUQ=="], + "@effect/sql-pg": ["@effect/sql-pg@0.53.0", "", { "dependencies": { "pg": "^8.16.3", "pg-connection-string": "2.9.1", "pg-cursor": "^2.15.3", "pg-pool": "^3.10.1", "pg-types": "^4.1.0" }, "peerDependencies": { "@effect/experimental": "^0.61.0", "@effect/platform": "^0.97.0", "@effect/sql": "^0.52.0", "effect": "^3.22.0" } }, "sha512-E9LPkP3aB9RNPhAl9R19z6dcmleNyJJGatg0N/zpH3LyIg3dB/4Qhv543rNOGyQlox5ebUADbHUxWk5UzDfSCA=="], "@effect/typeclass": ["@effect/typeclass@0.31.12", "", { "peerDependencies": { "effect": "^3.12.12" } }, "sha512-iyg8EJgZ7PS4hFnkWFtyeaXIH4544tLprmerRYFjgGHMsqxoorTfHd2N01inXd3stUgcGiHoVy1DDAxWKLomkQ=="], - "@effect/workflow": ["@effect/workflow@0.15.0", "", { "peerDependencies": { "@effect/experimental": "^0.57.9", "@effect/platform": "^0.93.6", "@effect/rpc": "^0.72.2", "effect": "^3.19.8" } }, "sha512-0u9aO1Uwo5ZdYJL6jygBvNrYt7JKVEI3HKeiSj5c/PP2fXvtdKelADf7FVUZeGEM/cmiYfU/f4LXO+lBJY87HQ=="], + "@effect/workflow": ["@effect/workflow@0.19.1", "", { "peerDependencies": { "@effect/experimental": "^0.61.1", "@effect/platform": "^0.97.1", "@effect/rpc": "^0.76.2", "effect": "^3.22.1" } }, "sha512-irU6ddHdIxYOhv0cJiJTk2AUxRmPYjRJ5JdB5UCvpJC4vLCyR2XyKspiCNDiEX1aSGPhVWEXhVMWXR1cErMS3A=="], "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], @@ -817,7 +817,7 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "effect": ["effect@3.19.9", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-taMXnfG/p+j7AmMOHHQaCHvjqwu9QBO3cxuZqL2dMG/yWcEMw0ZHruHe9B49OxtfKH/vKKDDKRhZ+1GJ2p5R5w=="], + "effect": ["effect@3.22.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-fm1CQXCs7GTMwpadZxwU5rnAb2bN6mbQpknq/pXBGCzSKkosNYb9k0wsoL0TfB10rSa5xOEZJjo77u/KLXSANg=="], "electron-to-chromium": ["electron-to-chromium@1.5.223", "", {}, "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ=="], @@ -1141,11 +1141,11 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + "msgpackr": ["msgpackr@1.12.1", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -1387,7 +1387,7 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -1483,14 +1483,10 @@ "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@codeforbreakfast/eventsourcing-example-todo/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - "@commitlint/config-validator/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "@commitlint/resolve-extends/global-directory": ["global-directory@5.0.0", "", { "dependencies": { "ini": "6.0.0" } }, "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w=="], - "@effect/cli/yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1967,8 +1963,6 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@codeforbreakfast/eventsourcing-example-todo/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@commitlint/resolve-extends/global-directory/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -2083,8 +2077,6 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@codeforbreakfast/eventsourcing-example-todo/@types/bun/bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], - "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], @@ -2211,8 +2203,6 @@ "npm/wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@codeforbreakfast/eventsourcing-example-todo/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "eslint-plugin-expect-type/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json index f9c00c17..100f47bf 100644 --- a/examples/todo-app/package.json +++ b/examples/todo-app/package.json @@ -21,9 +21,9 @@ "@codeforbreakfast/eventsourcing-projections": "workspace:*", "@codeforbreakfast/eventsourcing-store": "workspace:*", "@codeforbreakfast/eventsourcing-store-filesystem": "workspace:*", - "@effect/cli": "0.72.1", - "@effect/platform-bun": "0.86.0", - "effect": "3.19.9" + "@effect/cli": "0.77.2", + "@effect/platform-bun": "0.91.2", + "effect": "3.22.2" }, "devDependencies": { "@types/bun": "latest" diff --git a/examples/todo-app/src/domain/todoAggregate.ts b/examples/todo-app/src/domain/todoAggregate.ts index ce30ad4d..8da907a8 100644 --- a/examples/todo-app/src/domain/todoAggregate.ts +++ b/examples/todo-app/src/domain/todoAggregate.ts @@ -1,4 +1,4 @@ -import { Effect, Match, Option, ParseResult, Schema, pipe } from 'effect'; +import { Data, Effect, Match, Option, ParseResult, Schema, pipe } from 'effect'; import { makeAggregateRoot, defineAggregateEventStore, @@ -91,20 +91,26 @@ const applyEvent = Match.orElse(() => handleNonCreatedEvent(state, event)) ); +class TodoCommandError extends Data.TaggedError('TodoCommandError')<{ + readonly message: string; +}> {} + const requireExistingTodo = ( operation: string, onSome: (state: TodoState) => Effect.Effect -): ((state: Readonly>) => Effect.Effect) => +): ((state: Readonly>) => Effect.Effect) => Option.match({ - onNone: () => Effect.fail(new Error(`Cannot ${operation} non-existent TODO`)), + onNone: () => + Effect.fail(new TodoCommandError({ message: `Cannot ${operation} non-existent TODO` })), onSome, }); const failIfDeletedTodo = (operation: string) => - (state: TodoState): Effect.Effect => + (state: TodoState): Effect.Effect => Effect.if(state.deleted, { - onTrue: () => Effect.fail(new Error(`Cannot ${operation} deleted TODO`)), + onTrue: () => + Effect.fail(new TodoCommandError({ message: `Cannot ${operation} deleted TODO` })), onFalse: () => Effect.succeed(state), }); diff --git a/examples/todo-app/src/infrastructure/processManager.ts b/examples/todo-app/src/infrastructure/processManager.ts index 8d6deaea..9ff6b0ef 100644 --- a/examples/todo-app/src/infrastructure/processManager.ts +++ b/examples/todo-app/src/infrastructure/processManager.ts @@ -16,12 +16,7 @@ const isTodoCreated = (event: unknown): event is EventRecord => typeof event === 'object' && event !== null && 'type' in event && event.type === 'TodoDeleted'; -const parseUserId = (userId: unknown): Effect.Effect => - pipe( - userId, - Schema.decodeUnknown(UserIdSchema), - Effect.mapError(() => new Error(`Invalid UserId: ${String(userId)}`)) - ); +const parseUserId = Schema.decodeUnknown(UserIdSchema); const provideInitiatorFromUserId = (effect: Effect.Effect) => @@ -49,12 +44,7 @@ const executeAndCommit = , TError>( ) => pipe(command, withCommandInitiator(event), Effect.flatMap(commitEvents(state.nextEventNumber))); -const parseTodoId = (todoId: string): Effect.Effect => - pipe( - todoId, - Schema.decode(TodoIdSchema), - Effect.mapError(() => new Error(`Invalid TodoId: ${todoId}`)) - ); +const parseTodoId = Schema.decode(TodoIdSchema); const handleCommand = , TError>( event: TEvent, diff --git a/package.json b/package.json index 1595adac..e646236c 100644 --- a/package.json +++ b/package.json @@ -47,17 +47,17 @@ "@changesets/cli": "2.31.1", "@commitlint/cli": "20.5.3", "@commitlint/config-conventional": "20.5.3", - "@effect/cluster": "0.55.0", - "@effect/language-service": "0.60.0", - "@effect/platform": "0.93.6", - "@effect/platform-bun": "0.86.0", - "@effect/rpc": "0.72.2", - "@effect/workflow": "0.15.0", + "@effect/cluster": "0.60.2", + "@effect/language-service": "0.87.3", + "@effect/platform": "0.97.2", + "@effect/platform-bun": "0.91.2", + "@effect/rpc": "0.76.2", + "@effect/workflow": "0.19.1", "@types/bun": "1.4.2", "@typescript-eslint/eslint-plugin": "8.70.1", "@typescript-eslint/parser": "8.70.1", "dependency-cruiser": "17.4.3", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "eslint-config-prettier": "10.1.8", "eslint-plugin-compat": "6.2.1", diff --git a/packages/bun-test-effect/package.json b/packages/bun-test-effect/package.json index c278756a..88d06d06 100644 --- a/packages/bun-test-effect/package.json +++ b/packages/bun-test-effect/package.json @@ -44,6 +44,6 @@ "effect": ">=3.0.0" }, "devDependencies": { - "effect": "3.19.9" + "effect": "3.22.2" } } diff --git a/packages/eslint-effect/package.json b/packages/eslint-effect/package.json index 78dda6b4..f7cb6622 100644 --- a/packages/eslint-effect/package.json +++ b/packages/eslint-effect/package.json @@ -48,6 +48,6 @@ "eslint": "9.39.5", "@typescript-eslint/eslint-plugin": "8.70.1", "typescript": "5.9.3", - "effect": "3.19.9" + "effect": "3.22.2" } } diff --git a/packages/eslint-effect/test/no-if-statement.test.ts b/packages/eslint-effect/test/no-if-statement.test.ts index bff27d84..18ea2a85 100644 --- a/packages/eslint-effect/test/no-if-statement.test.ts +++ b/packages/eslint-effect/test/no-if-statement.test.ts @@ -1,4 +1,4 @@ -import { Effect, Either, Option } from 'effect'; +import { Cause, Effect, Either, Option } from 'effect'; const either = Either.right(42); const option = Option.some(42); @@ -23,7 +23,7 @@ if (event.type === 'TodoCreated') { // eslint-disable-next-line effect/no-if-statement if (state.deleted) { - const _unused1 = Effect.fail(new Error('Cannot complete deleted TODO')); + const _unused1 = Effect.fail(new Cause.RuntimeException('Cannot complete deleted TODO')); } else { const _unused2 = Effect.succeed(state); } diff --git a/packages/eslint-effect/test/prefer-match-over-ternary.test.ts b/packages/eslint-effect/test/prefer-match-over-ternary.test.ts index ba6e88a5..6d8d3616 100644 --- a/packages/eslint-effect/test/prefer-match-over-ternary.test.ts +++ b/packages/eslint-effect/test/prefer-match-over-ternary.test.ts @@ -1,4 +1,4 @@ -import { pipe, Effect, Match, Option } from 'effect'; +import { pipe, Cause, Effect, Match, Option } from 'effect'; // Should fail - ternary with Effect calls in return statement const ternaryWithEffect = (condition: boolean) => { @@ -49,8 +49,10 @@ const ternaryInEffectSucceed = (condition: boolean) => Effect.succeed(condition // Should fail - ternary inside Effect.fail (complex condition) const ternaryInEffectFail = (hasError: boolean) => - // eslint-disable-next-line effect/prefer-match-over-ternary - Effect.fail(hasError ? new Error('Critical') : new Error('Warning')); + Effect.fail( + // eslint-disable-next-line effect/prefer-match-over-ternary + hasError ? new Cause.RuntimeException('Critical') : new Cause.RuntimeException('Warning') + ); // Should NOT fail - simple literal equality INSIDE Effect.succeed (no duplication) const simpleLiteralInEffect = (id: string) => Effect.succeed(id === 'user-1' ? 'John' : 'Guest'); diff --git a/packages/eventsourcing-aggregates/package.json b/packages/eventsourcing-aggregates/package.json index b71c1f5c..e5f5c96c 100644 --- a/packages/eventsourcing-aggregates/package.json +++ b/packages/eventsourcing-aggregates/package.json @@ -72,7 +72,7 @@ "@types/node": "24.19.0", "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-store-inmemory": "workspace:*", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-commands/package.json b/packages/eventsourcing-commands/package.json index cbf05da7..f6f790c2 100644 --- a/packages/eventsourcing-commands/package.json +++ b/packages/eventsourcing-commands/package.json @@ -67,7 +67,7 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-projections/package.json b/packages/eventsourcing-projections/package.json index daaa0095..2bc7052e 100644 --- a/packages/eventsourcing-projections/package.json +++ b/packages/eventsourcing-projections/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-protocol/package.json b/packages/eventsourcing-protocol/package.json index b5419ef0..8b995a8c 100644 --- a/packages/eventsourcing-protocol/package.json +++ b/packages/eventsourcing-protocol/package.json @@ -77,7 +77,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-transport-inmemory": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-server/package.json b/packages/eventsourcing-server/package.json index 020735ce..31e8ded7 100644 --- a/packages/eventsourcing-server/package.json +++ b/packages/eventsourcing-server/package.json @@ -68,7 +68,7 @@ "@codeforbreakfast/eventsourcing-store-inmemory": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@codeforbreakfast/bun-test-effect": "workspace:*", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-store-filesystem/package.json b/packages/eventsourcing-store-filesystem/package.json index 5d3ed1cd..fd2f3584 100644 --- a/packages/eventsourcing-store-filesystem/package.json +++ b/packages/eventsourcing-store-filesystem/package.json @@ -68,9 +68,9 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", - "@effect/platform-bun": "0.86.0", + "@effect/platform-bun": "0.91.2", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3" }, diff --git a/packages/eventsourcing-store-filesystem/src/lib/FileSystemStore.ts b/packages/eventsourcing-store-filesystem/src/lib/FileSystemStore.ts index 1f81a18f..27a73e76 100644 --- a/packages/eventsourcing-store-filesystem/src/lib/FileSystemStore.ts +++ b/packages/eventsourcing-store-filesystem/src/lib/FileSystemStore.ts @@ -1,4 +1,5 @@ import { + Cause, Chunk, Effect, Stream, @@ -318,11 +319,8 @@ const appendEventsToStream = ); }; -const parseJsonContent = (content: string): Effect.Effect => - Effect.try({ - try: () => JSON.parse(content) as V, - catch: () => new Error('Failed to parse event'), - }); +const parseJsonContent = (content: string): Effect.Effect => + Effect.try(() => JSON.parse(content) as V); const readEventFromFile = ( eventPath: string, diff --git a/packages/eventsourcing-store-inmemory/package.json b/packages/eventsourcing-store-inmemory/package.json index b6aa7521..d753b98d 100644 --- a/packages/eventsourcing-store-inmemory/package.json +++ b/packages/eventsourcing-store-inmemory/package.json @@ -69,7 +69,7 @@ "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-store-postgres/package.json b/packages/eventsourcing-store-postgres/package.json index 5ffb5802..f3242f06 100644 --- a/packages/eventsourcing-store-postgres/package.json +++ b/packages/eventsourcing-store-postgres/package.json @@ -67,17 +67,17 @@ }, "dependencies": { "@codeforbreakfast/eventsourcing-store": "workspace:*", - "@effect/experimental": "0.57.11", - "@effect/sql": "0.48.6", - "@effect/sql-pg": "0.49.7", + "@effect/experimental": "0.61.1", + "@effect/sql": "0.52.1", + "@effect/sql-pg": "0.53.0", "type-fest": "5.10.0" }, "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", - "@effect/platform-bun": "0.86.0", + "@effect/platform-bun": "0.91.2", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3" }, diff --git a/packages/eventsourcing-store-postgres/src/bridge.integration.test.ts b/packages/eventsourcing-store-postgres/src/bridge.integration.test.ts index 3002cb45..59694908 100644 --- a/packages/eventsourcing-store-postgres/src/bridge.integration.test.ts +++ b/packages/eventsourcing-store-postgres/src/bridge.integration.test.ts @@ -42,11 +42,15 @@ const appendEvents = ( events: readonly string[] ) => pipe(events, Stream.fromIterable, Stream.run(store.append({ streamId, eventNumber: 0 }))); -const forkPerStreamSubscription = (store: EventStore, streamId: EventStreamId) => { +const forkPerStreamSubscription = ( + store: EventStore, + streamId: EventStreamId, + count: number +) => { const subscription = store.subscribe({ streamId, eventNumber: 0 }); return pipe( subscription, - Effect.flatMap((stream) => collectEvents(stream, 2)) + Effect.flatMap((stream) => collectEvents(stream, count)) ); }; @@ -60,7 +64,7 @@ const forkAllEventsSubscription = (store: EventStore, count: number) => const setupDualSubscriptions = (store: EventStore, streamId: EventStreamId) => Effect.all({ - perStreamFiber: forkPerStreamSubscription(store, streamId), + perStreamFiber: forkPerStreamSubscription(store, streamId, 2), allEventsFiber: forkAllEventsSubscription(store, 2), }); @@ -209,6 +213,63 @@ const runMultiStreamTest = (streamId1: EventStreamId, streamId2: EventStreamId) }) ); +const appendImmediatelyAndJoin = ( + store: EventStore, + streamId: EventStreamId, + fiber: Fiber.Fiber, unknown> +) => + pipe( + appendEvents(store, streamId, ['immediate-event']), + Effect.andThen(Fiber.join(fiber)), + Effect.timeout('2 seconds') + ); + +const runImmediatePerStreamTest = (streamId: EventStreamId) => + pipe( + StringEventStore, + Effect.flatMap((store) => + Effect.flatMap(forkPerStreamSubscription(store, streamId, 1), (fiber) => + appendImmediatelyAndJoin(store, streamId, fiber) + ) + ), + Effect.map((events) => { + expect(Array.from(events)).toEqual(['immediate-event']); + }) + ); + +const runImmediateAllEventsTest = (streamId: EventStreamId) => + pipe( + StringEventStore, + Effect.flatMap((store) => + Effect.flatMap(forkAllEventsSubscription(store, 1), (fiber) => + appendImmediatelyAndJoin(store, streamId, fiber) + ) + ), + Effect.map((events) => { + expect(Array.from(Chunk.map(events, (e) => e.event))).toEqual(['immediate-event']); + }) + ); + +describe('Subscriptions are live as soon as they return', () => { + it('should deliver an event appended as soon as subscribe returns', () => + pipe( + randomId(), + decodeStreamId, + Effect.flatMap(runImmediatePerStreamTest), + Effect.provide(TestLayer), + Effect.runPromise + )); + + it('should deliver an event appended as soon as subscribeAll returns', () => + pipe( + randomId(), + decodeStreamId, + Effect.flatMap(runImmediateAllEventsTest), + Effect.provide(TestLayer), + Effect.runPromise + )); +}); + describe('Bridge notification publishing', () => { it('should publish events to BOTH per-stream subscribers AND all-events subscribers', () => { const streamId = decodeStreamId(randomId()); diff --git a/packages/eventsourcing-store-postgres/src/notificationListener.ts b/packages/eventsourcing-store-postgres/src/notificationListener.ts index fb6fd9fb..0fff65d9 100644 --- a/packages/eventsourcing-store-postgres/src/notificationListener.ts +++ b/packages/eventsourcing-store-postgres/src/notificationListener.ts @@ -1,5 +1,18 @@ +import { SqlError } from '@effect/sql'; import { PgClient } from '@effect/sql-pg'; -import { Effect, Layer, Stream, Ref, Queue, Schema, HashSet, pipe } from 'effect'; +import { + Deferred, + Effect, + Layer, + Match, + Stream, + Ref, + Queue, + Schedule, + Schema, + HashSet, + pipe, +} from 'effect'; import { EventStreamId, EventStoreError, @@ -25,6 +38,14 @@ export const makeChannelName = (streamId: EventStreamId): string => `eventstore_ */ export const ALL_EVENTS_CHANNEL = 'eventstore_events_all'; +/** + * Prefix of the payloads a listener sends to its own channel to learn that LISTEN is active. + * Every listener on the channel receives them, so every listener drops them. + */ +const LISTEN_PROBE_PREFIX = 'eventstore_listen_probe:'; + +const LISTEN_PROBE_INTERVAL = '10 millis'; + /** * Parse notification payload from PostgreSQL trigger JSON */ @@ -166,6 +187,73 @@ const processRawNotification = ) ); +const handleUnlessProbe = + ( + probe: string, + active: Deferred.Deferred, + handleNotification: (rawPayload: string) => Effect.Effect + ) => + (rawPayload: string) => + pipe( + rawPayload, + Match.value, + Match.when(probe, () => Deferred.succeed(active, undefined)), + Match.when( + (payload) => payload.startsWith(LISTEN_PROBE_PREFIX), + () => Effect.void + ), + Match.orElse(handleNotification), + Effect.asVoid + ); + +const sendProbeUntilActive = ( + client: PgClient.PgClient, + channelName: string, + probe: string, + active: Deferred.Deferred +) => + pipe( + // Not client.notify: @effect/sql-pg 0.53 sends `NOTIFY channel, $1`, which Postgres rejects. + client`SELECT pg_notify(${channelName}, ${probe})`, + Effect.repeat(Schedule.spaced(LISTEN_PROBE_INTERVAL)), + Effect.raceFirst(Deferred.await(active)) + ); + +const listenInBackground = ( + client: PgClient.PgClient, + channelName: string, + probe: string, + active: Deferred.Deferred, + handleNotification: (rawPayload: string) => Effect.Effect +) => + pipe( + channelName, + client.listen, + Stream.runForEach(handleUnlessProbe(probe, active, handleNotification)), + Effect.tapError((error) => Deferred.fail(active, error)), + Effect.fork + ); + +/** + * Starts listening on a channel in the background and waits until LISTEN is active, + * so that no notification sent after this returns can be missed. + */ +const listenUntilActive = ( + client: PgClient.PgClient, + channelName: string, + handleNotification: (rawPayload: string) => Effect.Effect +) => + pipe( + Deferred.make(), + Effect.flatMap((active) => { + const probe = `${LISTEN_PROBE_PREFIX}${crypto.randomUUID()}`; + return Effect.andThen( + listenInBackground(client, channelName, probe, active, handleNotification), + sendProbeUntilActive(client, channelName, probe, active) + ); + }) + ); + const startListeningOnChannel = ( client: PgClient.PgClient, notificationQueue: Queue.Queue<{ @@ -175,13 +263,10 @@ const startListeningOnChannel = ( }>, channelName: string ) => - pipe( + listenUntilActive( + client, channelName, - client.listen, - Stream.tap(processRawNotification(notificationQueue, channelName, false)), - Stream.runDrain, - Effect.fork, - Effect.asVoid + processRawNotification(notificationQueue, channelName, false) ); const activateChannelAndStartListening = ( @@ -345,15 +430,7 @@ const startListeningOnAllEventsChannel = ( readonly payload: NotificationPayload; readonly isAllEvents: boolean; }> -) => - pipe( - ALL_EVENTS_CHANNEL, - client.listen, - Stream.tap(processAllEventsNotification(notificationQueue)), - Stream.runDrain, - Effect.fork, - Effect.asVoid - ); +) => listenUntilActive(client, ALL_EVENTS_CHANNEL, processAllEventsNotification(notificationQueue)); const activateAllEventsChannel = ( activeChannels: Ref.Ref>, diff --git a/packages/eventsourcing-store/package.json b/packages/eventsourcing-store/package.json index 6f58a79e..65b6c904 100644 --- a/packages/eventsourcing-store/package.json +++ b/packages/eventsourcing-store/package.json @@ -69,7 +69,7 @@ "devDependencies": { "@types/node": "24.19.0", "@codeforbreakfast/bun-test-effect": "workspace:*", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-store/src/lib/streaming/connectionManager.ts b/packages/eventsourcing-store/src/lib/streaming/connectionManager.ts index 1968fa24..21935810 100644 --- a/packages/eventsourcing-store/src/lib/streaming/connectionManager.ts +++ b/packages/eventsourcing-store/src/lib/streaming/connectionManager.ts @@ -76,7 +76,11 @@ export const ConnectionConfigLive = Layer.effect( ConnectionConfigTag, Effect.mapError( Config.unwrap(ConnectionConfigSchema), - (error) => new Error(`Failed to load connection configuration: ${JSON.stringify(error)}`) + (error) => + new ConnectionError({ + message: `Failed to load connection configuration: ${JSON.stringify(error)}`, + cause: error, + }) ) ); diff --git a/packages/eventsourcing-store/src/lib/testing/eventstore-test-suite.ts b/packages/eventsourcing-store/src/lib/testing/eventstore-test-suite.ts index fd757399..96da5dfd 100644 --- a/packages/eventsourcing-store/src/lib/testing/eventstore-test-suite.ts +++ b/packages/eventsourcing-store/src/lib/testing/eventstore-test-suite.ts @@ -1,4 +1,15 @@ -import { Chunk, Duration, Effect, Layer, ParseResult, Schema, Stream, pipe, Ref } from 'effect'; +import { + Cause, + Chunk, + Duration, + Effect, + Layer, + ParseResult, + Schema, + Stream, + pipe, + Ref, +} from 'effect'; import { beforeAll, beforeEach, describe, expect, it } from 'bun:test'; import { EventStreamId, EventStreamPosition, beginning } from '../streamTypes'; import { type EventStore, ConcurrencyConflictError } from '../eventstore'; @@ -625,7 +636,7 @@ export function runEventStoreTestSuite( Effect.sleep, Effect.andThen( Effect.fail( - new Error( + new Cause.TimeoutException( `Subscription timed out. Received ${receivedEvents.length} events: ${JSON.stringify(receivedEvents)}` ) ) diff --git a/packages/eventsourcing-testing-contracts/package.json b/packages/eventsourcing-testing-contracts/package.json index 5dc4e034..dc3bfaa6 100644 --- a/packages/eventsourcing-testing-contracts/package.json +++ b/packages/eventsourcing-testing-contracts/package.json @@ -72,7 +72,7 @@ "devDependencies": { "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-testing-contracts/src/lib/test-utilities.ts b/packages/eventsourcing-testing-contracts/src/lib/test-utilities.ts index 1f386fb5..34402228 100644 --- a/packages/eventsourcing-testing-contracts/src/lib/test-utilities.ts +++ b/packages/eventsourcing-testing-contracts/src/lib/test-utilities.ts @@ -5,7 +5,7 @@ * and test data generators for transport testing scenarios. */ -import { Effect, Stream, pipe, Chunk, Ref, Duration, Scope } from 'effect'; +import { Cause, Effect, Stream, pipe, Chunk, Ref, Duration, Scope } from 'effect'; import { TransportError, makeTransportMessage } from '@codeforbreakfast/eventsourcing-transport'; import type { TransportMessage, @@ -238,7 +238,7 @@ const flipAndFilterError = ( Effect.filterOrFail( Effect.flip(effect), errorPredicate, - () => new Error('Error did not match predicate') + () => new Cause.NoSuchElementException('Error did not match predicate') ) as Effect.Effect; /** @@ -286,7 +286,7 @@ const filterTakeAndDrain = ( ), timeoutMs ), - () => new Error(`Timeout waiting for connection state: ${expectedState}`) + () => new Cause.TimeoutException(`Timeout waiting for connection state: ${expectedState}`) ); /** @@ -310,7 +310,7 @@ const takeCollectAndTimeout = ( Effect.map(Stream.runCollect(Stream.take(stream, count)), (chunk) => Array.from(chunk)), timeoutMs ), - () => new Error(`Timeout collecting ${count} messages`) + () => new Cause.TimeoutException(`Timeout collecting ${count} messages`) ); /** diff --git a/packages/eventsourcing-testing-contracts/src/lib/transport/client-server-contract-tests.ts b/packages/eventsourcing-testing-contracts/src/lib/transport/client-server-contract-tests.ts index db3e82ff..0cff0750 100644 --- a/packages/eventsourcing-testing-contracts/src/lib/transport/client-server-contract-tests.ts +++ b/packages/eventsourcing-testing-contracts/src/lib/transport/client-server-contract-tests.ts @@ -9,7 +9,7 @@ */ import { describe, test, expect, beforeEach, afterEach } from '@codeforbreakfast/bun-test-effect'; -import { Effect, Stream, Scope, pipe, Option, Exit } from 'effect'; +import { Cause, Effect, Stream, Scope, pipe, Option, Exit } from 'effect'; import type { TransportMessage, ConnectionState } from '@codeforbreakfast/eventsourcing-transport'; // ============================================================================= @@ -209,7 +209,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( if (Option.isSome(clientState)) { return Effect.sync(() => expect(clientState.value).toBe('connected')); } else { - return Effect.fail(new Error('Expected client to reach connected state')); + return Effect.fail( + new Cause.NoSuchElementException('Expected client to reach connected state') + ); } } ) @@ -267,7 +269,11 @@ export const runClientServerContractTests: ClientServerTestRunner = ( Stream.runHead(Stream.take(server.connections, 1)), (serverConnection) => { if (!Option.isSome(serverConnection)) { - return Effect.fail(new Error('Expected server connection to be available')); + return Effect.fail( + new Cause.NoSuchElementException( + 'Expected server connection to be available' + ) + ); } const connection = serverConnection.value; return Effect.flatMap(connection.transport.subscribe(), (messageStream) => @@ -436,7 +442,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( const handleServerConnection = (client: ClientTransport) => (serverConnection: Option.Option) => { if (!Option.isSome(serverConnection)) { - return Effect.fail(new Error('Expected server connection to be available')); + return Effect.fail( + new Cause.NoSuchElementException('Expected server connection to be available') + ); } const connection = serverConnection.value; return subscribeAndCommunicate(client, connection); @@ -552,7 +560,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( if (Option.isSome(finalClientState)) { return Effect.sync(() => expect(finalClientState.value).toBe('disconnected')); } else { - return Effect.fail(new Error('Expected final client state to be available')); + return Effect.fail( + new Cause.NoSuchElementException('Expected final client state to be available') + ); } }; @@ -580,7 +590,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( (clientScope: Scope.CloseableScope) => (serverConnection: Option.Option) => { if (!Option.isSome(serverConnection)) { - return Effect.fail(new Error('Expected server connection to be available')); + return Effect.fail( + new Cause.NoSuchElementException('Expected server connection to be available') + ); } return closeScopeAndVerify(clientScope, serverConnection.value); }; @@ -646,7 +658,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( if (Option.isSome(disconnectedState)) { return Effect.sync(() => expect(disconnectedState.value).toBe('disconnected')); } else { - return Effect.fail(new Error('Expected disconnected state to be available')); + return Effect.fail( + new Cause.NoSuchElementException('Expected disconnected state to be available') + ); } }; @@ -778,7 +792,9 @@ export const runClientServerContractTests: ClientServerTestRunner = ( const handleServerConnection = (client: ClientTransport) => (serverConnection: Option.Option) => { if (!Option.isSome(serverConnection)) { - return Effect.fail(new Error('Expected server connection to be available')); + return Effect.fail( + new Cause.NoSuchElementException('Expected server connection to be available') + ); } const connection = serverConnection.value; return subscribeAndPublish(connection, client); diff --git a/packages/eventsourcing-testing-contracts/src/lib/transport/server-transport-contract-tests.ts b/packages/eventsourcing-testing-contracts/src/lib/transport/server-transport-contract-tests.ts index 4779c92c..4371d9cf 100644 --- a/packages/eventsourcing-testing-contracts/src/lib/transport/server-transport-contract-tests.ts +++ b/packages/eventsourcing-testing-contracts/src/lib/transport/server-transport-contract-tests.ts @@ -9,7 +9,19 @@ */ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { Effect, Stream, Scope, pipe, Option, Exit, Fiber, Duration, Chunk, Ref } from 'effect'; +import { + Cause, + Effect, + Stream, + Scope, + pipe, + Option, + Exit, + Fiber, + Duration, + Chunk, + Ref, +} from 'effect'; import type { TransportMessage, ConnectionState } from '@codeforbreakfast/eventsourcing-transport'; // ============================================================================= @@ -288,7 +300,9 @@ export const runServerTransportContractTests: ServerTestRunner = ( Effect.flatMap((serverConnection) => { if (!Option.isSome(serverConnection)) { return Effect.fail( - new Error('Expected server connection to be available') + new Cause.NoSuchElementException( + 'Expected server connection to be available' + ) ); } @@ -581,7 +595,9 @@ export const runServerTransportContractTests: ServerTestRunner = ( Effect.flatMap((serverConnection) => { if (!Option.isSome(serverConnection)) { return Effect.fail( - new Error('Expected server connection to be available') + new Cause.NoSuchElementException( + 'Expected server connection to be available' + ) ); } @@ -796,7 +812,9 @@ export const runServerTransportContractTests: ServerTestRunner = ( Effect.flatMap((serverConnection) => { if (!Option.isSome(serverConnection)) { return Effect.fail( - new Error('Expected server connection to be available') + new Cause.NoSuchElementException( + 'Expected server connection to be available' + ) ); } diff --git a/packages/eventsourcing-transport-inmemory/package.json b/packages/eventsourcing-transport-inmemory/package.json index 63fa256e..01b9a30c 100644 --- a/packages/eventsourcing-transport-inmemory/package.json +++ b/packages/eventsourcing-transport-inmemory/package.json @@ -70,7 +70,7 @@ "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@codeforbreakfast/bun-test-effect": "workspace:*", "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3" }, diff --git a/packages/eventsourcing-transport-websocket/package.json b/packages/eventsourcing-transport-websocket/package.json index a715bc2a..28b8043f 100644 --- a/packages/eventsourcing-transport-websocket/package.json +++ b/packages/eventsourcing-transport-websocket/package.json @@ -63,7 +63,7 @@ }, "dependencies": { "@codeforbreakfast/eventsourcing-transport": "workspace:*", - "@effect/platform": "0.93.6", + "@effect/platform": "0.97.2", "type-fest": "5.10.0" }, "devDependencies": { @@ -71,7 +71,7 @@ "@codeforbreakfast/eventsourcing-testing-contracts": "workspace:*", "@types/node": "24.19.0", "bun-types": "1.4.2", - "effect": "3.19.9", + "effect": "3.22.2", "eslint": "9.39.5", "typescript": "5.9.3" }, diff --git a/packages/eventsourcing-transport/package.json b/packages/eventsourcing-transport/package.json index 021ad655..88bd6e0b 100644 --- a/packages/eventsourcing-transport/package.json +++ b/packages/eventsourcing-transport/package.json @@ -64,7 +64,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/packages/eventsourcing-websocket/package.json b/packages/eventsourcing-websocket/package.json index e95c86f6..e360097b 100644 --- a/packages/eventsourcing-websocket/package.json +++ b/packages/eventsourcing-websocket/package.json @@ -72,7 +72,7 @@ }, "devDependencies": { "@types/node": "24.19.0", - "effect": "3.19.9", + "effect": "3.22.2", "typescript": "5.9.3", "eslint": "9.39.5" }, diff --git a/scripts/validate-markdown-examples.ts b/scripts/validate-markdown-examples.ts index d5564b62..d96f0c1b 100644 --- a/scripts/validate-markdown-examples.ts +++ b/scripts/validate-markdown-examples.ts @@ -1,9 +1,13 @@ #!/usr/bin/env bun -import { Effect, pipe, Array as EffectArray, Console, Option, Match } from 'effect'; +import { Data, Effect, pipe, Array as EffectArray, Console, Option, Match } from 'effect'; import { Path, Command, Terminal, FileSystem } from '@effect/platform'; import { BunContext, BunRuntime } from '@effect/platform-bun'; +class DocsValidationError extends Data.TaggedError('DocsValidationError')<{ + readonly message: string; +}> {} + interface CodeBlock { readonly code: string; readonly file: string; @@ -263,7 +267,10 @@ const readDirectoryForProcessing = (currentDir: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.readDirectory(currentDir)), - Effect.mapError((error) => new Error(`Failed to read directory ${currentDir}: ${error}`)) + Effect.mapError( + (error) => + new DocsValidationError({ message: `Failed to read directory ${currentDir}: ${error}` }) + ) ); const processDirectory = ( @@ -309,7 +316,9 @@ const writeFileStringToPath = (content: string) => (path: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.writeFileString(path, content)), - Effect.mapError((error) => new Error(`Failed to write tsconfig: ${error}`)) + Effect.mapError( + (error) => new DocsValidationError({ message: `Failed to write tsconfig: ${error}` }) + ) ); const writeTsConfig = (tempDir: string) => @@ -321,7 +330,9 @@ const writeBlockFileContent = pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.writeFileString(filepath, content)), - Effect.mapError((error) => new Error(`Failed to write ${filename}: ${error}`)), + Effect.mapError( + (error) => new DocsValidationError({ message: `Failed to write ${filename}: ${error}` }) + ), Effect.as({ filename, block, headerLines }) ); @@ -442,7 +453,9 @@ const cleanupTempDir = (tempDir: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.remove(tempDir, { recursive: true })), - Effect.mapError(() => new Error(`Failed to cleanup temp dir ${tempDir}`)), + Effect.mapError( + () => new DocsValidationError({ message: `Failed to cleanup temp dir ${tempDir}` }) + ), Effect.orElseSucceed(() => undefined) ); @@ -475,7 +488,7 @@ const cleanTempDirectory = (tempDir: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.remove(tempDir, { recursive: true })), - Effect.mapError(() => new Error(`Failed to clean temp dir`)), + Effect.mapError(() => new DocsValidationError({ message: `Failed to clean temp dir` })), Effect.orElseSucceed(() => undefined) ); @@ -483,7 +496,9 @@ const createTempDirectory = (tempDir: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.makeDirectory(tempDir, { recursive: true })), - Effect.mapError((error) => new Error(`Failed to create temp dir: ${error}`)) + Effect.mapError( + (error) => new DocsValidationError({ message: `Failed to create temp dir: ${error}` }) + ) ); const getRelativePath = (from: string, to: string) => @@ -496,7 +511,9 @@ const readFileAsString = (file: string) => pipe( FileSystem.FileSystem, Effect.andThen((fs) => fs.readFileString(file)), - Effect.mapError((error) => new Error(`Failed to read ${file}: ${error}`)) + Effect.mapError( + (error) => new DocsValidationError({ message: `Failed to read ${file}: ${error}` }) + ) ); const readAndExtractCodeBlocks = @@ -534,7 +551,7 @@ const displayErrorsAndFail = (errors: readonly ValidationError[]) => { Effect.andThen(Console.log(' 1. Update the code examples to match current APIs')), Effect.andThen(Console.log(' 2. Add missing imports or type annotations')), Effect.andThen(Console.log(' 3. Verify examples compile with: bun run validate:docs\n')), - Effect.andThen(Effect.fail(new Error('Validation failed'))) + Effect.andThen(Effect.fail(new DocsValidationError({ message: 'Validation failed' }))) ); };