odac 1.4.6 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/client/odac.js +1 -1
  3. package/docs/ai/README.md +1 -1
  4. package/docs/ai/skills/SKILL.md +1 -1
  5. package/docs/ai/skills/backend/database.md +103 -12
  6. package/docs/ai/skills/backend/ipc.md +71 -12
  7. package/docs/ai/skills/backend/views.md +6 -1
  8. package/docs/backend/00-getting-started/01-quick-start.md +77 -0
  9. package/docs/backend/07-views/03-template-syntax.md +5 -0
  10. package/docs/backend/07-views/04-request-data.md +13 -0
  11. package/docs/backend/08-database/05-write-behind-cache.md +230 -0
  12. package/docs/backend/13-utilities/02-ipc.md +117 -0
  13. package/docs/index.json +10 -0
  14. package/package.json +1 -1
  15. package/src/Database/WriteBuffer.js +605 -0
  16. package/src/Database.js +32 -1
  17. package/src/Ipc.js +343 -81
  18. package/src/Odac.js +2 -1
  19. package/src/Storage.js +4 -2
  20. package/src/View.js +33 -18
  21. package/test/Database/WriteBuffer/_recoverFromCheckpoint.test.js +207 -0
  22. package/test/Database/WriteBuffer/buffer.test.js +143 -0
  23. package/test/Database/WriteBuffer/flush.test.js +192 -0
  24. package/test/Database/WriteBuffer/get.test.js +72 -0
  25. package/test/Database/WriteBuffer/increment.test.js +118 -0
  26. package/test/Database/WriteBuffer/update.test.js +178 -0
  27. package/test/Ipc/hset.test.js +59 -0
  28. package/test/Ipc/incrBy.test.js +65 -0
  29. package/test/Ipc/lock.test.js +62 -0
  30. package/test/Ipc/rpush.test.js +68 -0
  31. package/test/Ipc/sadd.test.js +68 -0
  32. package/test/View/addNavigateAttribute.test.js +53 -0
  33. package/test/View/print.test.js +45 -1
  34. package/test/View/tags.test.js +132 -0
