hide-a-bed 6.0.0-beta.0 → 6.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.
Files changed (107) hide show
  1. package/migration_guides/v6.md +70 -0
  2. package/package.json +4 -1
  3. package/docs/.nojekyll +0 -1
  4. package/docs/assets/hierarchy.js +0 -1
  5. package/docs/assets/highlight.css +0 -113
  6. package/docs/assets/icons.js +0 -18
  7. package/docs/assets/icons.svg +0 -1
  8. package/docs/assets/main.js +0 -60
  9. package/docs/assets/navigation.js +0 -1
  10. package/docs/assets/search.js +0 -1
  11. package/docs/assets/style.css +0 -1633
  12. package/docs/classes/QueryBuilder.html +0 -42
  13. package/docs/functions/bindConfig.html +0 -4
  14. package/docs/functions/bulkGet.html +0 -14
  15. package/docs/functions/bulkGetDictionary.html +0 -10
  16. package/docs/functions/bulkRemove.html +0 -12
  17. package/docs/functions/bulkRemoveMap.html +0 -11
  18. package/docs/functions/bulkSave.html +0 -10
  19. package/docs/functions/bulkSaveTransaction.html +0 -23
  20. package/docs/functions/createLock.html +0 -7
  21. package/docs/functions/createQuery.html +0 -1
  22. package/docs/functions/get.html +0 -1
  23. package/docs/functions/getAtRev.html +0 -1
  24. package/docs/functions/getDBInfo.html +0 -10
  25. package/docs/functions/patch.html +0 -8
  26. package/docs/functions/patchDangerously.html +0 -9
  27. package/docs/functions/put.html +0 -1
  28. package/docs/functions/query.html +0 -15
  29. package/docs/functions/queryStream.html +0 -6
  30. package/docs/functions/remove.html +0 -1
  31. package/docs/functions/removeLock.html +0 -6
  32. package/docs/functions/watchDocs.html +0 -9
  33. package/docs/functions/withRetry.html +0 -6
  34. package/docs/hierarchy.html +0 -1
  35. package/docs/index.html +0 -483
  36. package/docs/interfaces/NetworkError.html +0 -6
  37. package/docs/interfaces/NotFoundError.html +0 -10
  38. package/docs/interfaces/RetryOptions.html +0 -10
  39. package/docs/interfaces/RetryableError.html +0 -10
  40. package/docs/interfaces/StandardSchemaV1.FailureResult.html +0 -4
  41. package/docs/interfaces/StandardSchemaV1.Issue.html +0 -6
  42. package/docs/interfaces/StandardSchemaV1.Options.html +0 -3
  43. package/docs/interfaces/StandardSchemaV1.PathSegment.html +0 -4
  44. package/docs/interfaces/StandardSchemaV1.Props.html +0 -10
  45. package/docs/interfaces/StandardSchemaV1.SuccessResult.html +0 -6
  46. package/docs/interfaces/StandardSchemaV1.Types.html +0 -6
  47. package/docs/interfaces/StandardSchemaV1.html +0 -4
  48. package/docs/modules/StandardSchemaV1.html +0 -1
  49. package/docs/modules.html +0 -1
  50. package/docs/types/BoundInstance.html +0 -1
  51. package/docs/types/BulkGetBound.html +0 -2
  52. package/docs/types/BulkGetDictionaryBound.html +0 -1
  53. package/docs/types/BulkGetDictionaryOptions.html +0 -2
  54. package/docs/types/BulkGetDictionaryResult.html +0 -3
  55. package/docs/types/BulkGetOptions.html +0 -3
  56. package/docs/types/BulkGetResponse.html +0 -1
  57. package/docs/types/CouchConfig-1.html +0 -1
  58. package/docs/types/CouchConfig.html +0 -1
  59. package/docs/types/CouchConfigInput.html +0 -1
  60. package/docs/types/CouchDoc-1.html +0 -1
  61. package/docs/types/CouchDoc.html +0 -2
  62. package/docs/types/CouchDocInput.html +0 -2
  63. package/docs/types/GetAtRevBound.html +0 -1
  64. package/docs/types/GetBound.html +0 -1
  65. package/docs/types/GetOptions.html +0 -2
  66. package/docs/types/LockDoc-1.html +0 -1
  67. package/docs/types/LockDoc.html +0 -1
  68. package/docs/types/LockOptions-1.html +0 -1
  69. package/docs/types/LockOptions.html +0 -1
  70. package/docs/types/LockOptionsInput.html +0 -1
  71. package/docs/types/OnInvalidDocAction.html +0 -1
  72. package/docs/types/OnRow.html +0 -1
  73. package/docs/types/QueryBound.html +0 -1
  74. package/docs/types/SimpleViewOptions-1.html +0 -1
  75. package/docs/types/SimpleViewOptions.html +0 -1
  76. package/docs/types/StandardSchemaV1.InferInput.html +0 -2
  77. package/docs/types/StandardSchemaV1.InferOutput.html +0 -2
  78. package/docs/types/StandardSchemaV1.Result.html +0 -2
  79. package/docs/types/ViewQueryResponse-1.html +0 -1
  80. package/docs/types/ViewQueryResponse.html +0 -2
  81. package/docs/types/ViewQueryResponseValidated.html +0 -2
  82. package/docs/types/ViewRow-1.html +0 -1
  83. package/docs/types/ViewRow.html +0 -2
  84. package/docs/types/ViewRowValidated.html +0 -7
  85. package/docs/types/ViewString.html +0 -1
  86. package/docs/types/WatchOptionsInput.html +0 -1
  87. package/docs/types/WatchOptionsSchema-1.html +0 -1
  88. package/docs/types/WatchOptionsSchema.html +0 -1
  89. package/impl/bulkGet.test.mts +0 -159
  90. package/impl/bulkRemove.test.mts +0 -102
  91. package/impl/bulkSave.test.mts +0 -319
  92. package/impl/get.test.mts +0 -114
  93. package/impl/getDBInfo.test.mts +0 -62
  94. package/impl/patch.test.mts +0 -142
  95. package/impl/put.test.mts +0 -114
  96. package/impl/query.test.mts +0 -280
  97. package/impl/remove.test.mts +0 -82
  98. package/impl/retry.test.mts +0 -77
  99. package/impl/stream.test.mts +0 -205
  100. package/impl/sugar/lock.test.mts +0 -113
  101. package/impl/sugar/watch.test.mts +0 -155
  102. package/impl/utils/errors.test.mts +0 -58
  103. package/impl/utils/logger.test.mts +0 -129
  104. package/impl/utils/parseRows.test.mts +0 -183
  105. package/impl/utils/queryBuilder.test.mts +0 -83
  106. package/impl/utils/queryString.test.mts +0 -53
  107. package/index.test.mts +0 -415
