odac 1.4.7 → 1.4.8
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/CHANGELOG.md +17 -0
- package/client/odac.js +1 -1
- package/docs/ai/README.md +1 -1
- package/docs/ai/skills/SKILL.md +1 -1
- package/docs/ai/skills/backend/database.md +103 -12
- package/docs/ai/skills/backend/ipc.md +71 -12
- package/docs/backend/08-database/05-write-behind-cache.md +230 -0
- package/docs/backend/13-utilities/02-ipc.md +117 -0
- package/package.json +1 -1
- package/src/Database/WriteBuffer.js +605 -0
- package/src/Database.js +32 -1
- package/src/Ipc.js +343 -81
- package/src/Odac.js +2 -1
- package/src/Storage.js +4 -2
- package/test/Database/WriteBuffer/_recoverFromCheckpoint.test.js +207 -0
- package/test/Database/WriteBuffer/buffer.test.js +143 -0
- package/test/Database/WriteBuffer/flush.test.js +192 -0
- package/test/Database/WriteBuffer/get.test.js +72 -0
- package/test/Database/WriteBuffer/increment.test.js +118 -0
- package/test/Database/WriteBuffer/update.test.js +178 -0
- package/test/Ipc/hset.test.js +59 -0
- package/test/Ipc/incrBy.test.js +65 -0
- package/test/Ipc/lock.test.js +62 -0
- package/test/Ipc/rpush.test.js +68 -0
- package/test/Ipc/sadd.test.js +68 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests WriteBuffer.update() — last-write-wins coalescing.
|
|
7
|
+
* Why: Validates that repeated updates to the same row collapse into one UPDATE query,
|
|
8
|
+
* and that different rows/tables are isolated correctly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let knexLib, db, WriteBuffer
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
|
|
16
|
+
knexLib = require('knex')
|
|
17
|
+
db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
|
|
18
|
+
|
|
19
|
+
await db.schema.createTable('posts', table => {
|
|
20
|
+
table.integer('id').primary()
|
|
21
|
+
table.integer('views').defaultTo(0)
|
|
22
|
+
table.string('title', 255)
|
|
23
|
+
table.string('slug', 255)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await db('posts').insert([
|
|
27
|
+
{id: 1, views: 100, title: 'First Post', slug: 'first-post'},
|
|
28
|
+
{id: 2, views: 200, title: 'Second Post', slug: 'second-post'}
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
32
|
+
|
|
33
|
+
const Ipc = require('../../../src/Ipc')
|
|
34
|
+
global.Odac = {
|
|
35
|
+
Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
|
|
36
|
+
Storage: {
|
|
37
|
+
isReady: () => false,
|
|
38
|
+
put: jest.fn(),
|
|
39
|
+
remove: jest.fn(),
|
|
40
|
+
getRange: () => []
|
|
41
|
+
},
|
|
42
|
+
Ipc
|
|
43
|
+
}
|
|
44
|
+
await Ipc.init()
|
|
45
|
+
|
|
46
|
+
WriteBuffer = require('../../../src/Database/WriteBuffer')
|
|
47
|
+
await WriteBuffer.init({default: db})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
await WriteBuffer.close()
|
|
52
|
+
await Odac.Ipc.close()
|
|
53
|
+
await db.destroy()
|
|
54
|
+
delete global.Odac
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('WriteBuffer - update()', () => {
|
|
58
|
+
it('should buffer and flush a single update', async () => {
|
|
59
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Updated Title'})
|
|
60
|
+
await WriteBuffer.flush()
|
|
61
|
+
|
|
62
|
+
const row = await db('posts').where({id: 1}).first()
|
|
63
|
+
expect(row.title).toBe('Updated Title')
|
|
64
|
+
expect(row.slug).toBe('first-post') // Untouched
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should merge multiple updates (last-write-wins)', async () => {
|
|
68
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'First Update'})
|
|
69
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Second Update'})
|
|
70
|
+
await WriteBuffer.flush()
|
|
71
|
+
|
|
72
|
+
const row = await db('posts').where({id: 1}).first()
|
|
73
|
+
expect(row.title).toBe('Second Update')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should merge different columns from multiple updates', async () => {
|
|
77
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'New Title'})
|
|
78
|
+
await WriteBuffer.update('default', 'posts', 1, {slug: 'new-slug'})
|
|
79
|
+
await WriteBuffer.flush()
|
|
80
|
+
|
|
81
|
+
const row = await db('posts').where({id: 1}).first()
|
|
82
|
+
expect(row.title).toBe('New Title')
|
|
83
|
+
expect(row.slug).toBe('new-slug')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should handle different rows independently', async () => {
|
|
87
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Row 1'})
|
|
88
|
+
await WriteBuffer.update('default', 'posts', 2, {title: 'Row 2'})
|
|
89
|
+
await WriteBuffer.flush()
|
|
90
|
+
|
|
91
|
+
const row1 = await db('posts').where({id: 1}).first()
|
|
92
|
+
const row2 = await db('posts').where({id: 2}).first()
|
|
93
|
+
expect(row1.title).toBe('Row 1')
|
|
94
|
+
expect(row2.title).toBe('Row 2')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should handle composite where key', async () => {
|
|
98
|
+
await db.schema.createTable('user_prefs', table => {
|
|
99
|
+
table.string('pref_key', 50)
|
|
100
|
+
table.integer('user_id')
|
|
101
|
+
table.string('value', 255)
|
|
102
|
+
table.primary(['pref_key', 'user_id'])
|
|
103
|
+
})
|
|
104
|
+
await db('user_prefs').insert({pref_key: 'theme', user_id: 1, value: 'light'})
|
|
105
|
+
|
|
106
|
+
await WriteBuffer.update('default', 'user_prefs', {pref_key: 'theme', user_id: 1}, {value: 'dark'})
|
|
107
|
+
await WriteBuffer.flush()
|
|
108
|
+
|
|
109
|
+
const row = await db('user_prefs').where({pref_key: 'theme', user_id: 1}).first()
|
|
110
|
+
expect(row.value).toBe('dark')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should return true when buffered', async () => {
|
|
114
|
+
const result = await WriteBuffer.update('default', 'posts', 1, {title: 'Test'})
|
|
115
|
+
expect(result).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should clear update index after successful flush', async () => {
|
|
119
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Test'})
|
|
120
|
+
await WriteBuffer.flush()
|
|
121
|
+
|
|
122
|
+
const remaining = await Odac.Ipc.smembers('wb:idx:updates')
|
|
123
|
+
expect(remaining).toHaveLength(0)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should combine increment and update on same row during flush', async () => {
|
|
127
|
+
await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
|
|
128
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Combo Test'})
|
|
129
|
+
await WriteBuffer.flush()
|
|
130
|
+
|
|
131
|
+
const row = await db('posts').where({id: 1}).first()
|
|
132
|
+
expect(row.views).toBe(105)
|
|
133
|
+
expect(row.title).toBe('Combo Test')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should handle different tables independently', async () => {
|
|
137
|
+
await db.schema.createTable('comments', table => {
|
|
138
|
+
table.integer('id').primary()
|
|
139
|
+
table.string('body', 255)
|
|
140
|
+
})
|
|
141
|
+
await db('comments').insert({id: 1, body: 'Original'})
|
|
142
|
+
|
|
143
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Post Updated'})
|
|
144
|
+
await WriteBuffer.update('default', 'comments', 1, {body: 'Comment Updated'})
|
|
145
|
+
await WriteBuffer.flush()
|
|
146
|
+
|
|
147
|
+
const post = await db('posts').where({id: 1}).first()
|
|
148
|
+
const comment = await db('comments').where({id: 1}).first()
|
|
149
|
+
expect(post.title).toBe('Post Updated')
|
|
150
|
+
expect(comment.body).toBe('Comment Updated')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should scope flush to specific table when provided', async () => {
|
|
154
|
+
await db.schema.createTable('comments', table => {
|
|
155
|
+
table.integer('id').primary()
|
|
156
|
+
table.string('body', 255)
|
|
157
|
+
})
|
|
158
|
+
await db('comments').insert({id: 1, body: 'Original'})
|
|
159
|
+
|
|
160
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Post Updated'})
|
|
161
|
+
await WriteBuffer.update('default', 'comments', 1, {body: 'Comment Updated'})
|
|
162
|
+
|
|
163
|
+
// Flush only posts
|
|
164
|
+
await WriteBuffer.flush('default', 'posts')
|
|
165
|
+
|
|
166
|
+
const post = await db('posts').where({id: 1}).first()
|
|
167
|
+
const comment = await db('comments').where({id: 1}).first()
|
|
168
|
+
expect(post.title).toBe('Post Updated')
|
|
169
|
+
expect(comment.body).toBe('Original') // Not flushed
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should not modify DB before flush is called', async () => {
|
|
173
|
+
await WriteBuffer.update('default', 'posts', 1, {title: 'Buffered Only'})
|
|
174
|
+
|
|
175
|
+
const row = await db('posts').where({id: 1}).first()
|
|
176
|
+
expect(row.title).toBe('First Post') // Still original
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests Ipc hash operations: hset() and hgetall().
|
|
7
|
+
* Why: Validates that hash merge semantics work correctly for Write-Behind Cache
|
|
8
|
+
* update coalescing (last-write-wins per field).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Ipc
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
16
|
+
|
|
17
|
+
Ipc = require('../../src/Ipc')
|
|
18
|
+
global.Odac = {Config: {}}
|
|
19
|
+
await Ipc.init()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Ipc.close()
|
|
24
|
+
delete global.Odac
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Ipc - hset()', () => {
|
|
28
|
+
it('should store and retrieve hash fields', async () => {
|
|
29
|
+
await Ipc.hset('hash:a', {title: 'Hello', views: 100})
|
|
30
|
+
const result = await Ipc.hgetall('hash:a')
|
|
31
|
+
expect(result).toEqual({title: 'Hello', views: 100})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should merge new fields into existing hash', async () => {
|
|
35
|
+
await Ipc.hset('hash:b', {title: 'First'})
|
|
36
|
+
await Ipc.hset('hash:b', {slug: 'first-post'})
|
|
37
|
+
const result = await Ipc.hgetall('hash:b')
|
|
38
|
+
expect(result).toEqual({title: 'First', slug: 'first-post'})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should overwrite existing fields (last-write-wins)', async () => {
|
|
42
|
+
await Ipc.hset('hash:c', {title: 'Old'})
|
|
43
|
+
await Ipc.hset('hash:c', {title: 'New'})
|
|
44
|
+
const result = await Ipc.hgetall('hash:c')
|
|
45
|
+
expect(result).toEqual({title: 'New'})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return null for non-existent hash', async () => {
|
|
49
|
+
const result = await Ipc.hgetall('hash:nonexistent')
|
|
50
|
+
expect(result).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should be deletable via del()', async () => {
|
|
54
|
+
await Ipc.hset('hash:d', {title: 'Test'})
|
|
55
|
+
await Ipc.del('hash:d')
|
|
56
|
+
const result = await Ipc.hgetall('hash:d')
|
|
57
|
+
expect(result).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests Ipc.incrBy() atomic counter operation.
|
|
7
|
+
* Why: Validates that concurrent increment operations are atomic and return
|
|
8
|
+
* accurate accumulated values, essential for Write-Behind Cache counters.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Ipc
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
16
|
+
|
|
17
|
+
Ipc = require('../../src/Ipc')
|
|
18
|
+
global.Odac = {Config: {}}
|
|
19
|
+
await Ipc.init()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Ipc.close()
|
|
24
|
+
delete global.Odac
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Ipc - incrBy()', () => {
|
|
28
|
+
it('should initialize and return delta on first call', async () => {
|
|
29
|
+
const result = await Ipc.incrBy('counter:a', 5)
|
|
30
|
+
expect(result).toBe(5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should accumulate multiple increments', async () => {
|
|
34
|
+
await Ipc.incrBy('counter:b', 3)
|
|
35
|
+
await Ipc.incrBy('counter:b', 7)
|
|
36
|
+
const result = await Ipc.incrBy('counter:b', 2)
|
|
37
|
+
expect(result).toBe(12) // 3 + 7 + 2
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should handle negative deltas (decrement)', async () => {
|
|
41
|
+
await Ipc.incrBy('counter:c', 10)
|
|
42
|
+
const result = await Ipc.incrBy('counter:c', -3)
|
|
43
|
+
expect(result).toBe(7)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should not interfere with different keys', async () => {
|
|
47
|
+
await Ipc.incrBy('counter:x', 5)
|
|
48
|
+
await Ipc.incrBy('counter:y', 10)
|
|
49
|
+
|
|
50
|
+
expect(await Ipc.get('counter:x')).toBe(5)
|
|
51
|
+
expect(await Ipc.get('counter:y')).toBe(10)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should be readable via get()', async () => {
|
|
55
|
+
await Ipc.incrBy('counter:d', 42)
|
|
56
|
+
const result = await Ipc.get('counter:d')
|
|
57
|
+
expect(result).toBe(42)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should work with decrBy()', async () => {
|
|
61
|
+
await Ipc.incrBy('counter:e', 10)
|
|
62
|
+
const result = await Ipc.decrBy('counter:e', 4)
|
|
63
|
+
expect(result).toBe(6)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests Ipc distributed lock: lock() and unlock().
|
|
7
|
+
* Why: Validates that only one process can hold the flush lock at a time,
|
|
8
|
+
* preventing duplicate writes in horizontal scaling scenarios.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Ipc
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
16
|
+
|
|
17
|
+
Ipc = require('../../src/Ipc')
|
|
18
|
+
global.Odac = {Config: {}}
|
|
19
|
+
await Ipc.init()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Ipc.close()
|
|
24
|
+
delete global.Odac
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Ipc - lock()', () => {
|
|
28
|
+
it('should acquire lock and return true', async () => {
|
|
29
|
+
const result = await Ipc.lock('lock:a', 5)
|
|
30
|
+
expect(result).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should reject second acquisition (mutex)', async () => {
|
|
34
|
+
await Ipc.lock('lock:b', 5)
|
|
35
|
+
const result = await Ipc.lock('lock:b', 5)
|
|
36
|
+
expect(result).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should allow re-acquisition after unlock', async () => {
|
|
40
|
+
await Ipc.lock('lock:c', 5)
|
|
41
|
+
await Ipc.unlock('lock:c')
|
|
42
|
+
const result = await Ipc.lock('lock:c', 5)
|
|
43
|
+
expect(result).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should auto-expire after TTL', async () => {
|
|
47
|
+
await Ipc.lock('lock:d', 1) // 1 second TTL
|
|
48
|
+
|
|
49
|
+
// Wait for expiry
|
|
50
|
+
await new Promise(r => setTimeout(r, 1100))
|
|
51
|
+
|
|
52
|
+
// Lock should be available again
|
|
53
|
+
const result = await Ipc.lock('lock:d', 5)
|
|
54
|
+
expect(result).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should not interfere with different lock keys', async () => {
|
|
58
|
+
await Ipc.lock('lock:e', 5)
|
|
59
|
+
const result = await Ipc.lock('lock:f', 5)
|
|
60
|
+
expect(result).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests Ipc list operations: rpush() and lrange().
|
|
7
|
+
* Why: Validates that batch insert queue (Write-Behind Cache) correctly appends
|
|
8
|
+
* items and retrieves them in order.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Ipc
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
16
|
+
|
|
17
|
+
Ipc = require('../../src/Ipc')
|
|
18
|
+
global.Odac = {Config: {}}
|
|
19
|
+
await Ipc.init()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Ipc.close()
|
|
24
|
+
delete global.Odac
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Ipc - rpush()', () => {
|
|
28
|
+
it('should append items and return new length', async () => {
|
|
29
|
+
const len1 = await Ipc.rpush('list:a', {action: 'view'})
|
|
30
|
+
const len2 = await Ipc.rpush('list:a', {action: 'click'})
|
|
31
|
+
expect(len1).toBe(1)
|
|
32
|
+
expect(len2).toBe(2)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should append multiple items at once', async () => {
|
|
36
|
+
const len = await Ipc.rpush('list:b', {a: 1}, {a: 2}, {a: 3})
|
|
37
|
+
expect(len).toBe(3)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('Ipc - lrange()', () => {
|
|
42
|
+
it('should return all items with 0, -1', async () => {
|
|
43
|
+
await Ipc.rpush('list:c', 'first')
|
|
44
|
+
await Ipc.rpush('list:c', 'second')
|
|
45
|
+
await Ipc.rpush('list:c', 'third')
|
|
46
|
+
|
|
47
|
+
const result = await Ipc.lrange('list:c', 0, -1)
|
|
48
|
+
expect(result).toEqual(['first', 'second', 'third'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should return a range of items', async () => {
|
|
52
|
+
await Ipc.rpush('list:d', 'a', 'b', 'c', 'd')
|
|
53
|
+
const result = await Ipc.lrange('list:d', 1, 2)
|
|
54
|
+
expect(result).toEqual(['b', 'c'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return empty array for non-existent list', async () => {
|
|
58
|
+
const result = await Ipc.lrange('list:nonexistent', 0, -1)
|
|
59
|
+
expect(result).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should clear list on del()', async () => {
|
|
63
|
+
await Ipc.rpush('list:e', 'item')
|
|
64
|
+
await Ipc.del('list:e')
|
|
65
|
+
const result = await Ipc.lrange('list:e', 0, -1)
|
|
66
|
+
expect(result).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests Ipc set operations: sadd(), smembers(), srem().
|
|
7
|
+
* Why: WriteBuffer uses index sets to track active counter/update/queue keys
|
|
8
|
+
* for efficient flush discovery without expensive SCAN operations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Ipc
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
jest.resetModules()
|
|
15
|
+
Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
|
|
16
|
+
|
|
17
|
+
Ipc = require('../../src/Ipc')
|
|
18
|
+
global.Odac = {Config: {}}
|
|
19
|
+
await Ipc.init()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Ipc.close()
|
|
24
|
+
delete global.Odac
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Ipc - sadd()', () => {
|
|
28
|
+
it('should add members and return count of new additions', async () => {
|
|
29
|
+
const added = await Ipc.sadd('set:a', 'x', 'y', 'z')
|
|
30
|
+
expect(added).toBe(3)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not duplicate existing members', async () => {
|
|
34
|
+
await Ipc.sadd('set:b', 'x', 'y')
|
|
35
|
+
const added = await Ipc.sadd('set:b', 'y', 'z')
|
|
36
|
+
expect(added).toBe(1) // Only 'z' is new
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('Ipc - smembers()', () => {
|
|
41
|
+
it('should return all members', async () => {
|
|
42
|
+
await Ipc.sadd('set:c', 'a', 'b', 'c')
|
|
43
|
+
const members = await Ipc.smembers('set:c')
|
|
44
|
+
expect(members.sort()).toEqual(['a', 'b', 'c'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should return empty array for non-existent set', async () => {
|
|
48
|
+
const result = await Ipc.smembers('set:nonexistent')
|
|
49
|
+
expect(result).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('Ipc - srem()', () => {
|
|
54
|
+
it('should remove specified members', async () => {
|
|
55
|
+
await Ipc.sadd('set:d', 'a', 'b', 'c')
|
|
56
|
+
const removed = await Ipc.srem('set:d', 'b')
|
|
57
|
+
expect(removed).toBe(1)
|
|
58
|
+
|
|
59
|
+
const remaining = await Ipc.smembers('set:d')
|
|
60
|
+
expect(remaining.sort()).toEqual(['a', 'c'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should return 0 for non-existent members', async () => {
|
|
64
|
+
await Ipc.sadd('set:e', 'x')
|
|
65
|
+
const removed = await Ipc.srem('set:e', 'nonexistent')
|
|
66
|
+
expect(removed).toBe(0)
|
|
67
|
+
})
|
|
68
|
+
})
|