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.
- package/migration_guides/v6.md +70 -0
- package/package.json +4 -1
- package/docs/.nojekyll +0 -1
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -113
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/QueryBuilder.html +0 -42
- package/docs/functions/bindConfig.html +0 -4
- package/docs/functions/bulkGet.html +0 -14
- package/docs/functions/bulkGetDictionary.html +0 -10
- package/docs/functions/bulkRemove.html +0 -12
- package/docs/functions/bulkRemoveMap.html +0 -11
- package/docs/functions/bulkSave.html +0 -10
- package/docs/functions/bulkSaveTransaction.html +0 -23
- package/docs/functions/createLock.html +0 -7
- package/docs/functions/createQuery.html +0 -1
- package/docs/functions/get.html +0 -1
- package/docs/functions/getAtRev.html +0 -1
- package/docs/functions/getDBInfo.html +0 -10
- package/docs/functions/patch.html +0 -8
- package/docs/functions/patchDangerously.html +0 -9
- package/docs/functions/put.html +0 -1
- package/docs/functions/query.html +0 -15
- package/docs/functions/queryStream.html +0 -6
- package/docs/functions/remove.html +0 -1
- package/docs/functions/removeLock.html +0 -6
- package/docs/functions/watchDocs.html +0 -9
- package/docs/functions/withRetry.html +0 -6
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -483
- package/docs/interfaces/NetworkError.html +0 -6
- package/docs/interfaces/NotFoundError.html +0 -10
- package/docs/interfaces/RetryOptions.html +0 -10
- package/docs/interfaces/RetryableError.html +0 -10
- package/docs/interfaces/StandardSchemaV1.FailureResult.html +0 -4
- package/docs/interfaces/StandardSchemaV1.Issue.html +0 -6
- package/docs/interfaces/StandardSchemaV1.Options.html +0 -3
- package/docs/interfaces/StandardSchemaV1.PathSegment.html +0 -4
- package/docs/interfaces/StandardSchemaV1.Props.html +0 -10
- package/docs/interfaces/StandardSchemaV1.SuccessResult.html +0 -6
- package/docs/interfaces/StandardSchemaV1.Types.html +0 -6
- package/docs/interfaces/StandardSchemaV1.html +0 -4
- package/docs/modules/StandardSchemaV1.html +0 -1
- package/docs/modules.html +0 -1
- package/docs/types/BoundInstance.html +0 -1
- package/docs/types/BulkGetBound.html +0 -2
- package/docs/types/BulkGetDictionaryBound.html +0 -1
- package/docs/types/BulkGetDictionaryOptions.html +0 -2
- package/docs/types/BulkGetDictionaryResult.html +0 -3
- package/docs/types/BulkGetOptions.html +0 -3
- package/docs/types/BulkGetResponse.html +0 -1
- package/docs/types/CouchConfig-1.html +0 -1
- package/docs/types/CouchConfig.html +0 -1
- package/docs/types/CouchConfigInput.html +0 -1
- package/docs/types/CouchDoc-1.html +0 -1
- package/docs/types/CouchDoc.html +0 -2
- package/docs/types/CouchDocInput.html +0 -2
- package/docs/types/GetAtRevBound.html +0 -1
- package/docs/types/GetBound.html +0 -1
- package/docs/types/GetOptions.html +0 -2
- package/docs/types/LockDoc-1.html +0 -1
- package/docs/types/LockDoc.html +0 -1
- package/docs/types/LockOptions-1.html +0 -1
- package/docs/types/LockOptions.html +0 -1
- package/docs/types/LockOptionsInput.html +0 -1
- package/docs/types/OnInvalidDocAction.html +0 -1
- package/docs/types/OnRow.html +0 -1
- package/docs/types/QueryBound.html +0 -1
- package/docs/types/SimpleViewOptions-1.html +0 -1
- package/docs/types/SimpleViewOptions.html +0 -1
- package/docs/types/StandardSchemaV1.InferInput.html +0 -2
- package/docs/types/StandardSchemaV1.InferOutput.html +0 -2
- package/docs/types/StandardSchemaV1.Result.html +0 -2
- package/docs/types/ViewQueryResponse-1.html +0 -1
- package/docs/types/ViewQueryResponse.html +0 -2
- package/docs/types/ViewQueryResponseValidated.html +0 -2
- package/docs/types/ViewRow-1.html +0 -1
- package/docs/types/ViewRow.html +0 -2
- package/docs/types/ViewRowValidated.html +0 -7
- package/docs/types/ViewString.html +0 -1
- package/docs/types/WatchOptionsInput.html +0 -1
- package/docs/types/WatchOptionsSchema-1.html +0 -1
- package/docs/types/WatchOptionsSchema.html +0 -1
- package/impl/bulkGet.test.mts +0 -159
- package/impl/bulkRemove.test.mts +0 -102
- package/impl/bulkSave.test.mts +0 -319
- package/impl/get.test.mts +0 -114
- package/impl/getDBInfo.test.mts +0 -62
- package/impl/patch.test.mts +0 -142
- package/impl/put.test.mts +0 -114
- package/impl/query.test.mts +0 -280
- package/impl/remove.test.mts +0 -82
- package/impl/retry.test.mts +0 -77
- package/impl/stream.test.mts +0 -205
- package/impl/sugar/lock.test.mts +0 -113
- package/impl/sugar/watch.test.mts +0 -155
- package/impl/utils/errors.test.mts +0 -58
- package/impl/utils/logger.test.mts +0 -129
- package/impl/utils/parseRows.test.mts +0 -183
- package/impl/utils/queryBuilder.test.mts +0 -83
- package/impl/utils/queryString.test.mts +0 -53
- package/index.test.mts +0 -415
package/impl/stream.test.mts
DELETED
|
@@ -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
|
-
})
|
package/impl/sugar/lock.test.mts
DELETED
|
@@ -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
|
-
})
|