@@ -1,205 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import { createServer } from 'node:http'
4
- import type { AddressInfo } from 'node:net'
5
- import { queryStream } from './stream.mts'
6
- import { bindConfig } from './bindConfig.mts'
7
-
8
- const startServer = async (handler: Parameters<typeof createServer>[0]) => {
9
- const server = createServer(handler)
10
- await new Promise<void>(resolve => server.listen(0, resolve))
11
- return server
12
- }
13
-
14
- suite('queryStream', () => {
15
- test('queryStream streams rows from chunked response', async t => {
16
- const expectedRows = [
17
- { id: 'row-1', key: 'row-1', value: { count: 1 } },
18
- { id: 'row-2', key: 'row-2', value: { count: 2 } }
19
- ]
20
-
21
- // @ts-expect-error testing server
22
- const server = await startServer((req, res) => {
23
- res.on('error', () => {})
24
- const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
25
- assert.strictEqual(req.method, 'GET')
26
- assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
27
-
28
- res.writeHead(200, { 'Content-Type': 'application/json' })
29
- const payload = JSON.stringify({
30
- total_rows: expectedRows.length,
31
- rows: expectedRows
32
- })
33
- const chunkSize = 7
34
- for (let i = 0; i < payload.length; i += chunkSize) {
35
- res.write(payload.slice(i, i + chunkSize))
36
- }
37
- res.end()
38
- })
39
-
40
- t.after(async () => {
41
- await new Promise<void>(resolve => server.close(() => resolve()))
42
- })
43
-
44
- const { port } = server.address() as AddressInfo
45
- const rows: unknown[] = []
46
-
47
- await queryStream(
48
- { couch: `http://127.0.0.1:${port}` },
49
- '_design/demo/_view/by-key',
50
- {},
51
- row => {
52
- const matchedRow = expectedRows.find(r => r.id === row.id)
53
- assert.ok(matchedRow)
54
- assert.deepStrictEqual(row, matchedRow)
55
- rows.push(row)
56
- }
57
- )
58
-
59
- assert.deepStrictEqual(rows, expectedRows)
60
- })
61
-
62
- test('queryStream works with bindConfig', async t => {
63
- const expectedRows = [
64
- { id: 'row-1', key: 'row-1', value: { count: 1 } },
65
- { id: 'row-2', key: 'row-2', value: { count: 2 } }
66
- ]
67
-
68
- // @ts-expect-error testing server
69
- const server = await startServer((req, res) => {
70
- res.on('error', () => {})
71
- const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
72
- assert.strictEqual(req.method, 'GET')
73
- assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
74
-
75
- res.writeHead(200, { 'Content-Type': 'application/json' })
76
- const payload = JSON.stringify({
77
- total_rows: expectedRows.length,
78
- rows: expectedRows
79
- })
80
- const chunkSize = 7
81
- for (let i = 0; i < payload.length; i += chunkSize) {
82
- res.write(payload.slice(i, i + chunkSize))
83
- }
84
- res.end()
85
- })
86
-
87
- t.after(async () => {
88
- await new Promise<void>(resolve => server.close(() => resolve()))
89
- })
90
-
91
- const { port } = server.address() as AddressInfo
92
-
93
- const rows: unknown[] = []
94
-
95
- const db = bindConfig({ couch: `http://127.0.0.1:${port}` })
96
-
97
- await db.queryStream('_design/demo/_view/by-key', {}, row => {
98
- const matchedRow = expectedRows.find(r => r.id === row.id)
99
- assert.ok(matchedRow)
100
- assert.deepStrictEqual(row, matchedRow)
101
- rows.push(row)
102
- })
103
-
104
- assert.deepStrictEqual(rows, expectedRows)
105
- })
106
-
107
- test('queryStream works with options chaining', async t => {
108
- const expectedRows = [
109
- { id: 'row-1', key: 'row-1', value: { count: 1 } },
110
- { id: 'row-2', key: 'row-2', value: { count: 2 } }
111
- ]
112
-
113
- // @ts-expect-error testing server
114
- const server = await startServer((req, res) => {
115
- res.on('error', () => {})
116
- const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
117
- assert.strictEqual(req.method, 'GET')
118
- assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
119
-
120
- res.writeHead(200, { 'Content-Type': 'application/json' })
121
- const payload = JSON.stringify({
122
- total_rows: expectedRows.length,
123
- rows: expectedRows
124
- })
125
- const chunkSize = 7
126
- for (let i = 0; i < payload.length; i += chunkSize) {
127
- res.write(payload.slice(i, i + chunkSize))
128
- }
129
- res.end()
130
- })
131
-
132
- t.after(async () => {
133
- await new Promise<void>(resolve => server.close(() => resolve()))
134
- })
135
-
136
- const { port } = server.address() as AddressInfo
137
- const rows: unknown[] = []
138
-
139
- const db = bindConfig({ couch: `http://127.0.0.1:${port}` })
140
-
141
- await db.options({ logger: console }).queryStream('_design/demo/_view/by-key', {}, row => {
142
- const matchedRow = expectedRows.find(r => r.id === row.id)
143
- assert.ok(matchedRow)
144
- assert.deepStrictEqual(row, matchedRow)
145
- rows.push(row)
146
- })
147
-
148
- assert.deepStrictEqual(rows, expectedRows)
149
- })
150
-
151
- test('queryStream handles empty result sets', async t => {
152
- // @ts-expect-error testing server
153
- const server = await startServer((_, res) => {
154
- res.on('error', () => {})
155
- res.writeHead(200, { 'Content-Type': 'application/json' })
156
- res.write('{"rows":[]}')
157
- res.end()
158
- })
159
-
160
- t.after(async () => {
161
- await new Promise<void>(resolve => server.close(() => resolve()))
162
- })
163
-
164
- const { port } = server.address() as AddressInfo
165
- let rowCount = 0
166
-
167
- await queryStream(
168
- { couch: `http://127.0.0.1:${port}` },
169
- '_design/demo/_view/by-key',
170
- {},
171
- () => {
172
- rowCount++
173
- }
174
- )
175
-
176
- assert.strictEqual(rowCount, 0)
177
- })
178
-
179
- test('queryStream rejects when row handler throws', async t => {
180
- // @ts-expect-error testing server
181
- const server = await startServer((_, res) => {
182
- res.on('error', () => {})
183
- res.writeHead(200, { 'Content-Type': 'application/json' })
184
- res.write('{"rows":[{"id":"broken","value":42}]}')
185
- res.end()
186
- })
187
-
188
- t.after(async () => {
189
- await new Promise<void>(resolve => server.close(() => resolve()))
190
- })
191
-
192
- const { port } = server.address() as AddressInfo
193
- const handlerError = new Error('row-failure')
194
-
195
- await assert.rejects(
196
- queryStream({ couch: `http://127.0.0.1:${port}` }, '_design/demo/_view/error', {}, () => {
197
- throw handlerError
198
- }),
199
- error => {
200
- assert.strictEqual(error, handlerError)
201
- return true
202
- }
203
- )
204
- })
205
- })
@@ -1,113 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import needle from 'needle'
4
- import type { CouchConfigInput } from '../../schema/config.mts'
5
- import { createLock, removeLock } from './lock.mts'
6
- import { TEST_DB_URL } from '../../test/setup-db.mts'
7
-
8
- const baseConfig: CouchConfigInput = {
9
- couch: TEST_DB_URL,
10
- useConsoleLogger: true
11
- }
12
-
13
- async function fetchLockDoc(docId: string) {
14
- return await needle('get', `${TEST_DB_URL}/lock-${docId}`, null, {
15
- json: true
16
- })
17
- }
18
-
19
- suite('lock', () => {
20
- test('integration with pouchdb-server', async t => {
21
- await t.test('creates a lock document when enabled', async () => {
22
- const docId = `lock-creates-${Date.now()}`
23
- const created = await createLock(baseConfig, docId, {
24
- enableLocking: true,
25
- username: 'alice'
26
- })
27
- assert.strictEqual(created, true)
28
-
29
- const response = await fetchLockDoc(docId)
30
- assert.strictEqual(response.statusCode, 200)
31
- const body = response.body as { lockedBy: string; locks: string }
32
- assert.strictEqual(body.lockedBy, 'alice')
33
- assert.strictEqual(body.locks, docId)
34
- })
35
-
36
- await t.test('returns true without writing when locking disabled', async () => {
37
- const docId = `lock-disabled-${Date.now()}`
38
- const created = await createLock(baseConfig, docId, {
39
- enableLocking: false,
40
- username: 'anyone'
41
- })
42
- assert.strictEqual(created, true)
43
-
44
- const response = await fetchLockDoc(docId)
45
- assert.strictEqual(response.statusCode, 404)
46
- })
47
-
48
- await t.test('returns false on conflict and keeps existing lock', async () => {
49
- const docId = `lock-conflict-${Date.now()}`
50
- await createLock(baseConfig, docId, {
51
- enableLocking: true,
52
- username: 'alice'
53
- })
54
- const created = await createLock(baseConfig, docId, {
55
- enableLocking: true,
56
- username: 'bob'
57
- })
58
- assert.strictEqual(created, false)
59
-
60
- const response = await fetchLockDoc(docId)
61
- assert.strictEqual(response.statusCode, 200)
62
- const body = response.body as { lockedBy: string }
63
- assert.strictEqual(body.lockedBy, 'alice')
64
- })
65
-
66
- await t.test('removes lock when owned by caller', async () => {
67
- const docId = `lock-remove-${Date.now()}`
68
- await createLock(baseConfig, docId, {
69
- enableLocking: true,
70
- username: 'alice'
71
- })
72
- await removeLock(baseConfig, docId, {
73
- enableLocking: true,
74
- username: 'alice'
75
- })
76
-
77
- const response = await fetchLockDoc(docId)
78
- assert.strictEqual(response.statusCode, 404)
79
- })
80
-
81
- await t.test('skips removal when lock owned by someone else', async () => {
82
- const docId = `lock-remove-others-${Date.now()}`
83
- await createLock(baseConfig, docId, {
84
- enableLocking: true,
85
- username: 'alice'
86
- })
87
- await removeLock(baseConfig, docId, {
88
- enableLocking: true,
89
- username: 'bob'
90
- })
91
-
92
- const response = await fetchLockDoc(docId)
93
- assert.strictEqual(response.statusCode, 200)
94
- const body = response.body as { lockedBy: string }
95
- assert.strictEqual(body.lockedBy, 'alice')
96
- })
97
-
98
- await t.test('respects disabled removal', async () => {
99
- const docId = `lock-disabled-remove-${Date.now()}`
100
- await createLock(baseConfig, docId, {
101
- enableLocking: true,
102
- username: 'alice'
103
- })
104
- await removeLock(baseConfig, docId, {
105
- enableLocking: false,
106
- username: 'alice'
107
- })
108
-
109
- const response = await fetchLockDoc(docId)
110
- assert.strictEqual(response.statusCode, 200)
111
- })
112
- })
113
- })
@@ -1,155 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import { EventEmitter } from 'node:events'
4
- import needle from 'needle'
5
- import type { CouchConfigInput } from '../../schema/config.mts'
6
- import { watchDocs } from './watch.mts'
7
-
8
- class FakeRequest extends EventEmitter {
9
- destroyed = false
10
-
11
- destroy() {
12
- this.destroyed = true
13
- this.emit('close')
14
- }
15
- }
16
-
17
- type NeedleRequest = ReturnType<typeof needle.get>
18
-
19
- const baseConfig = (): CouchConfigInput => ({
20
- couch: 'http://localhost:5984/watch-test'
21
- })
22
-
23
- const waitFor = async (predicate: () => boolean, timeoutMs = 2000, intervalMs = 10) => {
24
- const startedAt = Date.now()
25
- while (Date.now() - startedAt <= timeoutMs) {
26
- if (predicate()) return
27
- await new Promise(resolve => {
28
- setTimeout(resolve, intervalMs)
29
- })
30
- }
31
- throw new Error('waitFor timed out')
32
- }
33
-
34
- suite('watchDocs', () => {
35
- test('requires at least one document id', () => {
36
- assert.throws(() => {
37
- watchDocs(baseConfig(), [], () => {})
38
- }, /non-empty array/)
39
- })
40
-
41
- test('rejects more than 100 document ids', () => {
42
- const ids = Array.from({ length: 101 }, (_, index) => `doc-${index}`)
43
- assert.throws(() => {
44
- watchDocs(baseConfig(), ids, () => {})
45
- }, /100 or fewer elements/)
46
- })
47
-
48
- test('emits change events for streamed chunks', async t => {
49
- const requests: FakeRequest[] = []
50
- const getMock = t.mock.method(needle, 'get', () => {
51
- const request = new FakeRequest()
52
- requests.push(request)
53
- return request as unknown as NeedleRequest
54
- })
55
- t.after(() => {
56
- getMock.mock.restore()
57
- })
58
-
59
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
- const changes: any[] = []
61
- const watcher = watchDocs(
62
- baseConfig(),
63
- ['doc-a', 'doc-b'],
64
- change => {
65
- changes.push(change)
66
- },
67
- { include_docs: true }
68
- )
69
-
70
- await waitFor(() => requests.length === 1)
71
- const firstRequest = requests[0]
72
-
73
- firstRequest.emit('data', Buffer.from('{"id":"doc-a","seq":"1"}\n{"id":"doc-b","seq":"2"}\n'))
74
-
75
- await waitFor(() => changes.length === 2)
76
-
77
- assert.deepStrictEqual(
78
- changes.map(change => change.id),
79
- ['doc-a', 'doc-b']
80
- )
81
- const firstArg = getMock.mock.calls[0].arguments[0]
82
- if (typeof firstArg !== 'string') {
83
- throw new Error('Expected first argument to be a string')
84
- }
85
- assert.match(firstArg, /include_docs=true/)
86
- assert.match(firstArg, /doc_ids=\["doc-a","doc-b"\]/)
87
-
88
- watcher.stop()
89
- })
90
-
91
- test('reconnects after retryable response status', async t => {
92
- const requests: FakeRequest[] = []
93
- const getMock = t.mock.method(needle, 'get', () => {
94
- const request = new FakeRequest()
95
- requests.push(request)
96
- return request as unknown as NeedleRequest
97
- })
98
- t.after(() => {
99
- getMock.mock.restore()
100
- })
101
-
102
- const watcher = watchDocs(baseConfig(), 'doc-retry', () => {}, {
103
- initialDelay: 1,
104
- maxDelay: 1,
105
- maxRetries: 3
106
- })
107
-
108
- await waitFor(() => requests.length === 1)
109
- const firstRequest = requests[0]
110
- firstRequest.emit('response', { statusCode: 503 })
111
-
112
- await waitFor(() => requests.length === 2)
113
-
114
- assert.ok(firstRequest.destroyed)
115
- assert.strictEqual(getMock.mock.callCount(), 2)
116
-
117
- watcher.stop()
118
- })
119
-
120
- test('emits error after exhausting retries', async t => {
121
- const requests: FakeRequest[] = []
122
- const getMock = t.mock.method(needle, 'get', () => {
123
- const request = new FakeRequest()
124
- requests.push(request)
125
- return request as unknown as NeedleRequest
126
- })
127
- t.after(() => {
128
- getMock.mock.restore()
129
- })
130
-
131
- const errors: Error[] = []
132
- const watcher = watchDocs(baseConfig(), 'doc-max', () => {}, {
133
- maxRetries: 2,
134
- initialDelay: 1,
135
- maxDelay: 1
136
- })
137
- watcher.on('error', err => {
138
- errors.push(err as unknown as Error)
139
- })
140
-
141
- await waitFor(() => requests.length === 1)
142
- requests[0].emit('error', { code: 'ECONNRESET' })
143
-
144
- await waitFor(() => requests.length === 2)
145
- requests[1].emit('error', { code: 'ECONNRESET' })
146
-
147
- await waitFor(() => requests.length === 3)
148
- requests[2].emit('error', { code: 'ECONNRESET' })
149
-
150
- await waitFor(() => errors.length === 1)
151
- assert.strictEqual(errors[0].message, 'Max retries reached')
152
-
153
- watcher.stop()
154
- })
155
- })
@@ -1,58 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import { NotFoundError, RetryableError, isConflictError } from './errors.mts'
4
-
5
- suite('errors', () => {
6
- test('NotFoundError exposes docId and message', () => {
7
- const err = new NotFoundError('doc-123')
8
- assert.strictEqual(err.name, 'NotFoundError')
9
- assert.strictEqual(err.message, 'Document not found')
10
- assert.strictEqual(err.docId, 'doc-123')
11
- })
12
-
13
- test('NotFoundError accepts custom message', () => {
14
- const err = new NotFoundError('doc-456', 'missing doc')
15
- assert.strictEqual(err.message, 'missing doc')
16
- })
17
-
18
- test('RetryableError.isRetryableStatusCode identifies retryable statuses', () => {
19
- const retryable = [408, 429, 500, 502, 503, 504]
20
- for (const status of retryable) {
21
- assert.strictEqual(RetryableError.isRetryableStatusCode(status), true)
22
- }
23
- assert.strictEqual(RetryableError.isRetryableStatusCode(404), false)
24
- assert.strictEqual(RetryableError.isRetryableStatusCode(undefined), false)
25
- })
26
-
27
- test('handleNetworkError wraps known network failures', () => {
28
- assert.throws(
29
- () => RetryableError.handleNetworkError({ code: 'ECONNRESET' }),
30
- (err: unknown) =>
31
- err instanceof RetryableError &&
32
- err.statusCode === 503 &&
33
- err.message.includes('ECONNRESET')
34
- )
35
- })
36
-
37
- test('handleNetworkError rethrows unknown errors', () => {
38
- const original = new Error('boom')
39
- assert.throws(
40
- () => RetryableError.handleNetworkError(original),
41
- (err: unknown) => err === original
42
- )
43
- })
44
-
45
- test('handleNetworkError rethrows unrecognized network codes', () => {
46
- const networkErr = { code: 'UNKNOWN' }
47
- assert.throws(
48
- () => RetryableError.handleNetworkError(networkErr),
49
- (err: unknown) => err === networkErr
50
- )
51
- })
52
-
53
- test('isConflictError detects statusCode 409', () => {
54
- assert.strictEqual(isConflictError({ statusCode: 409 }), true)
55
- assert.strictEqual(isConflictError({ statusCode: 412 }), false)
56
- assert.strictEqual(isConflictError(null), false)
57
- })
58
- })
@@ -1,129 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import type { CouchConfigInput } from '../../schema/config.mts'
4
- import { createLogger, type Logger } from './logger.mts'
5
-
6
- const baseConfig = (): CouchConfigInput => ({
7
- couch: 'http://localhost:5984'
8
- })
9
-
10
- suite('createLogger', () => {
11
- test('returns cached logger when present', () => {
12
- const cached: Logger = {
13
- error: () => {},
14
- warn: () => {},
15
- info: () => {},
16
- debug: () => {}
17
- }
18
- const config: CouchConfigInput = {
19
- ...baseConfig(),
20
- '~normalizedLogger': cached
21
- }
22
-
23
- const logger = createLogger(config)
24
-
25
- assert.strictEqual(logger, cached)
26
- })
27
-
28
- test('uses console logger when requested', () => {
29
- const errorCalls: unknown[][] = []
30
- const warnCalls: unknown[][] = []
31
- const infoCalls: unknown[][] = []
32
- const debugCalls: unknown[][] = []
33
-
34
- const originalError = console.error
35
- const originalWarn = console.warn
36
- const originalInfo = console.info
37
- const originalDebug = console.debug
38
-
39
- console.error = (...args: unknown[]) => {
40
- errorCalls.push(args)
41
- }
42
- console.warn = (...args: unknown[]) => {
43
- warnCalls.push(args)
44
- }
45
- console.info = (...args: unknown[]) => {
46
- infoCalls.push(args)
47
- }
48
- console.debug = (...args: unknown[]) => {
49
- debugCalls.push(args)
50
- }
51
-
52
- try {
53
- const config: CouchConfigInput = {
54
- ...baseConfig(),
55
- useConsoleLogger: true
56
- }
57
-
58
- const logger = createLogger(config)
59
-
60
- logger.error('boom')
61
- logger.warn('warn', 123)
62
- logger.info('info')
63
- logger.debug('debug')
64
-
65
- assert.strictEqual(config['~normalizedLogger'], logger)
66
- assert.deepStrictEqual(errorCalls, [['boom']])
67
- assert.deepStrictEqual(warnCalls, [['warn', 123]])
68
- assert.deepStrictEqual(infoCalls, [['info']])
69
- assert.deepStrictEqual(debugCalls, [['debug']])
70
- } finally {
71
- console.error = originalError
72
- console.warn = originalWarn
73
- console.info = originalInfo
74
- console.debug = originalDebug
75
- }
76
- })
77
-
78
- test('creates no-op logger when none provided', () => {
79
- const config = baseConfig()
80
- const logger = createLogger(config)
81
-
82
- assert.strictEqual(config['~normalizedLogger'], logger)
83
- assert.doesNotThrow(() => logger.error('noop'))
84
- assert.doesNotThrow(() => logger.warn('noop'))
85
- assert.doesNotThrow(() => logger.info('noop'))
86
- assert.doesNotThrow(() => logger.debug('noop'))
87
- })
88
-
89
- test('wraps function logger', () => {
90
- const calls: Array<{ level: string; args: unknown[] }> = []
91
- const fnLogger = (level: string, ...args: unknown[]) => {
92
- calls.push({ level, args })
93
- }
94
-
95
- const config: CouchConfigInput = {
96
- ...baseConfig(),
97
- logger: fnLogger
98
- }
99
-
100
- const logger = createLogger(config)
101
- logger.info('hello', 42)
102
- logger.error('problem')
103
-
104
- assert.deepStrictEqual(calls, [
105
- { level: 'info', args: ['hello', 42] },
106
- { level: 'error', args: ['problem'] }
107
- ])
108
- })
109
-
110
- test('fills missing methods on object logger', () => {
111
- let warnCount = 0
112
- const config: CouchConfigInput = {
113
- ...baseConfig(),
114
- logger: {
115
- warn: () => {
116
- warnCount++
117
- }
118
- }
119
- }
120
-
121
- const logger = createLogger(config)
122
- logger.warn('watch out')
123
-
124
- assert.strictEqual(warnCount, 1)
125
- assert.doesNotThrow(() => logger.error('ignored'))
126
- assert.doesNotThrow(() => logger.info('ignored'))
127
- assert.doesNotThrow(() => logger.debug('ignored'))
128
- })
129
- })