@buenojs/bueno 0.8.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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,280 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { LinearRouter } from '../../src/router/linear';
3
+ import type { HTTPMethod, RouteHandler } from '../../src/types';
4
+
5
+ describe('LinearRouter', () => {
6
+ let router: LinearRouter;
7
+
8
+ const mockHandler: RouteHandler = () => new Response('OK');
9
+ const handler1: RouteHandler = () => new Response('handler1');
10
+ const handler2: RouteHandler = () => new Response('handler2');
11
+
12
+ beforeEach(() => {
13
+ router = new LinearRouter();
14
+ });
15
+
16
+ describe('Router Type', () => {
17
+ test('should return "linear" as router type', () => {
18
+ expect(router.getRouterType()).toBe('linear');
19
+ });
20
+ });
21
+
22
+ describe('Static Route Registration', () => {
23
+ test('should register a GET route', () => {
24
+ router.get('/users', mockHandler);
25
+ const match = router.match('GET', '/users');
26
+ expect(match).toBeDefined();
27
+ expect(match?.handler).toBe(mockHandler);
28
+ expect(match?.params).toEqual({});
29
+ });
30
+
31
+ test('should register a POST route', () => {
32
+ router.post('/users', mockHandler);
33
+ const match = router.match('POST', '/users');
34
+ expect(match).toBeDefined();
35
+ });
36
+
37
+ test('should register routes for all HTTP methods', () => {
38
+ router.get('/get', mockHandler);
39
+ router.post('/post', mockHandler);
40
+ router.put('/put', mockHandler);
41
+ router.patch('/patch', mockHandler);
42
+ router.delete('/delete', mockHandler);
43
+ router.head('/head', mockHandler);
44
+ router.options('/options', mockHandler);
45
+
46
+ expect(router.match('GET', '/get')).toBeDefined();
47
+ expect(router.match('POST', '/post')).toBeDefined();
48
+ expect(router.match('PUT', '/put')).toBeDefined();
49
+ expect(router.match('PATCH', '/patch')).toBeDefined();
50
+ expect(router.match('DELETE', '/delete')).toBeDefined();
51
+ expect(router.match('HEAD', '/head')).toBeDefined();
52
+ expect(router.match('OPTIONS', '/options')).toBeDefined();
53
+ });
54
+
55
+ test('should register route with all method', () => {
56
+ router.all('/catch-all', mockHandler);
57
+ expect(router.match('GET', '/catch-all')).toBeDefined();
58
+ expect(router.match('POST', '/catch-all')).toBeDefined();
59
+ expect(router.match('PUT', '/catch-all')).toBeDefined();
60
+ });
61
+
62
+ test('should be case-insensitive for static routes', () => {
63
+ router.get('/Users', mockHandler);
64
+ expect(router.match('GET', '/users')).toBeDefined();
65
+ expect(router.match('GET', '/USERS')).toBeDefined();
66
+ expect(router.match('GET', '/UsErS')).toBeDefined();
67
+ });
68
+
69
+ test('should normalize trailing slashes for static routes', () => {
70
+ router.get('/users', mockHandler);
71
+ expect(router.match('GET', '/users')).toBeDefined();
72
+ expect(router.match('GET', '/users/')).toBeDefined();
73
+ });
74
+ });
75
+
76
+ describe('Static Route Performance (O(1))', () => {
77
+ test('should match static routes in constant time', () => {
78
+ // Add multiple static routes
79
+ router.get('/users', handler1);
80
+ router.get('/posts', handler2);
81
+ router.get('/comments', mockHandler);
82
+ router.get('/tags', mockHandler);
83
+ router.get('/categories', mockHandler);
84
+
85
+ // All should match correctly regardless of order
86
+ expect(router.match('GET', '/users')?.handler).toBe(handler1);
87
+ expect(router.match('GET', '/posts')?.handler).toBe(handler2);
88
+ expect(router.match('GET', '/comments')?.handler).toBe(mockHandler);
89
+ });
90
+
91
+ test('should return empty params for static routes', () => {
92
+ router.get('/static-path', mockHandler);
93
+ const match = router.match('GET', '/static-path');
94
+ expect(match?.params).toEqual({});
95
+ });
96
+ });
97
+
98
+ describe('Dynamic Route Registration', () => {
99
+ test('should match single path parameter', () => {
100
+ router.get('/users/:id', mockHandler);
101
+ const match = router.match('GET', '/users/123');
102
+ expect(match).toBeDefined();
103
+ expect(match?.params).toEqual({ id: '123' });
104
+ });
105
+
106
+ test('should match multiple path parameters', () => {
107
+ router.get('/users/:userId/posts/:postId', mockHandler);
108
+ const match = router.match('GET', '/users/42/posts/100');
109
+ expect(match?.params).toEqual({ userId: '42', postId: '100' });
110
+ });
111
+
112
+ test('should match wildcard routes', () => {
113
+ router.get('/files/*', mockHandler);
114
+ expect(router.match('GET', '/files/path/to/file.txt')).toBeDefined();
115
+ expect(router.match('GET', '/files/')).toBeDefined();
116
+ });
117
+
118
+ test('should capture wildcard content', () => {
119
+ router.get('/files/*', mockHandler);
120
+ const match = router.match('GET', '/files/docs/readme.md');
121
+ expect(match?.params['*']).toBe('docs/readme.md');
122
+ });
123
+
124
+ test('should match optional parameters', () => {
125
+ router.get('/users/:id?', mockHandler);
126
+ expect(router.match('GET', '/users')).toBeDefined();
127
+ expect(router.match('GET', '/users/123')?.params).toEqual({ id: '123' });
128
+ });
129
+
130
+ test('should match regex patterns', () => {
131
+ router.get('/users/:id<\\d+>', mockHandler);
132
+ expect(router.match('GET', '/users/123')).toBeDefined();
133
+ expect(router.match('GET', '/users/abc')).toBeUndefined();
134
+ });
135
+ });
136
+
137
+ describe('Route Priority', () => {
138
+ test('should match static routes before dynamic (via separate storage)', () => {
139
+ // In LinearRouter, static and dynamic routes are stored separately
140
+ // Static routes are checked first (O(1) Map lookup)
141
+ router.get('/users/me', handler1);
142
+ router.get('/users/:id', handler2);
143
+
144
+ // Static route should match first
145
+ const match = router.match('GET', '/users/me');
146
+ expect(match?.handler).toBe(handler1);
147
+
148
+ // Dynamic route for other values
149
+ const match2 = router.match('GET', '/users/123');
150
+ expect(match2?.handler).toBe(handler2);
151
+ });
152
+ });
153
+
154
+ describe('Route Groups', () => {
155
+ test('should create route group with prefix', () => {
156
+ const api = router.group('/api');
157
+ api.get('/users', mockHandler);
158
+
159
+ expect(router.match('GET', '/api/users')).toBeDefined();
160
+ expect(router.match('GET', '/users')).toBeUndefined();
161
+ });
162
+
163
+ test('should support nested route groups', () => {
164
+ const api = router.group('/api');
165
+ const v1 = api.group('/v1');
166
+ v1.get('/users', mockHandler);
167
+
168
+ expect(router.match('GET', '/api/v1/users')).toBeDefined();
169
+ });
170
+
171
+ test('should apply middleware to route group', () => {
172
+ const middleware = () => new Response('middleware');
173
+ const api = router.group('/api', { middleware });
174
+ api.get('/users', mockHandler);
175
+
176
+ const match = router.match('GET', '/api/users');
177
+ expect(match?.middleware).toBeDefined();
178
+ expect(match?.middleware?.length).toBe(1);
179
+ });
180
+
181
+ test('should support group with both static and dynamic routes', () => {
182
+ const api = router.group('/api');
183
+ api.get('/users', handler1);
184
+ api.get('/users/:id', handler2);
185
+
186
+ expect(router.match('GET', '/api/users')?.handler).toBe(handler1);
187
+ expect(router.match('GET', '/api/users/123')?.handler).toBe(handler2);
188
+ });
189
+ });
190
+
191
+ describe('Route Information', () => {
192
+ test('should list all routes', () => {
193
+ router.get('/users', mockHandler);
194
+ router.post('/users', mockHandler);
195
+ router.get('/users/:id', mockHandler);
196
+
197
+ const routes = router.getRoutes();
198
+ expect(routes.length).toBe(3);
199
+ });
200
+
201
+ test('should include route metadata', () => {
202
+ router.get('/users', mockHandler, { name: 'users.list' });
203
+ const routes = router.getRoutes();
204
+ expect(routes[0].name).toBe('users.list');
205
+ });
206
+
207
+ test('should return route count breakdown', () => {
208
+ router.get('/users', mockHandler); // static
209
+ router.get('/posts', mockHandler); // static
210
+ router.get('/users/:id', mockHandler); // dynamic
211
+ router.get('/posts/:id/comments', mockHandler); // dynamic
212
+
213
+ const count = router.getRouteCount();
214
+ expect(count.static).toBe(2);
215
+ expect(count.dynamic).toBe(2);
216
+ expect(count.total).toBe(4);
217
+ });
218
+ });
219
+
220
+ describe('Middleware', () => {
221
+ test('should accept handler with middleware', () => {
222
+ const middleware = () => new Response('middleware');
223
+ router.get('/protected', mockHandler, { middleware: [middleware] });
224
+
225
+ const match = router.match('GET', '/protected');
226
+ expect(match?.middleware).toHaveLength(1);
227
+ });
228
+
229
+ test('should accept single middleware', () => {
230
+ const middleware = () => new Response('middleware');
231
+ router.get('/protected', mockHandler, { middleware });
232
+
233
+ const match = router.match('GET', '/protected');
234
+ expect(match?.middleware).toHaveLength(1);
235
+ });
236
+ });
237
+
238
+ describe('Edge Cases', () => {
239
+ test('should return undefined for non-matching route', () => {
240
+ router.get('/users', mockHandler);
241
+ expect(router.match('GET', '/posts')).toBeUndefined();
242
+ });
243
+
244
+ test('should return undefined for wrong method', () => {
245
+ router.get('/users', mockHandler);
246
+ expect(router.match('POST', '/users')).toBeUndefined();
247
+ });
248
+
249
+ test('should handle root path', () => {
250
+ router.get('/', mockHandler);
251
+ expect(router.match('GET', '/')).toBeDefined();
252
+ });
253
+
254
+ test('should handle empty optional param', () => {
255
+ router.get('/search/:query?', mockHandler);
256
+ const match = router.match('GET', '/search');
257
+ expect(match).toBeDefined();
258
+ });
259
+
260
+ test('should handle "ALL" method for dynamic routes', () => {
261
+ router.all('/api/*', mockHandler);
262
+ expect(router.match('GET', '/api/users')).toBeDefined();
263
+ expect(router.match('POST', '/api/users')).toBeDefined();
264
+ expect(router.match('DELETE', '/api/users/123')).toBeDefined();
265
+ });
266
+ });
267
+
268
+ describe('Async Handlers', () => {
269
+ test('should accept async handlers', async () => {
270
+ const asyncHandler: RouteHandler = async () => {
271
+ await Promise.resolve();
272
+ return new Response('async');
273
+ };
274
+
275
+ router.get('/async', asyncHandler);
276
+ const match = router.match('GET', '/async');
277
+ expect(match?.handler).toBe(asyncHandler);
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,336 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import {
3
+ DistributedLock,
4
+ createDistributedLock,
5
+ createMemoryLock,
6
+ LockAcquireError,
7
+ } from '../../src/lock';
8
+
9
+ describe('DistributedLock (In-Memory)', () => {
10
+ let lock: DistributedLock;
11
+
12
+ beforeEach(() => {
13
+ lock = createMemoryLock();
14
+ });
15
+
16
+ afterEach(() => {
17
+ lock.disconnect();
18
+ });
19
+
20
+ describe('Basic Lock Acquisition', () => {
21
+ test('should acquire a lock successfully', async () => {
22
+ const handle = await lock.acquire('test-key');
23
+
24
+ expect(handle.acquired).toBe(true);
25
+ expect(handle.key).toBe('lock:test-key');
26
+ expect(handle.value).toBeDefined();
27
+ expect(handle.value.length).toBe(32); // 16 bytes = 32 hex chars
28
+ });
29
+
30
+ test('should prevent concurrent access', async () => {
31
+ const handle1 = await lock.acquire('shared-key');
32
+ expect(handle1.acquired).toBe(true);
33
+
34
+ const handle2 = await lock.acquire('shared-key', { retryCount: 0 });
35
+ expect(handle2.acquired).toBe(false);
36
+ });
37
+
38
+ test('should release a lock', async () => {
39
+ const handle = await lock.acquire('release-test');
40
+ expect(handle.acquired).toBe(true);
41
+
42
+ const released = await handle.release();
43
+ expect(released).toBe(true);
44
+
45
+ // Should be able to acquire again
46
+ const handle2 = await lock.acquire('release-test');
47
+ expect(handle2.acquired).toBe(true);
48
+ });
49
+
50
+ test('should not release a lock we dont own', async () => {
51
+ const handle = await lock.acquire('ownership-test');
52
+
53
+ // Create another lock instance trying to release
54
+ const otherLock = createMemoryLock();
55
+ const otherHandle = await otherLock.acquire('ownership-test', { retryCount: 0 });
56
+
57
+ // Other lock should fail to acquire
58
+ expect(otherHandle.acquired).toBe(false);
59
+
60
+ // Other handle release should return false
61
+ const released = await otherHandle.release();
62
+ expect(released).toBe(false);
63
+
64
+ otherLock.disconnect();
65
+ });
66
+ });
67
+
68
+ describe('Lock Extension', () => {
69
+ test('should extend a lock', async () => {
70
+ const handle = await lock.acquire('extend-test', { ttl: 1000 });
71
+ expect(handle.acquired).toBe(true);
72
+
73
+ const extended = await handle.extend(2000);
74
+ expect(extended).toBe(true);
75
+
76
+ const remaining = await handle.getRemainingTTL();
77
+ expect(remaining).toBeGreaterThan(1000);
78
+ });
79
+
80
+ test('should not extend a released lock', async () => {
81
+ const handle = await lock.acquire('extend-released-test');
82
+ await handle.release();
83
+
84
+ const extended = await handle.extend(1000);
85
+ expect(extended).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('Lock Validity', () => {
90
+ test('should check if lock is valid', async () => {
91
+ const handle = await lock.acquire('validity-test');
92
+ expect(await handle.isValid()).toBe(true);
93
+
94
+ await handle.release();
95
+ expect(await handle.isValid()).toBe(false);
96
+ });
97
+
98
+ test('should get remaining TTL', async () => {
99
+ const handle = await lock.acquire('ttl-test', { ttl: 5000 });
100
+ const remaining = await handle.getRemainingTTL();
101
+
102
+ expect(remaining).toBeGreaterThan(4000);
103
+ expect(remaining).toBeLessThanOrEqual(5000);
104
+ });
105
+
106
+ test('should return -1 for invalid lock TTL', async () => {
107
+ const handle = await lock.acquire('ttl-invalid-test', { retryCount: 0 });
108
+
109
+ if (!handle.acquired) {
110
+ const remaining = await handle.getRemainingTTL();
111
+ expect(remaining).toBe(-1);
112
+ }
113
+ });
114
+ });
115
+
116
+ describe('Retry Logic', () => {
117
+ test('should retry on failure', async () => {
118
+ const handle1 = await lock.acquire('retry-test', { ttl: 100 });
119
+
120
+ // Start acquiring the same lock
121
+ const acquirePromise = lock.acquire('retry-test', {
122
+ ttl: 500,
123
+ retryCount: 5,
124
+ retryDelay: 50,
125
+ });
126
+
127
+ // Release the first lock after a short delay
128
+ setTimeout(() => handle1.release(), 80);
129
+
130
+ const handle2 = await acquirePromise;
131
+ expect(handle2.acquired).toBe(true);
132
+ });
133
+
134
+ test('should fail after max retries', async () => {
135
+ const handle1 = await lock.acquire('max-retry-test', { ttl: 10000 });
136
+
137
+ const handle2 = await lock.acquire('max-retry-test', {
138
+ ttl: 500,
139
+ retryCount: 2,
140
+ retryDelay: 10,
141
+ });
142
+
143
+ expect(handle2.acquired).toBe(false);
144
+
145
+ await handle1.release();
146
+ });
147
+ });
148
+
149
+ describe('withLock Helper', () => {
150
+ test('should execute function with lock', async () => {
151
+ const result = await lock.withLock('withlock-test', async (handle) => {
152
+ expect(handle.acquired).toBe(true);
153
+ return 'success';
154
+ });
155
+
156
+ expect(result).toBe('success');
157
+
158
+ // Lock should be released
159
+ const handle2 = await lock.acquire('withlock-test');
160
+ expect(handle2.acquired).toBe(true);
161
+ });
162
+
163
+ test('should release lock on error', async () => {
164
+ try {
165
+ await lock.withLock('error-test', async () => {
166
+ throw new Error('Test error');
167
+ });
168
+ expect(true).toBe(false); // Should not reach here
169
+ } catch (error) {
170
+ expect((error as Error).message).toBe('Test error');
171
+ }
172
+
173
+ // Lock should be released even after error
174
+ const handle = await lock.acquire('error-test');
175
+ expect(handle.acquired).toBe(true);
176
+ });
177
+
178
+ test('should throw LockAcquireError if lock fails', async () => {
179
+ const handle1 = await lock.acquire('acquire-fail-test', { ttl: 10000 });
180
+
181
+ try {
182
+ await lock.withLock('acquire-fail-test', async () => 'success', {
183
+ retryCount: 0,
184
+ });
185
+ expect(true).toBe(false); // Should not reach here
186
+ } catch (error) {
187
+ expect(error).toBeInstanceOf(LockAcquireError);
188
+ }
189
+
190
+ await handle1.release();
191
+ });
192
+ });
193
+
194
+ describe('withAutoExtend', () => {
195
+ test('should auto-extend lock for long operations', async () => {
196
+ const result = await lock.withAutoExtend(
197
+ 'autoextend-test',
198
+ async (handle) => {
199
+ // Simulate long operation
200
+ await new Promise(resolve => setTimeout(resolve, 100));
201
+
202
+ // Lock should still be valid
203
+ expect(await handle.isValid()).toBe(true);
204
+
205
+ return 'completed';
206
+ },
207
+ { ttl: 50 } // Short TTL to trigger extension
208
+ );
209
+
210
+ expect(result).toBe('completed');
211
+ });
212
+ });
213
+
214
+ describe('tryLock', () => {
215
+ test('should try to acquire without waiting', async () => {
216
+ const handle = await lock.tryLock('trylock-test');
217
+ expect(handle.acquired).toBe(true);
218
+
219
+ const handle2 = await lock.tryLock('trylock-test');
220
+ expect(handle2.acquired).toBe(false);
221
+ });
222
+ });
223
+
224
+ describe('isLocked', () => {
225
+ test('should check if a key is locked', async () => {
226
+ expect(await lock.isLocked('check-locked')).toBe(false);
227
+
228
+ const handle = await lock.acquire('check-locked');
229
+ expect(await lock.isLocked('check-locked')).toBe(true);
230
+
231
+ await handle.release();
232
+ expect(await lock.isLocked('check-locked')).toBe(false);
233
+ });
234
+ });
235
+ });
236
+
237
+ describe('DistributedLock Factory Functions', () => {
238
+ test('createDistributedLock should create memory lock by default', () => {
239
+ const lock = createDistributedLock();
240
+ expect(lock.getDriverType()).toBe('memory');
241
+ lock.disconnect();
242
+ });
243
+
244
+ test('createMemoryLock should create memory lock', () => {
245
+ const lock = createMemoryLock();
246
+ expect(lock.getDriverType()).toBe('memory');
247
+ lock.disconnect();
248
+ });
249
+ });
250
+
251
+ describe('DistributedLock Connection State', () => {
252
+ test('should be connected after initialization for memory', () => {
253
+ const lock = createMemoryLock();
254
+ expect(lock.isConnected).toBe(true);
255
+ lock.disconnect();
256
+ });
257
+
258
+ test('should not be connected after disconnect', async () => {
259
+ const lock = createMemoryLock();
260
+ await lock.disconnect();
261
+ expect(lock.isConnected).toBe(false);
262
+ });
263
+ });
264
+
265
+ describe('Default Lock Instance', () => {
266
+ test('should use default lock instance', async () => {
267
+ const { getDefaultLock, lock: withLockFn } = await import('../../src/lock');
268
+
269
+ const result = await withLockFn('default-test', async () => 'default-result');
270
+ expect(result).toBe('default-result');
271
+
272
+ const defaultLock = getDefaultLock();
273
+ expect(defaultLock).toBeDefined();
274
+ });
275
+ });
276
+
277
+ describe('Key Prefix', () => {
278
+ test('should use custom key prefix', async () => {
279
+ const lock = createDistributedLock({ keyPrefix: 'myapp:lock:' });
280
+
281
+ const handle = await lock.acquire('prefixed-key');
282
+ expect(handle.key).toBe('myapp:lock:prefixed-key');
283
+
284
+ lock.disconnect();
285
+ });
286
+ });
287
+
288
+ describe('Concurrent Access Simulation', () => {
289
+ test('should handle sequential lock attempts', async () => {
290
+ const lock = createMemoryLock();
291
+ const results: number[] = [];
292
+ const lockKey = 'sequential-test';
293
+
294
+ // Sequential execution - each waits for the previous to complete
295
+ await lock.withLock(lockKey, async () => {
296
+ results.push(1);
297
+ await new Promise(r => setTimeout(r, 20));
298
+ });
299
+
300
+ await lock.withLock(lockKey, async () => {
301
+ results.push(2);
302
+ await new Promise(r => setTimeout(r, 20));
303
+ });
304
+
305
+ await lock.withLock(lockKey, async () => {
306
+ results.push(3);
307
+ await new Promise(r => setTimeout(r, 20));
308
+ });
309
+
310
+ expect(results).toEqual([1, 2, 3]);
311
+
312
+ lock.disconnect();
313
+ });
314
+
315
+ test('should allow retry on lock contention', async () => {
316
+ const lock = createMemoryLock();
317
+ const lockKey = 'retry-contention-test';
318
+
319
+ // Acquire lock
320
+ const handle1 = await lock.acquire(lockKey, { ttl: 10000 });
321
+ expect(handle1.acquired).toBe(true);
322
+
323
+ // Try to acquire with retry - should fail initially
324
+ let acquired = false;
325
+ const acquirePromise = lock.acquire(lockKey, { retryCount: 5, retryDelay: 20 });
326
+
327
+ // Release after a short delay
328
+ setTimeout(() => handle1.release(), 50);
329
+
330
+ const handle2 = await acquirePromise;
331
+ expect(handle2.acquired).toBe(true);
332
+
333
+ await handle2.release();
334
+ lock.disconnect();
335
+ });
336
+ });