effect-distributed-lock 0.0.1
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 +15 -0
- package/bun.lock +249 -0
- package/examples/index.ts +89 -0
- package/package.json +30 -0
- package/src/DistributedMutex.ts +304 -0
- package/src/Errors.ts +56 -0
- package/src/RedisBacking.ts +268 -0
- package/src/index.ts +50 -0
- package/tsconfig.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# effect-distributed-lock
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "effect-distributed-lock",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"effect": "^3.19.13",
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "latest",
|
|
11
|
+
"ioredis": "^5.4.1",
|
|
12
|
+
"tsup": "^8.5.1",
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"effect": "^3.0.0",
|
|
16
|
+
"ioredis": "^5.0.0",
|
|
17
|
+
"typescript": "^5",
|
|
18
|
+
},
|
|
19
|
+
"optionalPeers": [
|
|
20
|
+
"ioredis",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"packages": {
|
|
25
|
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
|
26
|
+
|
|
27
|
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
|
28
|
+
|
|
29
|
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
|
30
|
+
|
|
31
|
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
|
32
|
+
|
|
33
|
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
|
34
|
+
|
|
35
|
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
|
36
|
+
|
|
37
|
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
|
38
|
+
|
|
39
|
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
|
40
|
+
|
|
41
|
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
|
42
|
+
|
|
43
|
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
|
44
|
+
|
|
45
|
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
|
46
|
+
|
|
47
|
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
|
48
|
+
|
|
49
|
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
|
50
|
+
|
|
51
|
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
|
52
|
+
|
|
53
|
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
|
54
|
+
|
|
55
|
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
|
56
|
+
|
|
57
|
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
|
58
|
+
|
|
59
|
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
|
60
|
+
|
|
61
|
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
|
62
|
+
|
|
63
|
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
|
64
|
+
|
|
65
|
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
|
66
|
+
|
|
67
|
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
|
68
|
+
|
|
69
|
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
|
70
|
+
|
|
71
|
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
|
72
|
+
|
|
73
|
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
|
74
|
+
|
|
75
|
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
|
76
|
+
|
|
77
|
+
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
|
78
|
+
|
|
79
|
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
|
80
|
+
|
|
81
|
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
|
82
|
+
|
|
83
|
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
|
84
|
+
|
|
85
|
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
|
86
|
+
|
|
87
|
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="],
|
|
88
|
+
|
|
89
|
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="],
|
|
90
|
+
|
|
91
|
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="],
|
|
92
|
+
|
|
93
|
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="],
|
|
94
|
+
|
|
95
|
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="],
|
|
96
|
+
|
|
97
|
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="],
|
|
98
|
+
|
|
99
|
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="],
|
|
100
|
+
|
|
101
|
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="],
|
|
102
|
+
|
|
103
|
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="],
|
|
104
|
+
|
|
105
|
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="],
|
|
106
|
+
|
|
107
|
+
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="],
|
|
108
|
+
|
|
109
|
+
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="],
|
|
110
|
+
|
|
111
|
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="],
|
|
112
|
+
|
|
113
|
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="],
|
|
114
|
+
|
|
115
|
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="],
|
|
116
|
+
|
|
117
|
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="],
|
|
118
|
+
|
|
119
|
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="],
|
|
120
|
+
|
|
121
|
+
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="],
|
|
122
|
+
|
|
123
|
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="],
|
|
124
|
+
|
|
125
|
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="],
|
|
126
|
+
|
|
127
|
+
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="],
|
|
128
|
+
|
|
129
|
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="],
|
|
130
|
+
|
|
131
|
+
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
132
|
+
|
|
133
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
134
|
+
|
|
135
|
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
|
136
|
+
|
|
137
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
138
|
+
|
|
139
|
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
|
140
|
+
|
|
141
|
+
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
|
142
|
+
|
|
143
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
144
|
+
|
|
145
|
+
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
|
146
|
+
|
|
147
|
+
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
|
148
|
+
|
|
149
|
+
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
|
150
|
+
|
|
151
|
+
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
|
152
|
+
|
|
153
|
+
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
|
154
|
+
|
|
155
|
+
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
|
156
|
+
|
|
157
|
+
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
|
158
|
+
|
|
159
|
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
160
|
+
|
|
161
|
+
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
|
162
|
+
|
|
163
|
+
"effect": ["effect@3.19.13", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-8MZ783YuHRwHZX2Mmm+bpGxq+7XPd88sWwYAz2Ysry80sEKpftDZXs2Hg9ZyjESi1IBTNHF0oDKe0zJRkUlyew=="],
|
|
164
|
+
|
|
165
|
+
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
|
166
|
+
|
|
167
|
+
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
|
168
|
+
|
|
169
|
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
|
170
|
+
|
|
171
|
+
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
|
|
172
|
+
|
|
173
|
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
174
|
+
|
|
175
|
+
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
|
176
|
+
|
|
177
|
+
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
|
178
|
+
|
|
179
|
+
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
|
180
|
+
|
|
181
|
+
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
|
182
|
+
|
|
183
|
+
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
|
184
|
+
|
|
185
|
+
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
|
186
|
+
|
|
187
|
+
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
|
188
|
+
|
|
189
|
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
|
190
|
+
|
|
191
|
+
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
|
192
|
+
|
|
193
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
194
|
+
|
|
195
|
+
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
|
196
|
+
|
|
197
|
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
|
198
|
+
|
|
199
|
+
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
|
200
|
+
|
|
201
|
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
202
|
+
|
|
203
|
+
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
|
204
|
+
|
|
205
|
+
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
|
206
|
+
|
|
207
|
+
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
|
208
|
+
|
|
209
|
+
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
|
210
|
+
|
|
211
|
+
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
|
212
|
+
|
|
213
|
+
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
|
214
|
+
|
|
215
|
+
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
|
216
|
+
|
|
217
|
+
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
|
218
|
+
|
|
219
|
+
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
|
220
|
+
|
|
221
|
+
"rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="],
|
|
222
|
+
|
|
223
|
+
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
|
224
|
+
|
|
225
|
+
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
|
226
|
+
|
|
227
|
+
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
|
228
|
+
|
|
229
|
+
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
|
230
|
+
|
|
231
|
+
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
|
232
|
+
|
|
233
|
+
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
|
234
|
+
|
|
235
|
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
|
236
|
+
|
|
237
|
+
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
|
238
|
+
|
|
239
|
+
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
|
240
|
+
|
|
241
|
+
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
|
|
242
|
+
|
|
243
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
244
|
+
|
|
245
|
+
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
|
246
|
+
|
|
247
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the distributed mutex library.
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun run example.ts
|
|
5
|
+
* Requires REDIS_URL environment variable.
|
|
6
|
+
*/
|
|
7
|
+
import { Effect, Console, Duration } from "effect";
|
|
8
|
+
import Redis from "ioredis";
|
|
9
|
+
import { DistributedMutex, RedisBacking } from "../src/index.ts";
|
|
10
|
+
|
|
11
|
+
// Create Redis client
|
|
12
|
+
const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
|
|
13
|
+
|
|
14
|
+
// Create the Redis backing layer
|
|
15
|
+
const RedisLayer = RedisBacking.layer(redis, "example:");
|
|
16
|
+
|
|
17
|
+
// Example 1: Using withLock for a critical section
|
|
18
|
+
const example1 = Effect.gen(function* () {
|
|
19
|
+
yield* Console.log("=== Example 1: withLock ===");
|
|
20
|
+
|
|
21
|
+
const mutex = yield* DistributedMutex.make("my-resource", {
|
|
22
|
+
ttl: Duration.seconds(10),
|
|
23
|
+
acquireTimeout: Duration.seconds(5),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
yield* mutex.withLock(
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
yield* Console.log("Lock acquired! Doing critical work...");
|
|
29
|
+
yield* Effect.sleep(2000);
|
|
30
|
+
yield* Console.log("Critical work done!");
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
yield* Console.log("Lock released automatically");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Example 2: Using tryAcquire (non-blocking)
|
|
38
|
+
const example2 = Effect.gen(function* () {
|
|
39
|
+
yield* Console.log("\n=== Example 2: withLockIfAvailable ===");
|
|
40
|
+
|
|
41
|
+
const mutex = yield* DistributedMutex.make("another-resource", {
|
|
42
|
+
ttl: Duration.seconds(10),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = yield* mutex.withLockIfAvailable(
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
yield* Console.log("Got the lock without waiting!");
|
|
48
|
+
return "success";
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
yield* Console.log(`Result: ${JSON.stringify(result)}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Example 3: Manual scope management with acquire()
|
|
56
|
+
const example3 = Effect.gen(function* () {
|
|
57
|
+
yield* Console.log("\n=== Example 3: Manual acquire with Scope ===");
|
|
58
|
+
|
|
59
|
+
const mutex = yield* DistributedMutex.make("manual-resource", {
|
|
60
|
+
ttl: Duration.seconds(10),
|
|
61
|
+
acquireTimeout: Duration.seconds(5),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Using Effect.scoped to manage the lock lifecycle
|
|
65
|
+
yield* Effect.scoped(
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
yield* mutex.acquire;
|
|
68
|
+
yield* Console.log("Lock acquired via acquire()");
|
|
69
|
+
yield* Effect.sleep(1000);
|
|
70
|
+
yield* Console.log("About to exit scope...");
|
|
71
|
+
// Lock is automatically released when scope closes
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
yield* Console.log("Scope closed, lock released");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Run all examples
|
|
79
|
+
const main = Effect.gen(function* () {
|
|
80
|
+
yield* example1;
|
|
81
|
+
yield* example2;
|
|
82
|
+
yield* example3;
|
|
83
|
+
yield* Console.log("\n✓ All examples completed!");
|
|
84
|
+
}).pipe(
|
|
85
|
+
Effect.ensuring(Effect.promise(() => redis.quit())),
|
|
86
|
+
Effect.provide(RedisLayer)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
Effect.runPromise(main).catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "effect-distributed-lock",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A distributed mutex library for Effect with pluggable backends",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"check": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest",
|
|
15
|
+
"ioredis": "^5.4.1"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5",
|
|
19
|
+
"effect": "^3.0.0",
|
|
20
|
+
"ioredis": "^5.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"ioredis": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"effect": "^3.19.13"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Option, Schedule, Scope } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
AcquireTimeoutError,
|
|
4
|
+
BackingError,
|
|
5
|
+
LockLostError,
|
|
6
|
+
NotYetAcquiredError,
|
|
7
|
+
} from "./Errors.js";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Backing Service
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Low-level backing store interface for distributed mutex operations.
|
|
15
|
+
* Implementations handle the actual storage (Redis, etcd, DynamoDB, etc.)
|
|
16
|
+
*/
|
|
17
|
+
export interface DistributedMutexBacking {
|
|
18
|
+
/**
|
|
19
|
+
* Try to acquire the lock. Returns true if acquired, false if already held.
|
|
20
|
+
* Must set TTL on the lock.
|
|
21
|
+
*/
|
|
22
|
+
readonly tryAcquire: (
|
|
23
|
+
key: string,
|
|
24
|
+
holderId: string,
|
|
25
|
+
ttlMs: number
|
|
26
|
+
) => Effect.Effect<boolean, BackingError>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Release the lock. Only succeeds if we are the current holder.
|
|
30
|
+
* Returns true if released, false if we weren't the holder.
|
|
31
|
+
*/
|
|
32
|
+
readonly release: (
|
|
33
|
+
key: string,
|
|
34
|
+
holderId: string
|
|
35
|
+
) => Effect.Effect<boolean, BackingError>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Refresh the TTL on a lock we hold.
|
|
39
|
+
* Returns true if refreshed, false if lock was lost.
|
|
40
|
+
*/
|
|
41
|
+
readonly refresh: (
|
|
42
|
+
key: string,
|
|
43
|
+
holderId: string,
|
|
44
|
+
ttlMs: number
|
|
45
|
+
) => Effect.Effect<boolean, BackingError>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if the lock is currently held (by anyone).
|
|
49
|
+
*/
|
|
50
|
+
readonly isLocked: (key: string) => Effect.Effect<boolean, BackingError>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the current holder ID, if any.
|
|
54
|
+
*/
|
|
55
|
+
readonly getHolder: (
|
|
56
|
+
key: string
|
|
57
|
+
) => Effect.Effect<Option.Option<string>, BackingError>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DistributedMutexBacking =
|
|
61
|
+
Context.GenericTag<DistributedMutexBacking>(
|
|
62
|
+
"@effect-distributed-lock/DistributedMutexBacking"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Mutex Configuration
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
export interface DistributedMutexConfig {
|
|
70
|
+
/**
|
|
71
|
+
* TTL for the lock. If the holder crashes, the lock auto-releases after this.
|
|
72
|
+
* @default 30 seconds
|
|
73
|
+
*/
|
|
74
|
+
readonly ttl?: Duration.DurationInput;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How often to refresh the TTL while holding the lock.
|
|
78
|
+
* Should be less than TTL to avoid losing the lock.
|
|
79
|
+
* @default 1/3 of TTL
|
|
80
|
+
*/
|
|
81
|
+
readonly refreshInterval?: Duration.DurationInput;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* How often to poll when waiting to acquire the lock.
|
|
85
|
+
* @default 100ms
|
|
86
|
+
*/
|
|
87
|
+
readonly retryInterval?: Duration.DurationInput;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Maximum time to wait when acquiring the lock.
|
|
91
|
+
* If not set, will wait forever.
|
|
92
|
+
*/
|
|
93
|
+
readonly acquireTimeout?: Duration.DurationInput;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DEFAULT_TTL = Duration.seconds(30);
|
|
97
|
+
const DEFAULT_RETRY_INTERVAL = Duration.millis(100);
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Distributed Mutex Interface
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A distributed mutex that can be used across multiple processes/services.
|
|
105
|
+
*/
|
|
106
|
+
export interface DistributedMutex {
|
|
107
|
+
/**
|
|
108
|
+
* The key identifying this mutex in the backing store.
|
|
109
|
+
*/
|
|
110
|
+
readonly key: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Acquire the lock, run the effect, then release.
|
|
114
|
+
* If acquisition fails within timeout, returns AcquireTimeoutError.
|
|
115
|
+
* The lock TTL is refreshed automatically while the effect runs.
|
|
116
|
+
*/
|
|
117
|
+
readonly withLock: <A, E, R>(
|
|
118
|
+
effect: Effect.Effect<A, E, R>
|
|
119
|
+
) => Effect.Effect<
|
|
120
|
+
A,
|
|
121
|
+
E | AcquireTimeoutError | LockLostError | BackingError,
|
|
122
|
+
R
|
|
123
|
+
>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Try to acquire the lock immediately without waiting.
|
|
127
|
+
* Returns Some(result) if lock was acquired and effect ran,
|
|
128
|
+
* None if lock was not available.
|
|
129
|
+
*/
|
|
130
|
+
readonly withLockIfAvailable: <A, E, R>(
|
|
131
|
+
effect: Effect.Effect<A, E, R>
|
|
132
|
+
) => Effect.Effect<Option.Option<A>, E | LockLostError | BackingError, R>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Acquire the lock, waiting if necessary.
|
|
136
|
+
* The lock is held until the scope is closed.
|
|
137
|
+
* The lock TTL is refreshed automatically while held.
|
|
138
|
+
*/
|
|
139
|
+
readonly acquire: Effect.Effect<
|
|
140
|
+
void,
|
|
141
|
+
AcquireTimeoutError | LockLostError | BackingError,
|
|
142
|
+
Scope.Scope
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Try to acquire immediately without waiting.
|
|
147
|
+
* Returns Some(void) if acquired (lock held until scope closes),
|
|
148
|
+
* None if lock was not available.
|
|
149
|
+
*/
|
|
150
|
+
readonly tryAcquire: Effect.Effect<
|
|
151
|
+
Option.Option<void>,
|
|
152
|
+
LockLostError | BackingError,
|
|
153
|
+
Scope.Scope
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if the lock is currently held.
|
|
158
|
+
*/
|
|
159
|
+
readonly isLocked: Effect.Effect<boolean, BackingError>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Factory
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create a distributed mutex for the given key.
|
|
168
|
+
*/
|
|
169
|
+
export const make = (
|
|
170
|
+
key: string,
|
|
171
|
+
config: DistributedMutexConfig = {}
|
|
172
|
+
): Effect.Effect<DistributedMutex, never, DistributedMutexBacking> =>
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const backing = yield* DistributedMutexBacking;
|
|
175
|
+
|
|
176
|
+
// Generate unique holder ID for this instance
|
|
177
|
+
const holderId = crypto.randomUUID();
|
|
178
|
+
|
|
179
|
+
// Resolve config with defaults
|
|
180
|
+
const ttl = config.ttl ? Duration.decode(config.ttl) : DEFAULT_TTL;
|
|
181
|
+
const ttlMs = Duration.toMillis(ttl);
|
|
182
|
+
const refreshInterval = config.refreshInterval
|
|
183
|
+
? Duration.decode(config.refreshInterval)
|
|
184
|
+
: Duration.millis(ttlMs / 3);
|
|
185
|
+
const retryInterval = config.retryInterval
|
|
186
|
+
? Duration.decode(config.retryInterval)
|
|
187
|
+
: DEFAULT_RETRY_INTERVAL;
|
|
188
|
+
const acquireTimeout = config.acquireTimeout
|
|
189
|
+
? Option.some(Duration.decode(config.acquireTimeout))
|
|
190
|
+
: Option.none<Duration.Duration>();
|
|
191
|
+
|
|
192
|
+
// Keep the lock alive by refreshing TTL periodically.
|
|
193
|
+
// This effect runs forever until interrupted (when scope closes).
|
|
194
|
+
const keepAlive = Effect.repeat(
|
|
195
|
+
Effect.gen(function* () {
|
|
196
|
+
const refreshed = yield* backing.refresh(key, holderId, ttlMs);
|
|
197
|
+
if (!refreshed) {
|
|
198
|
+
return yield* new LockLostError({ key });
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
Schedule.spaced(refreshInterval)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Try to acquire immediately, returns Option
|
|
205
|
+
const tryAcquire: Effect.Effect<
|
|
206
|
+
Option.Option<void>,
|
|
207
|
+
LockLostError | BackingError,
|
|
208
|
+
Scope.Scope
|
|
209
|
+
> = Effect.gen(function* () {
|
|
210
|
+
const acquired = yield* backing.tryAcquire(key, holderId, ttlMs);
|
|
211
|
+
if (!acquired) {
|
|
212
|
+
return Option.none();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Start keepalive fiber, tied to this scope
|
|
216
|
+
yield* Effect.forkScoped(keepAlive);
|
|
217
|
+
|
|
218
|
+
// Add finalizer to release lock when scope closes
|
|
219
|
+
yield* Effect.addFinalizer(() =>
|
|
220
|
+
backing.release(key, holderId).pipe(Effect.ignore)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return Option.some(undefined as void);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Acquire with retry/timeout, returns void when acquired
|
|
227
|
+
const acquire: Effect.Effect<
|
|
228
|
+
void,
|
|
229
|
+
AcquireTimeoutError | LockLostError | BackingError,
|
|
230
|
+
Scope.Scope
|
|
231
|
+
> = Effect.gen(function* () {
|
|
232
|
+
// Build retry schedule with optional timeout
|
|
233
|
+
const schedule = Option.match(acquireTimeout, {
|
|
234
|
+
onNone: () => Schedule.spaced(retryInterval),
|
|
235
|
+
onSome: (timeout) =>
|
|
236
|
+
Schedule.spaced(retryInterval).pipe(Schedule.upTo(timeout)),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Retry until we acquire the lock
|
|
240
|
+
yield* Effect.retry(
|
|
241
|
+
Effect.gen(function* () {
|
|
242
|
+
const maybeAcquired = yield* tryAcquire;
|
|
243
|
+
if (Option.isNone(maybeAcquired)) {
|
|
244
|
+
return yield* new NotYetAcquiredError();
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
schedule
|
|
248
|
+
).pipe(
|
|
249
|
+
Effect.catchTag(
|
|
250
|
+
"NotYetAcquiredError",
|
|
251
|
+
() =>
|
|
252
|
+
new AcquireTimeoutError({
|
|
253
|
+
key,
|
|
254
|
+
timeoutMs: Option.match(acquireTimeout, {
|
|
255
|
+
onNone: () => -1,
|
|
256
|
+
onSome: Duration.toMillis,
|
|
257
|
+
}),
|
|
258
|
+
})
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Convenience: acquire lock, run effect, release when done
|
|
264
|
+
const withLock = <A, E, R>(
|
|
265
|
+
effect: Effect.Effect<A, E, R>
|
|
266
|
+
): Effect.Effect<
|
|
267
|
+
A,
|
|
268
|
+
E | AcquireTimeoutError | LockLostError | BackingError,
|
|
269
|
+
R
|
|
270
|
+
> =>
|
|
271
|
+
Effect.scoped(
|
|
272
|
+
Effect.gen(function* () {
|
|
273
|
+
yield* acquire;
|
|
274
|
+
return yield* effect;
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Convenience: try to acquire, run effect if successful
|
|
279
|
+
const withLockIfAvailable = <A, E, R>(
|
|
280
|
+
effect: Effect.Effect<A, E, R>
|
|
281
|
+
): Effect.Effect<Option.Option<A>, E | LockLostError | BackingError, R> =>
|
|
282
|
+
Effect.scoped(
|
|
283
|
+
Effect.gen(function* () {
|
|
284
|
+
const maybeAcquired = yield* tryAcquire;
|
|
285
|
+
if (Option.isNone(maybeAcquired)) {
|
|
286
|
+
return Option.none<A>();
|
|
287
|
+
}
|
|
288
|
+
const result = yield* effect;
|
|
289
|
+
return Option.some(result);
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const isLocked: Effect.Effect<boolean, BackingError> =
|
|
294
|
+
backing.isLocked(key);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
key,
|
|
298
|
+
withLock,
|
|
299
|
+
withLockIfAvailable,
|
|
300
|
+
acquire,
|
|
301
|
+
tryAcquire,
|
|
302
|
+
isLocked,
|
|
303
|
+
} satisfies DistributedMutex;
|
|
304
|
+
});
|
package/src/Errors.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base error for all distributed mutex errors
|
|
5
|
+
*/
|
|
6
|
+
export class DistributedMutexError extends Data.TaggedError(
|
|
7
|
+
"DistributedMutexError"
|
|
8
|
+
)<{
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly cause?: unknown;
|
|
11
|
+
}> {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Failed to acquire the lock within the timeout period
|
|
15
|
+
*/
|
|
16
|
+
export class AcquireTimeoutError extends Data.TaggedError(
|
|
17
|
+
"AcquireTimeoutError"
|
|
18
|
+
)<{
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly timeoutMs: number;
|
|
21
|
+
}> {
|
|
22
|
+
get message() {
|
|
23
|
+
return `Failed to acquire lock "${this.key}" within ${this.timeoutMs}ms`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The lock was lost (TTL expired while we thought we held it)
|
|
29
|
+
*/
|
|
30
|
+
export class LockLostError extends Data.TaggedError("LockLostError")<{
|
|
31
|
+
readonly key: string;
|
|
32
|
+
}> {
|
|
33
|
+
get message() {
|
|
34
|
+
return `Lock "${this.key}" was lost (TTL expired or taken by another holder)`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Error from the backing store (Redis, etc.)
|
|
40
|
+
*/
|
|
41
|
+
export class BackingError extends Data.TaggedError("BackingError")<{
|
|
42
|
+
readonly operation: string;
|
|
43
|
+
readonly cause: unknown;
|
|
44
|
+
}> {
|
|
45
|
+
get message() {
|
|
46
|
+
return `Backing store error during "${this.operation}": ${this.cause}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal error: lock not yet acquired (used for retry logic)
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export class NotYetAcquiredError extends Data.TaggedError(
|
|
55
|
+
"NotYetAcquiredError"
|
|
56
|
+
)<{}> {}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Effect, Layer, Option } from "effect";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
import { DistributedMutexBacking } from "./DistributedMutex.js";
|
|
4
|
+
import { BackingError } from "./Errors.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lua script for atomic lock acquisition.
|
|
8
|
+
* SET key value NX PX ttl - set only if not exists, with TTL
|
|
9
|
+
*/
|
|
10
|
+
const ACQUIRE_SCRIPT = `
|
|
11
|
+
local key = KEYS[1]
|
|
12
|
+
local holderId = ARGV[1]
|
|
13
|
+
local ttlMs = tonumber(ARGV[2])
|
|
14
|
+
|
|
15
|
+
local result = redis.call('SET', key, holderId, 'NX', 'PX', ttlMs)
|
|
16
|
+
if result then
|
|
17
|
+
return 1
|
|
18
|
+
else
|
|
19
|
+
return 0
|
|
20
|
+
end
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lua script for atomic lock release.
|
|
25
|
+
* Only deletes if we are the current holder.
|
|
26
|
+
*/
|
|
27
|
+
const RELEASE_SCRIPT = `
|
|
28
|
+
local key = KEYS[1]
|
|
29
|
+
local holderId = ARGV[1]
|
|
30
|
+
|
|
31
|
+
local currentHolder = redis.call('GET', key)
|
|
32
|
+
if currentHolder == holderId then
|
|
33
|
+
redis.call('DEL', key)
|
|
34
|
+
return 1
|
|
35
|
+
else
|
|
36
|
+
return 0
|
|
37
|
+
end
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lua script for atomic TTL refresh.
|
|
42
|
+
* Only refreshes if we are the current holder.
|
|
43
|
+
*/
|
|
44
|
+
const REFRESH_SCRIPT = `
|
|
45
|
+
local key = KEYS[1]
|
|
46
|
+
local holderId = ARGV[1]
|
|
47
|
+
local ttlMs = tonumber(ARGV[2])
|
|
48
|
+
|
|
49
|
+
local currentHolder = redis.call('GET', key)
|
|
50
|
+
if currentHolder == holderId then
|
|
51
|
+
redis.call('PEXPIRE', key, ttlMs)
|
|
52
|
+
return 1
|
|
53
|
+
else
|
|
54
|
+
return 0
|
|
55
|
+
end
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a Redis-backed distributed mutex backing layer.
|
|
60
|
+
*
|
|
61
|
+
* @param redis - An ioredis client instance
|
|
62
|
+
* @param keyPrefix - Optional prefix for all keys (default: "dmutex:")
|
|
63
|
+
*/
|
|
64
|
+
export const layer = (
|
|
65
|
+
redis: Redis,
|
|
66
|
+
keyPrefix = "dmutex:"
|
|
67
|
+
): Layer.Layer<DistributedMutexBacking> => {
|
|
68
|
+
const prefixKey = (key: string) => `${keyPrefix}${key}`;
|
|
69
|
+
|
|
70
|
+
const tryAcquire = (
|
|
71
|
+
key: string,
|
|
72
|
+
holderId: string,
|
|
73
|
+
ttlMs: number
|
|
74
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
75
|
+
Effect.tryPromise({
|
|
76
|
+
try: async () => {
|
|
77
|
+
const result = await redis.eval(
|
|
78
|
+
ACQUIRE_SCRIPT,
|
|
79
|
+
1,
|
|
80
|
+
prefixKey(key),
|
|
81
|
+
holderId,
|
|
82
|
+
ttlMs.toString()
|
|
83
|
+
);
|
|
84
|
+
return result === 1;
|
|
85
|
+
},
|
|
86
|
+
catch: (cause) => new BackingError({ operation: "tryAcquire", cause }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const release = (
|
|
90
|
+
key: string,
|
|
91
|
+
holderId: string
|
|
92
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
93
|
+
Effect.tryPromise({
|
|
94
|
+
try: async () => {
|
|
95
|
+
const result = await redis.eval(
|
|
96
|
+
RELEASE_SCRIPT,
|
|
97
|
+
1,
|
|
98
|
+
prefixKey(key),
|
|
99
|
+
holderId
|
|
100
|
+
);
|
|
101
|
+
return result === 1;
|
|
102
|
+
},
|
|
103
|
+
catch: (cause) => new BackingError({ operation: "release", cause }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const refresh = (
|
|
107
|
+
key: string,
|
|
108
|
+
holderId: string,
|
|
109
|
+
ttlMs: number
|
|
110
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
111
|
+
Effect.tryPromise({
|
|
112
|
+
try: async () => {
|
|
113
|
+
const result = await redis.eval(
|
|
114
|
+
REFRESH_SCRIPT,
|
|
115
|
+
1,
|
|
116
|
+
prefixKey(key),
|
|
117
|
+
holderId,
|
|
118
|
+
ttlMs.toString()
|
|
119
|
+
);
|
|
120
|
+
return result === 1;
|
|
121
|
+
},
|
|
122
|
+
catch: (cause) => new BackingError({ operation: "refresh", cause }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const isLocked = (key: string): Effect.Effect<boolean, BackingError> =>
|
|
126
|
+
Effect.tryPromise({
|
|
127
|
+
try: async () => {
|
|
128
|
+
const exists = await redis.exists(prefixKey(key));
|
|
129
|
+
return exists === 1;
|
|
130
|
+
},
|
|
131
|
+
catch: (cause) => new BackingError({ operation: "isLocked", cause }),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const getHolder = (
|
|
135
|
+
key: string
|
|
136
|
+
): Effect.Effect<Option.Option<string>, BackingError> =>
|
|
137
|
+
Effect.tryPromise({
|
|
138
|
+
try: async () => {
|
|
139
|
+
const holder = await redis.get(prefixKey(key));
|
|
140
|
+
return holder ? Option.some(holder) : Option.none();
|
|
141
|
+
},
|
|
142
|
+
catch: (cause) => new BackingError({ operation: "getHolder", cause }),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return Layer.succeed(DistributedMutexBacking, {
|
|
146
|
+
tryAcquire,
|
|
147
|
+
release,
|
|
148
|
+
refresh,
|
|
149
|
+
isLocked,
|
|
150
|
+
getHolder,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a Redis backing from a connection URL.
|
|
156
|
+
* This creates and manages the Redis connection lifecycle.
|
|
157
|
+
*/
|
|
158
|
+
export const layerFromUrl = (
|
|
159
|
+
url: string,
|
|
160
|
+
keyPrefix = "dmutex:"
|
|
161
|
+
): Layer.Layer<DistributedMutexBacking, BackingError> =>
|
|
162
|
+
Layer.scoped(
|
|
163
|
+
DistributedMutexBacking,
|
|
164
|
+
Effect.gen(function* () {
|
|
165
|
+
// Dynamic import to avoid requiring ioredis at module load time
|
|
166
|
+
const { default: Redis } = yield* Effect.tryPromise({
|
|
167
|
+
try: () => import("ioredis"),
|
|
168
|
+
catch: (cause) =>
|
|
169
|
+
new BackingError({
|
|
170
|
+
operation: "import",
|
|
171
|
+
cause: `Failed to import ioredis: ${cause}`,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const redis = new Redis(url);
|
|
176
|
+
|
|
177
|
+
// Ensure cleanup on scope close
|
|
178
|
+
yield* Effect.addFinalizer(() =>
|
|
179
|
+
Effect.promise(() => redis.quit().catch(() => {}))
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const prefixKey = (key: string) => `${keyPrefix}${key}`;
|
|
183
|
+
|
|
184
|
+
const tryAcquire = (
|
|
185
|
+
key: string,
|
|
186
|
+
holderId: string,
|
|
187
|
+
ttlMs: number
|
|
188
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
189
|
+
Effect.tryPromise({
|
|
190
|
+
try: async () => {
|
|
191
|
+
const result = await redis.eval(
|
|
192
|
+
ACQUIRE_SCRIPT,
|
|
193
|
+
1,
|
|
194
|
+
prefixKey(key),
|
|
195
|
+
holderId,
|
|
196
|
+
ttlMs.toString()
|
|
197
|
+
);
|
|
198
|
+
return result === 1;
|
|
199
|
+
},
|
|
200
|
+
catch: (cause) =>
|
|
201
|
+
new BackingError({ operation: "tryAcquire", cause }),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const release = (
|
|
205
|
+
key: string,
|
|
206
|
+
holderId: string
|
|
207
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
208
|
+
Effect.tryPromise({
|
|
209
|
+
try: async () => {
|
|
210
|
+
const result = await redis.eval(
|
|
211
|
+
RELEASE_SCRIPT,
|
|
212
|
+
1,
|
|
213
|
+
prefixKey(key),
|
|
214
|
+
holderId
|
|
215
|
+
);
|
|
216
|
+
return result === 1;
|
|
217
|
+
},
|
|
218
|
+
catch: (cause) => new BackingError({ operation: "release", cause }),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const refresh = (
|
|
222
|
+
key: string,
|
|
223
|
+
holderId: string,
|
|
224
|
+
ttlMs: number
|
|
225
|
+
): Effect.Effect<boolean, BackingError> =>
|
|
226
|
+
Effect.tryPromise({
|
|
227
|
+
try: async () => {
|
|
228
|
+
const result = await redis.eval(
|
|
229
|
+
REFRESH_SCRIPT,
|
|
230
|
+
1,
|
|
231
|
+
prefixKey(key),
|
|
232
|
+
holderId,
|
|
233
|
+
ttlMs.toString()
|
|
234
|
+
);
|
|
235
|
+
return result === 1;
|
|
236
|
+
},
|
|
237
|
+
catch: (cause) => new BackingError({ operation: "refresh", cause }),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const isLocked = (key: string): Effect.Effect<boolean, BackingError> =>
|
|
241
|
+
Effect.tryPromise({
|
|
242
|
+
try: async () => {
|
|
243
|
+
const exists = await redis.exists(prefixKey(key));
|
|
244
|
+
return exists === 1;
|
|
245
|
+
},
|
|
246
|
+
catch: (cause) => new BackingError({ operation: "isLocked", cause }),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const getHolder = (
|
|
250
|
+
key: string
|
|
251
|
+
): Effect.Effect<Option.Option<string>, BackingError> =>
|
|
252
|
+
Effect.tryPromise({
|
|
253
|
+
try: async () => {
|
|
254
|
+
const holder = await redis.get(prefixKey(key));
|
|
255
|
+
return holder ? Option.some(holder) : Option.none();
|
|
256
|
+
},
|
|
257
|
+
catch: (cause) => new BackingError({ operation: "getHolder", cause }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
tryAcquire,
|
|
262
|
+
release,
|
|
263
|
+
refresh,
|
|
264
|
+
isLocked,
|
|
265
|
+
getHolder,
|
|
266
|
+
} satisfies DistributedMutexBacking;
|
|
267
|
+
})
|
|
268
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect Distributed Lock
|
|
3
|
+
*
|
|
4
|
+
* A distributed mutex library for Effect with pluggable backends.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { DistributedMutex, makeRedisBackingLayer } from "effect-distributed-lock";
|
|
9
|
+
* import { Effect } from "effect";
|
|
10
|
+
* import Redis from "ioredis";
|
|
11
|
+
*
|
|
12
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
13
|
+
*
|
|
14
|
+
* const program = Effect.gen(function* () {
|
|
15
|
+
* // Create a mutex for a specific resource
|
|
16
|
+
* const mutex = yield* DistributedMutex.make("my-resource-lock", {
|
|
17
|
+
* ttl: "30 seconds",
|
|
18
|
+
* acquireTimeout: "10 seconds",
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Use the lock
|
|
22
|
+
* yield* mutex.withLock(
|
|
23
|
+
* Effect.gen(function* () {
|
|
24
|
+
* // Critical section - only one process can be here at a time
|
|
25
|
+
* yield* doSomethingExclusive();
|
|
26
|
+
* })
|
|
27
|
+
* );
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* program.pipe(
|
|
31
|
+
* Effect.provide(makeRedisBackingLayer(redis)),
|
|
32
|
+
* Effect.runPromise
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Core module (namespace with types and functions)
|
|
40
|
+
export * as DistributedMutex from "./DistributedMutex.js";
|
|
41
|
+
// Errors
|
|
42
|
+
export {
|
|
43
|
+
AcquireTimeoutError,
|
|
44
|
+
BackingError,
|
|
45
|
+
DistributedMutexError,
|
|
46
|
+
LockLostError,
|
|
47
|
+
} from "./Errors.js";
|
|
48
|
+
|
|
49
|
+
// Redis backing
|
|
50
|
+
export * as RedisBacking from "./RedisBacking.js";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
|
|
15
|
+
// Build output
|
|
16
|
+
"rootDir": "src",
|
|
17
|
+
"outDir": "dist",
|
|
18
|
+
"declaration": true,
|
|
19
|
+
"declarationMap": true,
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
|
|
22
|
+
// Best practices
|
|
23
|
+
"strict": true,
|
|
24
|
+
"skipLibCheck": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true,
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
|
|
28
|
+
// Some stricter flags (disabled by default)
|
|
29
|
+
"noUnusedLocals": false,
|
|
30
|
+
"noUnusedParameters": false,
|
|
31
|
+
"noPropertyAccessFromIndexSignature": false
|
|
32
|
+
},
|
|
33
|
+
"include": ["src"],
|
|
34
|
+
"exclude": ["node_modules", "dist"]
|
|
35
|
+
}
|