queen-mq 0.12.3-beta.1 → 0.12.4

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.
@@ -153,6 +153,7 @@ export class BufferManager {
153
153
  }
154
154
 
155
155
  async flushAllBuffers() {
156
+ // Get all queue addresses that have buffers
156
157
  const queueAddresses = Array.from(this.#buffers.keys())
157
158
  logger.log('BufferManager.flushAllBuffers', { bufferCount: queueAddresses.length, pendingFlushes: this.#pendingFlushes.size })
158
159
 
@@ -8,9 +8,11 @@
8
8
  */
9
9
 
10
10
  const LOG_ENABLED = (() => {
11
+ // Node.js environment
11
12
  if (typeof process !== 'undefined' && process.env) {
12
13
  return process.env.QUEEN_CLIENT_LOG === 'true'
13
14
  }
15
+ // Browser environment
14
16
  if (typeof window !== 'undefined') {
15
17
  return window.QUEEN_CLIENT_LOG === true
16
18
  }
@@ -23,6 +25,9 @@ function getTimestamp() {
23
25
  return new Date().toISOString()
24
26
  }
25
27
 
28
+ /**
29
+ * Format log message with timestamp and operation
30
+ */
26
31
  function formatLog(operation, details, level = 'INFO') {
27
32
  const timestamp = getTimestamp()
28
33
  const detailsStr = typeof details === 'object' ? JSON.stringify(details) : details
@@ -84,6 +89,9 @@ export function error(operation, details) {
84
89
  console.error(formatLog(operation, details, 'ERROR'))
85
90
  }
86
91
 
92
+ /**
93
+ * Check if logging is enabled
94
+ */
87
95
  export function isEnabled() {
88
96
  return customLogger != null || LOG_ENABLED
89
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.12.3-beta.1",
3
+ "version": "0.12.4",
4
4
  "type": "module",
5
5
  "description": "High-performance C++ message queue backed by PostgreSQL",
6
6
  "main": "client-v2/index.js",
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Logger Unit Tests
3
+ *
4
+ * Tests the pluggable logger system in utils/logger.js:
5
+ * - Default console-based logging (gated by QUEEN_CLIENT_LOG)
6
+ * - Custom logger injection via configure()
7
+ * - Level mapping (log->info, warn->warn, error->error, debug->debug)
8
+ * - debug() fallback to info() when custom logger has no debug()
9
+ * - configure(null) resets to default
10
+ * - configure() validation
11
+ * - isEnabled() behavior
12
+ *
13
+ * These are pure unit tests — no Queen server required.
14
+ */
15
+
16
+ import * as logger from '../client-v2/utils/logger.js'
17
+
18
+ function createMockLogger() {
19
+ const calls = { info: [], warn: [], error: [], debug: [] }
20
+ return {
21
+ calls,
22
+ info: (msg) => calls.info.push(msg),
23
+ warn: (msg) => calls.warn.push(msg),
24
+ error: (msg) => calls.error.push(msg),
25
+ debug: (msg) => calls.debug.push(msg)
26
+ }
27
+ }
28
+
29
+ function createMockLoggerWithoutDebug() {
30
+ const calls = { info: [], warn: [], error: [] }
31
+ return {
32
+ calls,
33
+ info: (msg) => calls.info.push(msg),
34
+ warn: (msg) => calls.warn.push(msg),
35
+ error: (msg) => calls.error.push(msg)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Test that configure() accepts a valid logger and routes log() to info()
41
+ */
42
+ export async function testLoggerCustomLogRouting(_client) {
43
+ const mock = createMockLogger()
44
+ logger.configure(mock)
45
+
46
+ try {
47
+ logger.log('TestOp', 'hello')
48
+ logger.log('TestOp', { key: 'value' })
49
+
50
+ if (mock.calls.info.length !== 2) {
51
+ return { success: false, message: `Expected 2 info calls, got ${mock.calls.info.length}` }
52
+ }
53
+ if (!mock.calls.info[0].includes('[TestOp]') || !mock.calls.info[0].includes('hello')) {
54
+ return { success: false, message: `Unexpected info message: ${mock.calls.info[0]}` }
55
+ }
56
+ if (!mock.calls.info[1].includes('"key":"value"')) {
57
+ return { success: false, message: `Object details not serialized: ${mock.calls.info[1]}` }
58
+ }
59
+
60
+ return { success: true, message: 'log() routes to custom logger info()' }
61
+ } finally {
62
+ logger.configure(null)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Test that warn() routes to custom logger warn()
68
+ */
69
+ export async function testLoggerCustomWarnRouting(_client) {
70
+ const mock = createMockLogger()
71
+ logger.configure(mock)
72
+
73
+ try {
74
+ logger.warn('WarnOp', 'something bad')
75
+ logger.warn('WarnOp', { code: 42 })
76
+
77
+ if (mock.calls.warn.length !== 2) {
78
+ return { success: false, message: `Expected 2 warn calls, got ${mock.calls.warn.length}` }
79
+ }
80
+ if (!mock.calls.warn[0].includes('[WarnOp]')) {
81
+ return { success: false, message: `Missing operation in warn: ${mock.calls.warn[0]}` }
82
+ }
83
+
84
+ return { success: true, message: 'warn() routes to custom logger warn()' }
85
+ } finally {
86
+ logger.configure(null)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Test that error() routes to custom logger error()
92
+ */
93
+ export async function testLoggerCustomErrorRouting(_client) {
94
+ const mock = createMockLogger()
95
+ logger.configure(mock)
96
+
97
+ try {
98
+ logger.error('ErrOp', { error: 'boom' })
99
+
100
+ if (mock.calls.error.length !== 1) {
101
+ return { success: false, message: `Expected 1 error call, got ${mock.calls.error.length}` }
102
+ }
103
+ if (!mock.calls.error[0].includes('[ErrOp]') || !mock.calls.error[0].includes('boom')) {
104
+ return { success: false, message: `Unexpected error message: ${mock.calls.error[0]}` }
105
+ }
106
+
107
+ return { success: true, message: 'error() routes to custom logger error()' }
108
+ } finally {
109
+ logger.configure(null)
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Test that debug() routes to custom logger debug() when available
115
+ */
116
+ export async function testLoggerCustomDebugRouting(_client) {
117
+ const mock = createMockLogger()
118
+ logger.configure(mock)
119
+
120
+ try {
121
+ logger.debug('DebugOp', 'verbose detail')
122
+
123
+ if (mock.calls.debug.length !== 1) {
124
+ return { success: false, message: `Expected 1 debug call, got ${mock.calls.debug.length}` }
125
+ }
126
+ if (!mock.calls.debug[0].includes('[DebugOp]')) {
127
+ return { success: false, message: `Missing operation in debug: ${mock.calls.debug[0]}` }
128
+ }
129
+ // info() should NOT have been called
130
+ if (mock.calls.info.length !== 0) {
131
+ return { success: false, message: `debug() should not fall back to info() when debug() exists` }
132
+ }
133
+
134
+ return { success: true, message: 'debug() routes to custom logger debug()' }
135
+ } finally {
136
+ logger.configure(null)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Test that debug() falls back to info() when custom logger has no debug()
142
+ */
143
+ export async function testLoggerDebugFallbackToInfo(_client) {
144
+ const mock = createMockLoggerWithoutDebug()
145
+ logger.configure(mock)
146
+
147
+ try {
148
+ logger.debug('FallbackOp', 'should go to info')
149
+
150
+ if (mock.calls.info.length !== 1) {
151
+ return { success: false, message: `Expected 1 info call (fallback), got ${mock.calls.info.length}` }
152
+ }
153
+ if (!mock.calls.info[0].includes('[FallbackOp]')) {
154
+ return { success: false, message: `Missing operation in fallback: ${mock.calls.info[0]}` }
155
+ }
156
+
157
+ return { success: true, message: 'debug() falls back to info() when no debug() on logger' }
158
+ } finally {
159
+ logger.configure(null)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Test that configure(null) resets to default console behavior
165
+ */
166
+ export async function testLoggerConfigureNull(_client) {
167
+ const mock = createMockLogger()
168
+ logger.configure(mock)
169
+
170
+ // Verify custom logger is active
171
+ logger.log('Before', 'test')
172
+ if (mock.calls.info.length !== 1) {
173
+ return { success: false, message: 'Custom logger not active before reset' }
174
+ }
175
+
176
+ // Reset
177
+ logger.configure(null)
178
+
179
+ // Now log should NOT go to mock anymore
180
+ logger.log('After', 'test')
181
+ if (mock.calls.info.length !== 1) {
182
+ return { success: false, message: `Expected still 1 info call after reset, got ${mock.calls.info.length}` }
183
+ }
184
+
185
+ return { success: true, message: 'configure(null) resets to default' }
186
+ }
187
+
188
+ /**
189
+ * Test that configure() throws on invalid logger (missing info method)
190
+ */
191
+ export async function testLoggerConfigureValidation(_client) {
192
+ try {
193
+ logger.configure({ warn: () => {}, error: () => {} })
194
+ return { success: false, message: 'Should have thrown for logger missing info()' }
195
+ } catch (e) {
196
+ if (!e.message.includes('info()')) {
197
+ return { success: false, message: `Wrong error message: ${e.message}` }
198
+ }
199
+ } finally {
200
+ logger.configure(null)
201
+ }
202
+
203
+ // Empty object should also fail
204
+ try {
205
+ logger.configure({})
206
+ return { success: false, message: 'Should have thrown for empty object' }
207
+ } catch (e) {
208
+ // expected
209
+ } finally {
210
+ logger.configure(null)
211
+ }
212
+
213
+ return { success: true, message: 'configure() validates logger interface' }
214
+ }
215
+
216
+ /**
217
+ * Test isEnabled() returns true when custom logger is set
218
+ */
219
+ export async function testLoggerIsEnabledWithCustom(_client) {
220
+ // Without custom logger and without QUEEN_CLIENT_LOG, isEnabled depends on env
221
+ const beforeCustom = logger.isEnabled()
222
+
223
+ const mock = createMockLogger()
224
+ logger.configure(mock)
225
+
226
+ try {
227
+ if (!logger.isEnabled()) {
228
+ return { success: false, message: 'isEnabled() should return true with custom logger' }
229
+ }
230
+ return { success: true, message: `isEnabled() returns true with custom logger (was ${beforeCustom} before)` }
231
+ } finally {
232
+ logger.configure(null)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Test that levels are isolated — log() only hits info(), not warn() or error()
238
+ */
239
+ export async function testLoggerLevelIsolation(_client) {
240
+ const mock = createMockLogger()
241
+ logger.configure(mock)
242
+
243
+ try {
244
+ logger.log('Op', 'info msg')
245
+ logger.warn('Op', 'warn msg')
246
+ logger.error('Op', 'error msg')
247
+ logger.debug('Op', 'debug msg')
248
+
249
+ if (mock.calls.info.length !== 1) {
250
+ return { success: false, message: `info: expected 1, got ${mock.calls.info.length}` }
251
+ }
252
+ if (mock.calls.warn.length !== 1) {
253
+ return { success: false, message: `warn: expected 1, got ${mock.calls.warn.length}` }
254
+ }
255
+ if (mock.calls.error.length !== 1) {
256
+ return { success: false, message: `error: expected 1, got ${mock.calls.error.length}` }
257
+ }
258
+ if (mock.calls.debug.length !== 1) {
259
+ return { success: false, message: `debug: expected 1, got ${mock.calls.debug.length}` }
260
+ }
261
+
262
+ return { success: true, message: 'Each level routes to exactly its corresponding method' }
263
+ } finally {
264
+ logger.configure(null)
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Test that string details are passed through without JSON.stringify
270
+ */
271
+ export async function testLoggerStringDetails(_client) {
272
+ const mock = createMockLogger()
273
+ logger.configure(mock)
274
+
275
+ try {
276
+ logger.log('Op', 'plain string')
277
+
278
+ const msg = mock.calls.info[0]
279
+ if (!msg.includes('plain string')) {
280
+ return { success: false, message: `String details not passed through: ${msg}` }
281
+ }
282
+ // Should NOT be double-quoted as JSON
283
+ if (msg.includes('"plain string"')) {
284
+ return { success: false, message: `String was JSON-serialized: ${msg}` }
285
+ }
286
+
287
+ return { success: true, message: 'String details passed as-is' }
288
+ } finally {
289
+ logger.configure(null)
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Test that object details are JSON-serialized
295
+ */
296
+ export async function testLoggerObjectDetails(_client) {
297
+ const mock = createMockLogger()
298
+ logger.configure(mock)
299
+
300
+ try {
301
+ logger.log('Op', { foo: 'bar', count: 3 })
302
+
303
+ const msg = mock.calls.info[0]
304
+ if (!msg.includes('"foo":"bar"') || !msg.includes('"count":3')) {
305
+ return { success: false, message: `Object not serialized correctly: ${msg}` }
306
+ }
307
+
308
+ return { success: true, message: 'Object details JSON-serialized' }
309
+ } finally {
310
+ logger.configure(null)
311
+ }
312
+ }
package/test-v2/run.js CHANGED
@@ -12,6 +12,7 @@ import * as subscriptionTests from './subscription.js'
12
12
  import * as maintenanceTests from './maintenance.js'
13
13
  import * as retentionTests from './retention.js'
14
14
  import * as bootstrapTests from './bootstrap.js'
15
+ import * as loggerTests from './logger.js'
15
16
  import { LoadBalancer } from '../client-v2/http/LoadBalancer.js';
16
17
 
17
18
 
@@ -113,7 +114,8 @@ async function main() {
113
114
  subscriptionTests,
114
115
  retentionTests,
115
116
  maintenanceTests,
116
- bootstrapTests
117
+ bootstrapTests,
118
+ loggerTests
117
119
  ]
118
120
 
119
121
  const aiTests = [