@@ -0,0 +1,207 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests WriteBuffer LMDB checkpoint and crash recovery.
7
+ * Why: Validates zero data loss guarantee — buffered data survives process crashes
8
+ * via periodic LMDB checkpoints and is recovered on next startup.
9
+ */
10
+
11
+ let knexLib, db, storageData
12
+
13
+ function createMockStorage() {
14
+ storageData = new Map()
15
+ return {
16
+ isReady: () => true,
17
+ put: (key, value) => storageData.set(key, value),
18
+ remove: key => storageData.delete(key),
19
+ get: key => storageData.get(key) ?? null,
20
+ getRange: ({start, end}) => {
21
+ const results = []
22
+ for (const [key, value] of storageData) {
23
+ if (key >= start && key < end) {
24
+ results.push({key, value})
25
+ }
26
+ }
27
+ return results
28
+ }
29
+ }
30
+ }
31
+
32
+ beforeEach(async () => {
33
+ jest.resetModules()
34
+
35
+ knexLib = require('knex')
36
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
37
+
38
+ await db.schema.createTable('posts', table => {
39
+ table.integer('id').primary()
40
+ table.integer('views').defaultTo(0)
41
+ })
42
+ await db('posts').insert({id: 1, views: 100})
43
+
44
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
45
+ })
46
+
47
+ afterEach(async () => {
48
+ await db.destroy()
49
+ delete global.Odac
50
+ })
51
+
52
+ describe('WriteBuffer - Checkpoint', () => {
53
+ it('should write counter deltas to LMDB on checkpoint', async () => {
54
+ const Ipc = require('../../../src/Ipc')
55
+ global.Odac = {
56
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
57
+ Storage: createMockStorage(),
58
+ Ipc
59
+ }
60
+ await Ipc.init()
61
+
62
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
63
+ await WriteBuffer.init({default: db})
64
+
65
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
66
+ await WriteBuffer._writeCheckpoint()
67
+
68
+ const checkpoint = storageData.get('wb:c:default:posts:1:views')
69
+ expect(checkpoint).toBeDefined()
70
+ expect(checkpoint.delta).toBe(5)
71
+ expect(checkpoint.base).toBe(100)
72
+
73
+ await WriteBuffer.close()
74
+ await Ipc.close()
75
+ })
76
+
77
+ it('should write queue rows to LMDB on checkpoint', async () => {
78
+ const Ipc = require('../../../src/Ipc')
79
+ global.Odac = {
80
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
81
+ Storage: createMockStorage(),
82
+ Ipc
83
+ }
84
+ await Ipc.init()
85
+
86
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
87
+ await WriteBuffer.init({default: db})
88
+
89
+ await WriteBuffer.insert('default', 'activity_log', {user_id: 1, action: 'view'})
90
+ await WriteBuffer.insert('default', 'activity_log', {user_id: 2, action: 'click'})
91
+ await WriteBuffer._writeCheckpoint()
92
+
93
+ const checkpoint = storageData.get('wb:q:default:activity_log')
94
+ expect(checkpoint).toBeDefined()
95
+ expect(checkpoint).toHaveLength(2)
96
+ expect(checkpoint[0].action).toBe('view')
97
+
98
+ await WriteBuffer.close()
99
+ await Ipc.close()
100
+ })
101
+ })
102
+
103
+ describe('WriteBuffer - Recovery', () => {
104
+ it('should recover counter deltas from LMDB on startup', async () => {
105
+ // Simulate crash: write checkpoint data before init
106
+ const mockStorage = createMockStorage()
107
+ storageData.set('wb:c:default:posts:1:views', {delta: 7, base: 100})
108
+
109
+ const Ipc = require('../../../src/Ipc')
110
+ global.Odac = {
111
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
112
+ Storage: mockStorage,
113
+ Ipc
114
+ }
115
+ await Ipc.init()
116
+
117
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
118
+ await WriteBuffer.init({default: db})
119
+
120
+ // Should recover the delta from checkpoint
121
+ const result = await WriteBuffer.get('default', 'posts', 1, 'views')
122
+ expect(result).toBe(107) // base 100 + recovered delta 7
123
+
124
+ await WriteBuffer.close()
125
+ await Ipc.close()
126
+ })
127
+
128
+ it('should recover queue rows from LMDB on startup', async () => {
129
+ await db.schema.createTable('activity_log', table => {
130
+ table.increments('id')
131
+ table.integer('user_id')
132
+ table.string('action', 50)
133
+ })
134
+
135
+ const mockStorage = createMockStorage()
136
+ storageData.set('wb:q:default:activity_log', [{user_id: 1, action: 'recovered_view'}])
137
+
138
+ const Ipc = require('../../../src/Ipc')
139
+ global.Odac = {
140
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
141
+ Storage: mockStorage,
142
+ Ipc
143
+ }
144
+ await Ipc.init()
145
+
146
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
147
+ await WriteBuffer.init({default: db})
148
+
149
+ // Flush recovered data
150
+ await WriteBuffer.flush()
151
+
152
+ const rows = await db('activity_log').select()
153
+ expect(rows).toHaveLength(1)
154
+ expect(rows[0].action).toBe('recovered_view')
155
+
156
+ await WriteBuffer.close()
157
+ await Ipc.close()
158
+ })
159
+
160
+ it('should merge recovered data with new increments', async () => {
161
+ const mockStorage = createMockStorage()
162
+ storageData.set('wb:c:default:posts:1:views', {delta: 5, base: 100})
163
+
164
+ const Ipc = require('../../../src/Ipc')
165
+ global.Odac = {
166
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
167
+ Storage: mockStorage,
168
+ Ipc
169
+ }
170
+ await Ipc.init()
171
+
172
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
173
+ await WriteBuffer.init({default: db})
174
+
175
+ // Add more increments on top of recovered data
176
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 3)
177
+
178
+ const result = await WriteBuffer.get('default', 'posts', 1, 'views')
179
+ expect(result).toBe(108) // base 100 + recovered 5 + new 3
180
+
181
+ await WriteBuffer.close()
182
+ await Ipc.close()
183
+ })
184
+
185
+ it('should clear LMDB checkpoint after successful flush', async () => {
186
+ const mockStorage = createMockStorage()
187
+ storageData.set('wb:c:default:posts:1:views', {delta: 5, base: 100})
188
+
189
+ const Ipc = require('../../../src/Ipc')
190
+ global.Odac = {
191
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
192
+ Storage: mockStorage,
193
+ Ipc
194
+ }
195
+ await Ipc.init()
196
+
197
+ const WriteBuffer = require('../../../src/Database/WriteBuffer')
198
+ await WriteBuffer.init({default: db})
199
+ await WriteBuffer.flush()
200
+
201
+ // Checkpoint data should be cleared
202
+ expect(storageData.has('wb:c:default:posts:1:views')).toBe(false)
203
+
204
+ await WriteBuffer.close()
205
+ await Ipc.close()
206
+ })
207
+ })
@@ -0,0 +1,143 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests the chainable buffer API exposed via Database.js proxy.
7
+ * Why: Validates that the Odac.DB.table.buffer.where(id).update(data) pattern
8
+ * correctly delegates to WriteBuffer's internal methods.
9
+ */
10
+
11
+ let knexLib, db
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
+ })
24
+
25
+ await db.schema.createTable('activity_log', table => {
26
+ table.increments('id')
27
+ table.integer('user_id')
28
+ table.string('action', 50)
29
+ })
30
+
31
+ await db('posts').insert([
32
+ {id: 1, views: 100, title: 'First Post'},
33
+ {id: 2, views: 200, title: 'Second Post'}
34
+ ])
35
+
36
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
37
+
38
+ const Ipc = require('../../../src/Ipc')
39
+ global.Odac = {
40
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
41
+ Storage: {
42
+ isReady: () => false,
43
+ put: jest.fn(),
44
+ remove: jest.fn(),
45
+ getRange: () => []
46
+ },
47
+ Ipc
48
+ }
49
+ await Ipc.init()
50
+
51
+ // Initialize WriteBuffer with our test DB
52
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
53
+ await writeBuffer.init({default: db})
54
+
55
+ // Wire up Database.js proxy
56
+ const DB = require('../../../src/Database')
57
+ DB.connections = {default: db}
58
+ })
59
+
60
+ afterEach(async () => {
61
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
62
+ await writeBuffer.close()
63
+ await Odac.Ipc.close()
64
+ await db.destroy()
65
+ delete global.Odac
66
+ })
67
+
68
+ describe('Database.js Proxy - buffer.where().update()', () => {
69
+ it('should buffer and flush via Odac.DB.posts.buffer.where(id).update()', async () => {
70
+ const DB = require('../../../src/Database')
71
+
72
+ await DB.posts.buffer.where(1).update({title: 'Updated Title'})
73
+ await DB.posts.buffer.flush()
74
+
75
+ const row = await db('posts').where({id: 1}).first()
76
+ expect(row.title).toBe('Updated Title')
77
+ })
78
+
79
+ it('should merge multiple updates via chainable API', async () => {
80
+ const DB = require('../../../src/Database')
81
+
82
+ await DB.posts.buffer.where(1).update({title: 'New Title'})
83
+ await DB.posts.buffer.where(1).update({title: 'Final Title'})
84
+ await DB.posts.buffer.flush()
85
+
86
+ const row = await db('posts').where({id: 1}).first()
87
+ expect(row.title).toBe('Final Title')
88
+ })
89
+ })
90
+
91
+ describe('Database.js Proxy - buffer.where().increment()', () => {
92
+ it('should increment via Odac.DB.posts.buffer.where(id).increment(col)', async () => {
93
+ const DB = require('../../../src/Database')
94
+
95
+ const result = await DB.posts.buffer.where(1).increment('views')
96
+ expect(result).toBe(101)
97
+ })
98
+
99
+ it('should support custom delta', async () => {
100
+ const DB = require('../../../src/Database')
101
+
102
+ const result = await DB.posts.buffer.where(1).increment('views', 5)
103
+ expect(result).toBe(105)
104
+ })
105
+ })
106
+
107
+ describe('Database.js Proxy - buffer.where().get()', () => {
108
+ it('should get buffered value via Odac.DB.posts.buffer.where(id).get(col)', async () => {
109
+ const DB = require('../../../src/Database')
110
+
111
+ await DB.posts.buffer.where(1).increment('views', 10)
112
+ const result = await DB.posts.buffer.where(1).get('views')
113
+ expect(result).toBe(110)
114
+ })
115
+ })
116
+
117
+ describe('Database.js Proxy - buffer.insert()', () => {
118
+ it('should buffer insert via Odac.DB.activity_log.buffer.insert(row)', async () => {
119
+ const DB = require('../../../src/Database')
120
+
121
+ await DB.activity_log.buffer.insert({user_id: 1, action: 'view'})
122
+ await DB.activity_log.buffer.insert({user_id: 2, action: 'click'})
123
+ await DB.activity_log.buffer.flush()
124
+
125
+ const rows = await db('activity_log').select()
126
+ expect(rows).toHaveLength(2)
127
+ })
128
+ })
129
+
130
+ describe('Database.js Proxy - buffer.flush()', () => {
131
+ it('should flush all buffered data for the table', async () => {
132
+ const DB = require('../../../src/Database')
133
+
134
+ await DB.posts.buffer.where(1).increment('views', 5)
135
+ await DB.posts.buffer.where(2).update({title: 'Changed'})
136
+ await DB.posts.buffer.flush()
137
+
138
+ const row1 = await db('posts').where({id: 1}).first()
139
+ const row2 = await db('posts').where({id: 2}).first()
140
+ expect(row1.views).toBe(105)
141
+ expect(row2.title).toBe('Changed')
142
+ })
143
+ })
@@ -0,0 +1,192 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests WriteBuffer flush operations: counter flush (batch UPDATE) and queue flush (batch INSERT).
7
+ * Why: Validates that buffered data is correctly persisted to the database,
8
+ * with proper transaction handling and error recovery.
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.integer('likes').defaultTo(0)
23
+ table.string('title', 255)
24
+ })
25
+
26
+ await db('posts').insert([
27
+ {id: 1, views: 100, likes: 10, title: 'First Post'},
28
+ {id: 2, views: 200, likes: 20, title: '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 - Counter Flush', () => {
58
+ it('should persist accumulated deltas to the database', async () => {
59
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
60
+ await WriteBuffer.increment('default', 'posts', 2, 'views', 10)
61
+
62
+ await WriteBuffer.flush()
63
+
64
+ const row1 = await db('posts').where({id: 1}).first()
65
+ const row2 = await db('posts').where({id: 2}).first()
66
+ expect(row1.views).toBe(105)
67
+ expect(row2.views).toBe(210)
68
+ })
69
+
70
+ it('should flush multiple columns for the same row', async () => {
71
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 3)
72
+ await WriteBuffer.increment('default', 'posts', 1, 'likes', 7)
73
+
74
+ await WriteBuffer.flush()
75
+
76
+ const row = await db('posts').where({id: 1}).first()
77
+ expect(row.views).toBe(103)
78
+ expect(row.likes).toBe(17)
79
+ })
80
+
81
+ it('should clear counter index after successful flush', async () => {
82
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
83
+ await WriteBuffer.flush()
84
+
85
+ // Counter index should be empty after flush
86
+ const remaining = await Odac.Ipc.smembers('wb:idx:counters')
87
+ expect(remaining).toHaveLength(0)
88
+ })
89
+
90
+ it('should update base after flush so subsequent reads are correct', async () => {
91
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
92
+ await WriteBuffer.flush()
93
+
94
+ // New increment after flush
95
+ const result = await WriteBuffer.increment('default', 'posts', 1, 'views', 2)
96
+ expect(result).toBe(107) // base updated to 105, + 2
97
+
98
+ await WriteBuffer.flush()
99
+ const row = await db('posts').where({id: 1}).first()
100
+ expect(row.views).toBe(107)
101
+ })
102
+
103
+ it('should accumulate new deltas during flush correctly', async () => {
104
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 10)
105
+ await WriteBuffer.flush()
106
+
107
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 3)
108
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 2)
109
+ await WriteBuffer.flush()
110
+
111
+ const row = await db('posts').where({id: 1}).first()
112
+ expect(row.views).toBe(115) // 100 + 10 + 5
113
+ })
114
+
115
+ it('should scope flush to specific table when provided', async () => {
116
+ await db.schema.createTable('comments', table => {
117
+ table.integer('id').primary()
118
+ table.integer('votes').defaultTo(0)
119
+ })
120
+ await db('comments').insert({id: 1, votes: 50})
121
+
122
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 5)
123
+ await WriteBuffer.increment('default', 'comments', 1, 'votes', 3)
124
+
125
+ // Flush only posts
126
+ await WriteBuffer.flush('default', 'posts')
127
+
128
+ const post = await db('posts').where({id: 1}).first()
129
+ const comment = await db('comments').where({id: 1}).first()
130
+ expect(post.views).toBe(105) // Flushed
131
+ expect(comment.votes).toBe(50) // Not flushed
132
+ })
133
+ })
134
+
135
+ describe('WriteBuffer - Queue Flush (Batch Insert)', () => {
136
+ it('should batch insert queued rows', async () => {
137
+ await db.schema.createTable('activity_log', table => {
138
+ table.increments('id')
139
+ table.integer('user_id')
140
+ table.string('action', 50)
141
+ })
142
+
143
+ await WriteBuffer.insert('default', 'activity_log', {user_id: 1, action: 'view'})
144
+ await WriteBuffer.insert('default', 'activity_log', {user_id: 2, action: 'click'})
145
+ await WriteBuffer.insert('default', 'activity_log', {user_id: 1, action: 'scroll'})
146
+
147
+ await WriteBuffer.flush()
148
+
149
+ const rows = await db('activity_log').select()
150
+ expect(rows).toHaveLength(3)
151
+ expect(rows[0].action).toBe('view')
152
+ expect(rows[1].action).toBe('click')
153
+ expect(rows[2].action).toBe('scroll')
154
+ })
155
+
156
+ it('should clear queue after successful flush', async () => {
157
+ await db.schema.createTable('events', table => {
158
+ table.increments('id')
159
+ table.string('type', 50)
160
+ })
161
+
162
+ await WriteBuffer.insert('default', 'events', {type: 'pageview'})
163
+ await WriteBuffer.flush()
164
+
165
+ const queue = await Odac.Ipc.lrange('wb:q:default:events', 0, -1)
166
+ expect(queue).toHaveLength(0)
167
+ })
168
+
169
+ it('should handle empty queues gracefully', async () => {
170
+ await expect(WriteBuffer.flush()).resolves.not.toThrow()
171
+ })
172
+
173
+ it('should auto-flush when maxQueueSize is reached', async () => {
174
+ await db.schema.createTable('logs', table => {
175
+ table.increments('id')
176
+ table.string('msg', 50)
177
+ })
178
+
179
+ // Set low threshold for testing
180
+ WriteBuffer._config.maxQueueSize = 3
181
+
182
+ await WriteBuffer.insert('default', 'logs', {msg: 'a'})
183
+ await WriteBuffer.insert('default', 'logs', {msg: 'b'})
184
+ await WriteBuffer.insert('default', 'logs', {msg: 'c'}) // Triggers auto-flush
185
+
186
+ // Wait a tick for async auto-flush
187
+ await new Promise(r => setTimeout(r, 50))
188
+
189
+ const rows = await db('logs').select()
190
+ expect(rows.length).toBeGreaterThanOrEqual(3)
191
+ })
192
+ })
@@ -0,0 +1,72 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests WriteBuffer.get().
7
+ * Why: Validates that get() returns the accurate current value (DB base + buffered delta)
8
+ * without flushing to the database.
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
+ })
24
+
25
+ await db('posts').insert([
26
+ {id: 1, views: 100, title: 'First Post'},
27
+ {id: 2, views: 200, title: 'Second Post'}
28
+ ])
29
+
30
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
31
+
32
+ const Ipc = require('../../../src/Ipc')
33
+ global.Odac = {
34
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
35
+ Storage: {
36
+ isReady: () => false,
37
+ put: jest.fn(),
38
+ remove: jest.fn(),
39
+ getRange: () => []
40
+ },
41
+ Ipc
42
+ }
43
+ await Ipc.init()
44
+
45
+ WriteBuffer = require('../../../src/Database/WriteBuffer')
46
+ await WriteBuffer.init({default: db})
47
+ })
48
+
49
+ afterEach(async () => {
50
+ await WriteBuffer.close()
51
+ await Odac.Ipc.close()
52
+ await db.destroy()
53
+ delete global.Odac
54
+ })
55
+
56
+ describe('WriteBuffer - get()', () => {
57
+ it('should return base value from DB when no buffer exists', async () => {
58
+ const result = await WriteBuffer.get('default', 'posts', 1, 'views')
59
+ expect(result).toBe(100)
60
+ })
61
+
62
+ it('should return base + delta when buffer exists', async () => {
63
+ await WriteBuffer.increment('default', 'posts', 1, 'views', 7)
64
+ const result = await WriteBuffer.get('default', 'posts', 1, 'views')
65
+ expect(result).toBe(107)
66
+ })
67
+
68
+ it('should return 0 for non-existent rows', async () => {
69
+ const result = await WriteBuffer.get('default', 'posts', 999, 'views')
70
+ expect(result).toBe(0)
71
+ })
72
+ })