@lowerdeck/lock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+
2
+ $ microbundle
3
+ No name was provided for external module '@lowerdeck/redis' in output.globals – guessing 'redis'
4
+ Build "@lowerdeck/lock" to dist:
5
+ 1418 B: index.cjs.gz
6
+ 1275 B: index.cjs.br
7
+ 654 B: index.module.js.gz
8
+ 592 B: index.module.js.br
9
+ 1390 B: index.module.js.gz
10
+ 1263 B: index.module.js.br
11
+ 1509 B: index.umd.js.gz
12
+ 1371 B: index.umd.js.br
@@ -0,0 +1,11 @@
1
+
2
+ $ vitest run --passWithNoTests
3
+ [?25l
4
+  RUN  v3.2.4 /Users/tobias/code/metorial/metorial-enterprise/oss/src/packages/backend/lock
5
+
6
+ No test files found, exiting with code 0
7
+
8
+ include: **/*.{test,spec}.?(c|m)[jt]s?(x)
9
+ exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
10
+
11
+ [?25h
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # `@lowerdeck/lock`
2
+
3
+ Distributed locking using Redlock for Redis. Ensures atomic operations across multiple instances with automatic lock extension and retry logic.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @lowerdeck/lock
9
+ yarn add @lowerdeck/lock
10
+ bun add @lowerdeck/lock
11
+ pnpm add @lowerdeck/lock
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import { createLock } from '@lowerdeck/lock';
18
+
19
+ const lock = createLock({
20
+ name: 'my-service',
21
+ redisUrl: 'redis://localhost:6379'
22
+ });
23
+
24
+ // Use lock to ensure only one instance runs a critical section
25
+ await lock.usingLock('resource-key', async ({ passForNow }) => {
26
+ console.log('Inside critical section');
27
+
28
+ // If you want to pass on this execution and retry later
29
+ if (shouldSkip) {
30
+ passForNow();
31
+ return;
32
+ }
33
+
34
+ await performCriticalOperation();
35
+ });
36
+
37
+ // Ensure a function runs only once across all instances
38
+ await lock.doOnce('unique-task-id', async () => {
39
+ console.log('This will run on only one instance');
40
+ await oneTimeSetup();
41
+ });
42
+ // Other instances will wait and return null
43
+
44
+ // Run once and share the result with other instances
45
+ const result = await lock.doOnceAndReturn('compute-key', async () => {
46
+ console.log('Computing expensive result once');
47
+ return await expensiveComputation();
48
+ });
49
+ // First instance computes and caches, others get cached result
50
+
51
+ // Lock multiple resources at once
52
+ await lock.usingLock(['user:123', 'account:456'], async () => {
53
+ await transferFunds();
54
+ });
55
+ ```
56
+
57
+ ## License
58
+
59
+ This project is licensed under the Apache License 2.0.
60
+
61
+ <div align="center">
62
+ <sub>Built with ❤️ by <a href="https://metorial.com">Metorial</a></sub>
63
+ </div>
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ var n=require("@lowerdeck/redis"),e=require("ioredis"),r=require("superjson"),t=require("redlock");function i(n){return n&&"object"==typeof n&&"default"in n?n:{default:n}}var u=/*#__PURE__*/i(r),o=/*#__PURE__*/i(t),c=function(n){return new Promise(function(e){return setTimeout(e,n)})};function f(n,e,r){if(!n.s){if(r instanceof s){if(!r.s)return void(r.o=f.bind(null,n,e));1&e&&(e=r.s),r=r.v}if(r&&r.then)return void r.then(f.bind(null,n,e),f.bind(null,n,2));n.s=e,n.v=r;const t=n.o;t&&t(n)}}var s=/*#__PURE__*/function(){function n(){}return n.prototype.then=function(e,r){var t=new n,i=this.s;if(i){var u=1&i?e:r;if(u){try{f(t,1,u(this.v))}catch(n){f(t,2,n)}return t}return this}return this.o=function(n){try{var i=n.v;1&n.s?f(t,1,e?e(i):i):r?f(t,1,r(i)):f(t,2,i)}catch(n){f(t,2,n)}},t},n}();function h(n){return n instanceof s&&1&n.s}exports.createLock=function(r){var t=r.redisUrl,i=Bun.hash.cityHash32(r.name),a=new e.Redis(n.parseRedisUrl(t)),v=new o.default([a],{driftFactor:.01,retryCount:50,retryDelay:200,retryJitter:200,automaticExtensionThreshold:500}),l=function(n,e){try{var r=(Array.isArray(n)?n:[n]).map(function(n){return"l:"+i+":"+n}),t=function(){try{var n=!1,i=function(){n=!0};return Promise.resolve(v.using(r,1e4,function(){return e({passForNow:i})})).then(function(e){var r,i=function(){if(n)return Promise.resolve(c(100)).then(function(){var n=t();return r=1,n})}();return i&&i.then?i.then(function(n){return r?n:e}):r?i:e})}catch(n){return Promise.reject(n)}};return t()}catch(n){return Promise.reject(n)}},d=function(n,e){try{var r=i+":"+Math.random()+":"+Date.now(),t="luniq:"+i+":"+n;return Promise.resolve(a.setnx(t,r)).then(function(i){return Promise.resolve(a.expire(t,300)).then(function(){var u;function o(n){return u?n:Promise.resolve(a.get(t)).then(function(n){var e;if(!n)return null;var r=0,t=function(n,e,r){for(var t;;){var i=n();if(h(i)&&(i=i.v),!i)return u;if(i.then){t=0;break}var u=r();if(u&&u.then){if(!h(u)){t=1;break}u=u.s}if(e){var o=e();if(o&&o.then&&!h(o)){t=2;break}}}var c=new s,a=f.bind(null,c,2);return(0===t?i.then(l):1===t?u.then(v):o.then(d)).then(void 0,a),c;function v(t){u=t;do{if(e&&(o=e())&&o.then&&!h(o))return void o.then(d).then(void 0,a);if(!(i=n())||h(i)&&!i.v)return void f(c,1,u);if(i.then)return void i.then(l).then(void 0,a);h(u=r())&&(u=u.v)}while(!u||!u.then);u.then(v).then(void 0,a)}function l(n){n?(u=r())&&u.then?u.then(v).then(void 0,a):v(u):f(c,1,u)}function d(){(i=n())?i.then?i.then(l).then(void 0,a):l(i):f(c,1,u)}}(function(){return!e&&r<25},function(){return r++},function(){function t(){return Promise.resolve(a.get("luniq:"+n+":done")).then(function(n){if(n)return e=1,null})}var i=function(){if(r>0)return Promise.resolve(c(25)).then(function(){})}();return i&&i.then?i.then(t):t()});return t&&t.then?t.then(function(n){return e?n:null}):e?t:null})}var v=function(){if(i)return Promise.resolve(l(n,function(){try{return Promise.resolve(function(n,e){try{var r=n()}catch(n){return e(!0,n)}return r&&r.then?r.then(e.bind(null,!1),e.bind(null,!0)):e(!1,r)}(e,function(n,e){return Promise.resolve(a.set("luniq:"+r+":done","1","EX",10)).then(function(){return Promise.resolve(a.expire(t,5)).then(function(){if(n)throw e;return e})})}))}catch(n){return Promise.reject(n)}})).then(function(n){return u=1,n})}();return v&&v.then?v.then(o):o(v)})})}catch(n){return Promise.reject(n)}};return{usingLock:l,doOnce:d,doOnceAndReturn:function(n,e){var r="doa:"+i+":"+n;return Promise.resolve(d(n,function(){try{return Promise.resolve(e()).then(function(n){return Promise.resolve(a.set(r,u.default.stringify(n),"EX",60)).then(function(){return n})})}catch(n){return Promise.reject(n)}})).then(function(n){var e,t=function(){if(null==n){var t=u.default.parse;return Promise.resolve(a.get(r)).then(function(n){var r=t.call(u.default,n);return e=1,r})}}();return t&&t.then?t.then(function(r){return e?r:n}):e?t:n})}}};
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../delay/dist/index.module.js","../src/index.ts"],"sourcesContent":["var e=function(e){return new Promise(function(n){return setTimeout(n,e)})};export{e as delay};\n//# sourceMappingURL=index.module.js.map\n","import { delay } from '@lowerdeck/delay';\nimport { parseRedisUrl } from '@lowerdeck/redis';\nimport { Redis } from 'ioredis';\n\n// @ts-ignore\nimport SuperJSON from 'superjson';\n\n// @ts-ignore\nimport Redlock_ from 'redlock';\n\nexport let createLock = ({ name, redisUrl }: { name: string; redisUrl: string }) => {\n let nameHash = Bun.hash.cityHash32(name);\n let redis = new Redis(parseRedisUrl(redisUrl));\n\n let redlock = new Redlock_([redis as any], {\n // The expected clock drift; for more details see:\n // http://redis.io/topics/distlock\n driftFactor: 0.01, // multiplied by lock ttl to determine drift time\n\n // The max number of times Redlock will attempt to lock a resource\n // before erroring.\n retryCount: 50,\n\n // the time in ms between attempts\n retryDelay: 200, // time in ms\n\n // the max time in ms randomly added to retries\n // to improve performance under high contention\n // see https://www.awsarchitectureblog.com/2015/03/backoff.html\n retryJitter: 200, // time in ms\n\n // The minimum remaining time on a lock before an extension is automatically\n // attempted with the `using` API.\n automaticExtensionThreshold: 500\n } as any);\n\n let usingLock = async <T>(\n key: string | string[],\n fn: (controller: { passForNow: () => void }) => Promise<T>\n ): Promise<T> => {\n let keyArray = (Array.isArray(key) ? key : [key]).map(k => `l:${nameHash}:${k}`);\n\n let runLock = async () => {\n let passingForNow = false;\n let passForNow = () => {\n passingForNow = true;\n };\n\n let result = await redlock.using(keyArray, 10_000, () => fn({ passForNow }));\n\n if (passingForNow) {\n await delay(100);\n return runLock();\n }\n\n return result;\n };\n\n return runLock();\n };\n\n let doOnce = async <T>(key: string, fn: () => Promise<T>): Promise<T | null> => {\n let id = `${nameHash}:${Math.random()}:${Date.now()}`;\n let uniquenessHashKey = `luniq:${nameHash}:${key}`;\n\n let keyWasSet = await redis.setnx(uniquenessHashKey, id);\n await redis.expire(uniquenessHashKey, 60 * 5);\n\n if (keyWasSet) {\n return await usingLock(key, async () => {\n try {\n return await fn();\n } finally {\n await redis.set(`luniq:${id}:done`, '1', 'EX', 10);\n await redis.expire(uniquenessHashKey, 5);\n }\n });\n }\n\n let winnerId = await redis.get(uniquenessHashKey);\n if (!winnerId) return null;\n\n // If we lost, we'll wait for the winner to finish.\n for (let i = 0; i < 25; i++) {\n if (i > 0) await delay(25);\n\n if (await redis.get(`luniq:${winnerId}:done`)) {\n return null;\n }\n }\n\n return null;\n };\n\n let doOnceAndReturn = async <T>(key: string, fn: () => Promise<T>): Promise<T> => {\n let redisKey = `doa:${nameHash}:${key}`;\n\n let res = await doOnce(key, async () => {\n let res = await fn();\n\n await redis.set(redisKey, SuperJSON.stringify(res), 'EX', 60);\n\n return res;\n });\n\n if (res == null) {\n return SuperJSON.parse((await redis.get(redisKey)) as string);\n }\n\n return res;\n };\n\n return {\n usingLock,\n doOnce,\n doOnceAndReturn\n };\n};\n"],"names":["e","Promise","n","setTimeout","_settle","state","value","pact","s","_Pact","o","v","then","bind","observer","prototype","onFulfilled","onRejected","result","this","callback","_this","_isSettledPact","thenable","_ref","redisUrl","nameHash","Bun","hash","cityHash32","name","redis","Redis","parseRedisUrl","redlock","Redlock_","driftFactor","retryCount","retryDelay","retryJitter","automaticExtensionThreshold","usingLock","key","fn","keyArray","Array","isArray","map","k","runLock","passingForNow","passForNow","resolve","using","_exit","_temp","delay","_runLock","_result","reject","doOnce","id","Math","random","Date","now","uniquenessHashKey","setnx","keyWasSet","expire","_exit2","_temp7","_result2","get","winnerId","_exit3","i","_temp5","_for","_temp4","_redis$get","_temp3","_result3","_temp6","_finallyRethrows","_wasThrown","_result4","set","_await$usingLock","doOnceAndReturn","redisKey","res","SuperJSON","stringify","_exit4","_temp8","_parse","parse","_redis$get2","_SuperJSON$parse","call","_result5"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export declare let createLock: ({ name, redisUrl }: {
2
+ name: string;
3
+ redisUrl: string;
4
+ }) => {
5
+ usingLock: <T>(key: string | string[], fn: (controller: {
6
+ passForNow: () => void;
7
+ }) => Promise<T>) => Promise<T>;
8
+ doOnce: <T>(key: string, fn: () => Promise<T>) => Promise<T | null>;
9
+ doOnceAndReturn: <T>(key: string, fn: () => Promise<T>) => Promise<T>;
10
+ };
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,IAAI,UAAU,GAAI,oBAAoB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;gBA0BtD,CAAC,OACjB,MAAM,GAAG,MAAM,EAAE,MAClB,CAAC,UAAU,EAAE;QAAE,UAAU,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,OAAO,CAAC,CAAC,CAAC,KACzD,OAAO,CAAC,CAAC,CAAC;aAsBO,CAAC,OAAO,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;sBAiC/C,CAAC,OAAO,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC;CAuB/E,CAAC"}
@@ -0,0 +1,2 @@
1
+ import{parseRedisUrl as n}from"@lowerdeck/redis";import{Redis as r}from"ioredis";import t from"superjson";import e from"redlock";var i=function(n){return new Promise(function(r){return setTimeout(r,n)})};function o(n,r,t){if(!n.s){if(t instanceof u){if(!t.s)return void(t.o=o.bind(null,n,r));1&r&&(r=t.s),t=t.v}if(t&&t.then)return void t.then(o.bind(null,n,r),o.bind(null,n,2));n.s=r,n.v=t;const e=n.o;e&&e(n)}}var u=/*#__PURE__*/function(){function n(){}return n.prototype.then=function(r,t){var e=new n,i=this.s;if(i){var u=1&i?r:t;if(u){try{o(e,1,u(this.v))}catch(n){o(e,2,n)}return e}return this}return this.o=function(n){try{var i=n.v;1&n.s?o(e,1,r?r(i):i):t?o(e,1,t(i)):o(e,2,i)}catch(n){o(e,2,n)}},e},n}();function c(n){return n instanceof u&&1&n.s}var f=function(f){var h=f.redisUrl,s=Bun.hash.cityHash32(f.name),v=new r(n(h)),a=new e([v],{driftFactor:.01,retryCount:50,retryDelay:200,retryJitter:200,automaticExtensionThreshold:500}),l=function(n,r){try{var t=(Array.isArray(n)?n:[n]).map(function(n){return"l:"+s+":"+n}),e=function(){try{var n=!1,o=function(){n=!0};return Promise.resolve(a.using(t,1e4,function(){return r({passForNow:o})})).then(function(r){var t,o=function(){if(n)return Promise.resolve(i(100)).then(function(){var n=e();return t=1,n})}();return o&&o.then?o.then(function(n){return t?n:r}):t?o:r})}catch(n){return Promise.reject(n)}};return e()}catch(n){return Promise.reject(n)}},m=function(n,r){try{var t=s+":"+Math.random()+":"+Date.now(),e="luniq:"+s+":"+n;return Promise.resolve(v.setnx(e,t)).then(function(f){return Promise.resolve(v.expire(e,300)).then(function(){var h;function s(n){return h?n:Promise.resolve(v.get(e)).then(function(n){var r;if(!n)return null;var t=0,e=function(n,r,t){for(var e;;){var i=n();if(c(i)&&(i=i.v),!i)return f;if(i.then){e=0;break}var f=t();if(f&&f.then){if(!c(f)){e=1;break}f=f.s}if(r){var h=r();if(h&&h.then&&!c(h)){e=2;break}}}var s=new u,v=o.bind(null,s,2);return(0===e?i.then(l):1===e?f.then(a):h.then(m)).then(void 0,v),s;function a(e){f=e;do{if(r&&(h=r())&&h.then&&!c(h))return void h.then(m).then(void 0,v);if(!(i=n())||c(i)&&!i.v)return void o(s,1,f);if(i.then)return void i.then(l).then(void 0,v);c(f=t())&&(f=f.v)}while(!f||!f.then);f.then(a).then(void 0,v)}function l(n){n?(f=t())&&f.then?f.then(a).then(void 0,v):a(f):o(s,1,f)}function m(){(i=n())?i.then?i.then(l).then(void 0,v):l(i):o(s,1,f)}}(function(){return!r&&t<25},function(){return t++},function(){function e(){return Promise.resolve(v.get("luniq:"+n+":done")).then(function(n){if(n)return r=1,null})}var o=function(){if(t>0)return Promise.resolve(i(25)).then(function(){})}();return o&&o.then?o.then(e):e()});return e&&e.then?e.then(function(n){return r?n:null}):r?e:null})}var a=function(){if(f)return Promise.resolve(l(n,function(){try{return Promise.resolve(function(n,r){try{var t=n()}catch(n){return r(!0,n)}return t&&t.then?t.then(r.bind(null,!1),r.bind(null,!0)):r(!1,t)}(r,function(n,r){return Promise.resolve(v.set("luniq:"+t+":done","1","EX",10)).then(function(){return Promise.resolve(v.expire(e,5)).then(function(){if(n)throw r;return r})})}))}catch(n){return Promise.reject(n)}})).then(function(n){return h=1,n})}();return a&&a.then?a.then(s):s(a)})})}catch(n){return Promise.reject(n)}};return{usingLock:l,doOnce:m,doOnceAndReturn:function(n,r){var e="doa:"+s+":"+n;return Promise.resolve(m(n,function(){try{return Promise.resolve(r()).then(function(n){return Promise.resolve(v.set(e,t.stringify(n),"EX",60)).then(function(){return n})})}catch(n){return Promise.reject(n)}})).then(function(n){var r,i=function(){if(null==n){var i=t.parse;return Promise.resolve(v.get(e)).then(function(n){var e=i.call(t,n);return r=1,e})}}();return i&&i.then?i.then(function(t){return r?t:n}):r?i:n})}}};export{f as createLock};
2
+ //# sourceMappingURL=index.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.module.js","sources":["../../delay/dist/index.module.js","../src/index.ts"],"sourcesContent":["var e=function(e){return new Promise(function(n){return setTimeout(n,e)})};export{e as delay};\n//# sourceMappingURL=index.module.js.map\n","import { delay } from '@lowerdeck/delay';\nimport { parseRedisUrl } from '@lowerdeck/redis';\nimport { Redis } from 'ioredis';\n\n// @ts-ignore\nimport SuperJSON from 'superjson';\n\n// @ts-ignore\nimport Redlock_ from 'redlock';\n\nexport let createLock = ({ name, redisUrl }: { name: string; redisUrl: string }) => {\n let nameHash = Bun.hash.cityHash32(name);\n let redis = new Redis(parseRedisUrl(redisUrl));\n\n let redlock = new Redlock_([redis as any], {\n // The expected clock drift; for more details see:\n // http://redis.io/topics/distlock\n driftFactor: 0.01, // multiplied by lock ttl to determine drift time\n\n // The max number of times Redlock will attempt to lock a resource\n // before erroring.\n retryCount: 50,\n\n // the time in ms between attempts\n retryDelay: 200, // time in ms\n\n // the max time in ms randomly added to retries\n // to improve performance under high contention\n // see https://www.awsarchitectureblog.com/2015/03/backoff.html\n retryJitter: 200, // time in ms\n\n // The minimum remaining time on a lock before an extension is automatically\n // attempted with the `using` API.\n automaticExtensionThreshold: 500\n } as any);\n\n let usingLock = async <T>(\n key: string | string[],\n fn: (controller: { passForNow: () => void }) => Promise<T>\n ): Promise<T> => {\n let keyArray = (Array.isArray(key) ? key : [key]).map(k => `l:${nameHash}:${k}`);\n\n let runLock = async () => {\n let passingForNow = false;\n let passForNow = () => {\n passingForNow = true;\n };\n\n let result = await redlock.using(keyArray, 10_000, () => fn({ passForNow }));\n\n if (passingForNow) {\n await delay(100);\n return runLock();\n }\n\n return result;\n };\n\n return runLock();\n };\n\n let doOnce = async <T>(key: string, fn: () => Promise<T>): Promise<T | null> => {\n let id = `${nameHash}:${Math.random()}:${Date.now()}`;\n let uniquenessHashKey = `luniq:${nameHash}:${key}`;\n\n let keyWasSet = await redis.setnx(uniquenessHashKey, id);\n await redis.expire(uniquenessHashKey, 60 * 5);\n\n if (keyWasSet) {\n return await usingLock(key, async () => {\n try {\n return await fn();\n } finally {\n await redis.set(`luniq:${id}:done`, '1', 'EX', 10);\n await redis.expire(uniquenessHashKey, 5);\n }\n });\n }\n\n let winnerId = await redis.get(uniquenessHashKey);\n if (!winnerId) return null;\n\n // If we lost, we'll wait for the winner to finish.\n for (let i = 0; i < 25; i++) {\n if (i > 0) await delay(25);\n\n if (await redis.get(`luniq:${winnerId}:done`)) {\n return null;\n }\n }\n\n return null;\n };\n\n let doOnceAndReturn = async <T>(key: string, fn: () => Promise<T>): Promise<T> => {\n let redisKey = `doa:${nameHash}:${key}`;\n\n let res = await doOnce(key, async () => {\n let res = await fn();\n\n await redis.set(redisKey, SuperJSON.stringify(res), 'EX', 60);\n\n return res;\n });\n\n if (res == null) {\n return SuperJSON.parse((await redis.get(redisKey)) as string);\n }\n\n return res;\n };\n\n return {\n usingLock,\n doOnce,\n doOnceAndReturn\n };\n};\n"],"names":["e","Promise","n","setTimeout","_settle","state","value","pact","s","_Pact","o","v","then","bind","observer","prototype","onFulfilled","onRejected","result","this","callback","_this","_isSettledPact","thenable","createLock","_ref","redisUrl","nameHash","Bun","hash","cityHash32","name","redis","Redis","parseRedisUrl","redlock","Redlock_","driftFactor","retryCount","retryDelay","retryJitter","automaticExtensionThreshold","usingLock","key","fn","keyArray","Array","isArray","map","k","runLock","passingForNow","passForNow","resolve","using","_exit","_temp","delay","_runLock","_result","reject","doOnce","id","Math","random","Date","now","uniquenessHashKey","setnx","keyWasSet","expire","_exit2","_temp7","_result2","get","winnerId","_exit3","i","_temp5","_for","_temp4","_redis$get","_temp3","_result3","_temp6","_finallyRethrows","_wasThrown","_result4","set","_await$usingLock","doOnceAndReturn","redisKey","res","SuperJSON","stringify","_exit4","_temp8","_parse","parse","_redis$get2","_SuperJSON$parse","call","_result5"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@lowerdeck/redis"),require("ioredis"),require("superjson"),require("redlock")):"function"==typeof define&&define.amd?define(["exports","@lowerdeck/redis","ioredis","superjson","redlock"],e):e((n||self).lock={},n.redis,n.ioredis,n.superjson,n.redlock)}(this,function(n,e,r,t,i){function o(n){return n&&"object"==typeof n&&"default"in n?n:{default:n}}var u=/*#__PURE__*/o(t),f=/*#__PURE__*/o(i),c=function(n){return new Promise(function(e){return setTimeout(e,n)})};function s(n,e,r){if(!n.s){if(r instanceof h){if(!r.s)return void(r.o=s.bind(null,n,e));1&e&&(e=r.s),r=r.v}if(r&&r.then)return void r.then(s.bind(null,n,e),s.bind(null,n,2));n.s=e,n.v=r;const t=n.o;t&&t(n)}}var h=/*#__PURE__*/function(){function n(){}return n.prototype.then=function(e,r){var t=new n,i=this.s;if(i){var o=1&i?e:r;if(o){try{s(t,1,o(this.v))}catch(n){s(t,2,n)}return t}return this}return this.o=function(n){try{var i=n.v;1&n.s?s(t,1,e?e(i):i):r?s(t,1,r(i)):s(t,2,i)}catch(n){s(t,2,n)}},t},n}();function a(n){return n instanceof h&&1&n.s}n.createLock=function(n){var t=n.redisUrl,i=Bun.hash.cityHash32(n.name),o=new r.Redis(e.parseRedisUrl(t)),l=new f.default([o],{driftFactor:.01,retryCount:50,retryDelay:200,retryJitter:200,automaticExtensionThreshold:500}),v=function(n,e){try{var r=(Array.isArray(n)?n:[n]).map(function(n){return"l:"+i+":"+n}),t=function(){try{var n=!1,i=function(){n=!0};return Promise.resolve(l.using(r,1e4,function(){return e({passForNow:i})})).then(function(e){var r,i=function(){if(n)return Promise.resolve(c(100)).then(function(){var n=t();return r=1,n})}();return i&&i.then?i.then(function(n){return r?n:e}):r?i:e})}catch(n){return Promise.reject(n)}};return t()}catch(n){return Promise.reject(n)}},d=function(n,e){try{var r=i+":"+Math.random()+":"+Date.now(),t="luniq:"+i+":"+n;return Promise.resolve(o.setnx(t,r)).then(function(i){return Promise.resolve(o.expire(t,300)).then(function(){var u;function f(n){return u?n:Promise.resolve(o.get(t)).then(function(n){var e;if(!n)return null;var r=0,t=function(n,e,r){for(var t;;){var i=n();if(a(i)&&(i=i.v),!i)return o;if(i.then){t=0;break}var o=r();if(o&&o.then){if(!a(o)){t=1;break}o=o.s}if(e){var u=e();if(u&&u.then&&!a(u)){t=2;break}}}var f=new h,c=s.bind(null,f,2);return(0===t?i.then(v):1===t?o.then(l):u.then(d)).then(void 0,c),f;function l(t){o=t;do{if(e&&(u=e())&&u.then&&!a(u))return void u.then(d).then(void 0,c);if(!(i=n())||a(i)&&!i.v)return void s(f,1,o);if(i.then)return void i.then(v).then(void 0,c);a(o=r())&&(o=o.v)}while(!o||!o.then);o.then(l).then(void 0,c)}function v(n){n?(o=r())&&o.then?o.then(l).then(void 0,c):l(o):s(f,1,o)}function d(){(i=n())?i.then?i.then(v).then(void 0,c):v(i):s(f,1,o)}}(function(){return!e&&r<25},function(){return r++},function(){function t(){return Promise.resolve(o.get("luniq:"+n+":done")).then(function(n){if(n)return e=1,null})}var i=function(){if(r>0)return Promise.resolve(c(25)).then(function(){})}();return i&&i.then?i.then(t):t()});return t&&t.then?t.then(function(n){return e?n:null}):e?t:null})}var l=function(){if(i)return Promise.resolve(v(n,function(){try{return Promise.resolve(function(n,e){try{var r=n()}catch(n){return e(!0,n)}return r&&r.then?r.then(e.bind(null,!1),e.bind(null,!0)):e(!1,r)}(e,function(n,e){return Promise.resolve(o.set("luniq:"+r+":done","1","EX",10)).then(function(){return Promise.resolve(o.expire(t,5)).then(function(){if(n)throw e;return e})})}))}catch(n){return Promise.reject(n)}})).then(function(n){return u=1,n})}();return l&&l.then?l.then(f):f(l)})})}catch(n){return Promise.reject(n)}};return{usingLock:v,doOnce:d,doOnceAndReturn:function(n,e){var r="doa:"+i+":"+n;return Promise.resolve(d(n,function(){try{return Promise.resolve(e()).then(function(n){return Promise.resolve(o.set(r,u.default.stringify(n),"EX",60)).then(function(){return n})})}catch(n){return Promise.reject(n)}})).then(function(n){var e,t=function(){if(null==n){var t=u.default.parse;return Promise.resolve(o.get(r)).then(function(n){var r=t.call(u.default,n);return e=1,r})}}();return t&&t.then?t.then(function(r){return e?r:n}):e?t:n})}}}});
2
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../../delay/dist/index.module.js","../src/index.ts"],"sourcesContent":["var e=function(e){return new Promise(function(n){return setTimeout(n,e)})};export{e as delay};\n//# sourceMappingURL=index.module.js.map\n","import { delay } from '@lowerdeck/delay';\nimport { parseRedisUrl } from '@lowerdeck/redis';\nimport { Redis } from 'ioredis';\n\n// @ts-ignore\nimport SuperJSON from 'superjson';\n\n// @ts-ignore\nimport Redlock_ from 'redlock';\n\nexport let createLock = ({ name, redisUrl }: { name: string; redisUrl: string }) => {\n let nameHash = Bun.hash.cityHash32(name);\n let redis = new Redis(parseRedisUrl(redisUrl));\n\n let redlock = new Redlock_([redis as any], {\n // The expected clock drift; for more details see:\n // http://redis.io/topics/distlock\n driftFactor: 0.01, // multiplied by lock ttl to determine drift time\n\n // The max number of times Redlock will attempt to lock a resource\n // before erroring.\n retryCount: 50,\n\n // the time in ms between attempts\n retryDelay: 200, // time in ms\n\n // the max time in ms randomly added to retries\n // to improve performance under high contention\n // see https://www.awsarchitectureblog.com/2015/03/backoff.html\n retryJitter: 200, // time in ms\n\n // The minimum remaining time on a lock before an extension is automatically\n // attempted with the `using` API.\n automaticExtensionThreshold: 500\n } as any);\n\n let usingLock = async <T>(\n key: string | string[],\n fn: (controller: { passForNow: () => void }) => Promise<T>\n ): Promise<T> => {\n let keyArray = (Array.isArray(key) ? key : [key]).map(k => `l:${nameHash}:${k}`);\n\n let runLock = async () => {\n let passingForNow = false;\n let passForNow = () => {\n passingForNow = true;\n };\n\n let result = await redlock.using(keyArray, 10_000, () => fn({ passForNow }));\n\n if (passingForNow) {\n await delay(100);\n return runLock();\n }\n\n return result;\n };\n\n return runLock();\n };\n\n let doOnce = async <T>(key: string, fn: () => Promise<T>): Promise<T | null> => {\n let id = `${nameHash}:${Math.random()}:${Date.now()}`;\n let uniquenessHashKey = `luniq:${nameHash}:${key}`;\n\n let keyWasSet = await redis.setnx(uniquenessHashKey, id);\n await redis.expire(uniquenessHashKey, 60 * 5);\n\n if (keyWasSet) {\n return await usingLock(key, async () => {\n try {\n return await fn();\n } finally {\n await redis.set(`luniq:${id}:done`, '1', 'EX', 10);\n await redis.expire(uniquenessHashKey, 5);\n }\n });\n }\n\n let winnerId = await redis.get(uniquenessHashKey);\n if (!winnerId) return null;\n\n // If we lost, we'll wait for the winner to finish.\n for (let i = 0; i < 25; i++) {\n if (i > 0) await delay(25);\n\n if (await redis.get(`luniq:${winnerId}:done`)) {\n return null;\n }\n }\n\n return null;\n };\n\n let doOnceAndReturn = async <T>(key: string, fn: () => Promise<T>): Promise<T> => {\n let redisKey = `doa:${nameHash}:${key}`;\n\n let res = await doOnce(key, async () => {\n let res = await fn();\n\n await redis.set(redisKey, SuperJSON.stringify(res), 'EX', 60);\n\n return res;\n });\n\n if (res == null) {\n return SuperJSON.parse((await redis.get(redisKey)) as string);\n }\n\n return res;\n };\n\n return {\n usingLock,\n doOnce,\n doOnceAndReturn\n };\n};\n"],"names":["e","Promise","n","setTimeout","_settle","state","value","pact","s","_Pact","o","v","then","bind","observer","prototype","onFulfilled","onRejected","result","this","callback","_this","_isSettledPact","thenable","_ref","redisUrl","nameHash","Bun","hash","cityHash32","name","redis","Redis","parseRedisUrl","redlock","Redlock_","driftFactor","retryCount","retryDelay","retryJitter","automaticExtensionThreshold","usingLock","key","fn","keyArray","Array","isArray","map","k","runLock","passingForNow","passForNow","resolve","using","_exit","_temp","delay","_runLock","_result","reject","doOnce","id","Math","random","Date","now","uniquenessHashKey","setnx","keyWasSet","expire","_exit2","_temp7","_result2","get","winnerId","_exit3","i","_temp5","_for","_temp4","_redis$get","_temp3","_result3","_temp6","_finallyRethrows","_wasThrown","_result4","set","_await$usingLock","doOnceAndReturn","redisKey","res","SuperJSON","stringify","_exit4","_temp8","_parse","parse","_redis$get2","_SuperJSON$parse","call","_result5"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@lowerdeck/lock",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "author": "Tobias Herber",
8
+ "license": "Apache 2",
9
+ "type": "module",
10
+ "source": "src/index.ts",
11
+ "exports": {
12
+ "types": "./dist/index.d.ts",
13
+ "require": "./dist/index.cjs",
14
+ "import": "./dist/index.module.js",
15
+ "default": "./dist/index.modern.js"
16
+ },
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.module.js",
19
+ "types": "dist/index.d.ts",
20
+ "unpkg": "./dist/index.umd.js",
21
+ "scripts": {
22
+ "test": "vitest run --passWithNoTests",
23
+ "lint": "prettier src/**/*.ts --check",
24
+ "build": "microbundle"
25
+ },
26
+ "dependencies": {
27
+ "@lowerdeck/redis": "^1.0.0",
28
+ "@lowerdeck/delay": "^1.0.0",
29
+ "@types/bun": "^1.2.11",
30
+ "ioredis": "^5.4.1",
31
+ "redlock": "^5.0.0-beta.2",
32
+ "superjson": "^2.2.5"
33
+ },
34
+ "devDependencies": {
35
+ "@lowerdeck/tsconfig": "^1.0.0",
36
+ "typescript": "5.8.2",
37
+ "microbundle": "^0.15.1",
38
+ "vitest": "^3.1.2"
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { delay } from '@lowerdeck/delay';
2
+ import { parseRedisUrl } from '@lowerdeck/redis';
3
+ import { Redis } from 'ioredis';
4
+
5
+ // @ts-ignore
6
+ import SuperJSON from 'superjson';
7
+
8
+ // @ts-ignore
9
+ import Redlock_ from 'redlock';
10
+
11
+ export let createLock = ({ name, redisUrl }: { name: string; redisUrl: string }) => {
12
+ let nameHash = Bun.hash.cityHash32(name);
13
+ let redis = new Redis(parseRedisUrl(redisUrl));
14
+
15
+ let redlock = new Redlock_([redis as any], {
16
+ // The expected clock drift; for more details see:
17
+ // http://redis.io/topics/distlock
18
+ driftFactor: 0.01, // multiplied by lock ttl to determine drift time
19
+
20
+ // The max number of times Redlock will attempt to lock a resource
21
+ // before erroring.
22
+ retryCount: 50,
23
+
24
+ // the time in ms between attempts
25
+ retryDelay: 200, // time in ms
26
+
27
+ // the max time in ms randomly added to retries
28
+ // to improve performance under high contention
29
+ // see https://www.awsarchitectureblog.com/2015/03/backoff.html
30
+ retryJitter: 200, // time in ms
31
+
32
+ // The minimum remaining time on a lock before an extension is automatically
33
+ // attempted with the `using` API.
34
+ automaticExtensionThreshold: 500
35
+ } as any);
36
+
37
+ let usingLock = async <T>(
38
+ key: string | string[],
39
+ fn: (controller: { passForNow: () => void }) => Promise<T>
40
+ ): Promise<T> => {
41
+ let keyArray = (Array.isArray(key) ? key : [key]).map(k => `l:${nameHash}:${k}`);
42
+
43
+ let runLock = async () => {
44
+ let passingForNow = false;
45
+ let passForNow = () => {
46
+ passingForNow = true;
47
+ };
48
+
49
+ let result = await redlock.using(keyArray, 10_000, () => fn({ passForNow }));
50
+
51
+ if (passingForNow) {
52
+ await delay(100);
53
+ return runLock();
54
+ }
55
+
56
+ return result;
57
+ };
58
+
59
+ return runLock();
60
+ };
61
+
62
+ let doOnce = async <T>(key: string, fn: () => Promise<T>): Promise<T | null> => {
63
+ let id = `${nameHash}:${Math.random()}:${Date.now()}`;
64
+ let uniquenessHashKey = `luniq:${nameHash}:${key}`;
65
+
66
+ let keyWasSet = await redis.setnx(uniquenessHashKey, id);
67
+ await redis.expire(uniquenessHashKey, 60 * 5);
68
+
69
+ if (keyWasSet) {
70
+ return await usingLock(key, async () => {
71
+ try {
72
+ return await fn();
73
+ } finally {
74
+ await redis.set(`luniq:${id}:done`, '1', 'EX', 10);
75
+ await redis.expire(uniquenessHashKey, 5);
76
+ }
77
+ });
78
+ }
79
+
80
+ let winnerId = await redis.get(uniquenessHashKey);
81
+ if (!winnerId) return null;
82
+
83
+ // If we lost, we'll wait for the winner to finish.
84
+ for (let i = 0; i < 25; i++) {
85
+ if (i > 0) await delay(25);
86
+
87
+ if (await redis.get(`luniq:${winnerId}:done`)) {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ return null;
93
+ };
94
+
95
+ let doOnceAndReturn = async <T>(key: string, fn: () => Promise<T>): Promise<T> => {
96
+ let redisKey = `doa:${nameHash}:${key}`;
97
+
98
+ let res = await doOnce(key, async () => {
99
+ let res = await fn();
100
+
101
+ await redis.set(redisKey, SuperJSON.stringify(res), 'EX', 60);
102
+
103
+ return res;
104
+ });
105
+
106
+ if (res == null) {
107
+ return SuperJSON.parse((await redis.get(redisKey)) as string);
108
+ }
109
+
110
+ return res;
111
+ };
112
+
113
+ return {
114
+ usingLock,
115
+ doOnce,
116
+ doOnceAndReturn
117
+ };
118
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@lowerdeck/tsconfig/base.json",
4
+ "exclude": ["dist"],
5
+ "include": ["src"],
6
+ "compilerOptions": {
7
+ "outDir": "dist",
8
+ "lib": ["es2021"],
9
+ "target": "ES2019"
10
+ }
11
+ }