Skip to content

Commit 75aa26b

Browse files
logger
1 parent caea905 commit 75aa26b

File tree

16 files changed

+457
-39
lines changed

16 files changed

+457
-39
lines changed

.changeset/cli-previews-command.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
'@walkeros/cli': minor
2+
'@walkeros/cli': patch
33
---
44

55
Add `walkeros previews {list|get|create|delete}` commands for managing preview

.changeset/mcp-preview-actions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
'@walkeros/mcp': minor
2+
'@walkeros/mcp': patch
33
---
44

55
Add `preview.list`, `preview.get`, `preview.create`, and `preview.delete`

.changeset/preview-preflight-self-heal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
'@walkeros/cli': minor
2+
'@walkeros/cli': patch
33
---
44

55
Preview preflight now self-heals when a preview bundle is deleted. Instead of
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@walkeros/core': patch
3+
'@walkeros/collector': patch
4+
---
5+
6+
`useHooks` now isolates hook failures. A pre-hook that throws no longer crashes
7+
the pipeline — the wrapped function is called directly and a warning is logged.
8+
A post-hook that throws leaves the original result in place. Added optional 4th
9+
`logger` parameter so warnings route through the walkerOS Logger (falls back to
10+
`console.warn` when no logger is provided). All collector call sites now pass
11+
`collector.logger`.

packages/collector/src/command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ export function createCommand<T extends Collector.Instance>(
3232
},
3333
'Command',
3434
collector.hooks,
35+
collector.logger,
3536
) as Collector.CommandFn;
3637
}

packages/collector/src/destination.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ export async function destinationInit<Destination extends Destination.Instance>(
509509
destination.init,
510510
'DestinationInit',
511511
collector.hooks,
512+
collector.logger,
512513
)(context);
513514

514515
// Actively check for errors (when false)
@@ -634,6 +635,7 @@ export async function destinationPush<Destination extends Destination.Instance>(
634635
destination.pushBatch!,
635636
'DestinationPushBatch',
636637
collector.hooks,
638+
collector.logger,
637639
)(currentBatched, batchContext);
638640

639641
destLogger.debug('push batch done');
@@ -660,6 +662,7 @@ export async function destinationPush<Destination extends Destination.Instance>(
660662
destination.push,
661663
'DestinationPush',
662664
collector.hooks,
665+
collector.logger,
663666
)(processed.event, context);
664667

665668
destLogger.debug('push done');

packages/collector/src/push.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,15 @@ export function createPush<T extends Collector.Instance>(
4949
return await tryCatchAsync(
5050
async (): Promise<Elb.PushResult> => {
5151
const pushStart = Date.now();
52-
const { id, ingest, respond: initialRespond, mapping, preChain, include, exclude } =
53-
options;
52+
const {
53+
id,
54+
ingest,
55+
respond: initialRespond,
56+
mapping,
57+
preChain,
58+
include,
59+
exclude,
60+
} = options;
5461
let respond = initialRespond;
5562
let partialEvent = event;
5663

@@ -198,5 +205,6 @@ export function createPush<T extends Collector.Instance>(
198205
},
199206
'Push',
200207
collector.hooks,
208+
collector.logger,
201209
) as Collector.PushFn;
202210
}

packages/collector/src/store.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,24 @@ export async function initStores(
3232
const originalSet = instance.set;
3333
const originalDelete = instance.delete;
3434

35-
instance.get = useHooks(originalGet, 'StoreGet', collector.hooks);
36-
instance.set = useHooks(originalSet, 'StoreSet', collector.hooks);
37-
instance.delete = useHooks(originalDelete, 'StoreDelete', collector.hooks);
35+
instance.get = useHooks(
36+
originalGet,
37+
'StoreGet',
38+
collector.hooks,
39+
collector.logger,
40+
);
41+
instance.set = useHooks(
42+
originalSet,
43+
'StoreSet',
44+
collector.hooks,
45+
collector.logger,
46+
);
47+
instance.delete = useHooks(
48+
originalDelete,
49+
'StoreDelete',
50+
collector.hooks,
51+
collector.logger,
52+
);
3853

3954
result[storeId] = instance;
4055
}

packages/collector/src/transformer.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import {
4747
} from '@walkeros/core';
4848
import { getCacheStore } from './cache';
4949

50-
5150
/**
5251
* Extracts transformer next configuration for chain walking.
5352
* Maps transformer instances to their config.next values.
@@ -264,6 +263,7 @@ export async function transformerInit(
264263
transformer.init,
265264
'TransformerInit',
266265
collector.hooks,
266+
collector.logger,
267267
)(context);
268268

269269
// Check for initialization failure
@@ -325,6 +325,7 @@ export async function transformerPush(
325325
transformer.push,
326326
'TransformerPush',
327327
collector.hooks,
328+
collector.logger,
328329
)(event, context);
329330

330331
transformerLogger.debug('push done');
@@ -392,16 +393,23 @@ export async function runTransformerChain(
392393
}
393394

394395
// Path-specific mock check (takes precedence)
395-
if (chainContext && transformer.config?.chainMocks?.[chainContext] !== undefined) {
396+
if (
397+
chainContext &&
398+
transformer.config?.chainMocks?.[chainContext] !== undefined
399+
) {
396400
const chainMock = transformer.config.chainMocks[chainContext];
397-
collector.logger.scope(`transformer:${transformer.type || 'unknown'}`).debug('chainMock', { chain: chainContext });
401+
collector.logger
402+
.scope(`transformer:${transformer.type || 'unknown'}`)
403+
.debug('chainMock', { chain: chainContext });
398404
processedEvent = chainMock as WalkerOS.DeepPartialEvent;
399405
continue;
400406
}
401407

402408
// Global mock check
403409
if (transformer.config?.mock !== undefined) {
404-
collector.logger.scope(`transformer:${transformer.type || 'unknown'}`).debug('mock');
410+
collector.logger
411+
.scope(`transformer:${transformer.type || 'unknown'}`)
412+
.debug('mock');
405413
processedEvent = transformer.config.mock as WalkerOS.DeepPartialEvent;
406414
continue;
407415
}
@@ -433,7 +441,8 @@ export async function runTransformerChain(
433441

434442
if (cacheResult?.status === 'HIT' && cacheResult.value) {
435443
processedEvent = cacheResult.value as WalkerOS.DeepPartialEvent;
436-
if (compiledTCache.full) return { event: processedEvent, respond: currentRespond }; // full=true → stop chain
444+
if (compiledTCache.full)
445+
return { event: processedEvent, respond: currentRespond }; // full=true → stop chain
437446
continue; // full=false → next transformer
438447
}
439448

@@ -470,7 +479,11 @@ export async function runTransformerChain(
470479
currentRespond,
471480
chainContext,
472481
);
473-
if (beforeResult.event === null) return { event: null, respond: beforeResult.respond ?? currentRespond }; // Before chain stopped
482+
if (beforeResult.event === null)
483+
return {
484+
event: null,
485+
respond: beforeResult.respond ?? currentRespond,
486+
}; // Before chain stopped
474487
if (beforeResult.respond) currentRespond = beforeResult.respond;
475488
// Before chains use first result if fan-out occurred
476489
processedEvent = Array.isArray(beforeResult.event)
@@ -577,8 +590,10 @@ export async function runTransformerChain(
577590
flatEvents.push(fr as WalkerOS.DeepPartialEvent);
578591
}
579592
}
580-
if (flatEvents.length === 0) return { event: null, respond: lastForkRespond };
581-
if (flatEvents.length === 1) return { event: flatEvents[0], respond: lastForkRespond };
593+
if (flatEvents.length === 0)
594+
return { event: null, respond: lastForkRespond };
595+
if (flatEvents.length === 1)
596+
return { event: flatEvents[0], respond: lastForkRespond };
582597
return { event: flatEvents, respond: lastForkRespond };
583598
}
584599

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { Hooks, Logger } from '../types';
2+
import { useHooks } from '../useHooks';
3+
4+
describe('useHooks', () => {
5+
let warnSpy: jest.SpyInstance;
6+
7+
beforeEach(() => {
8+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
9+
});
10+
11+
afterEach(() => {
12+
warnSpy.mockRestore();
13+
});
14+
15+
test('calls function normally without hooks', () => {
16+
const fn = jest.fn((a: number, b: number) => a + b);
17+
const wrapped = useHooks(fn, 'Test', {});
18+
expect(wrapped(1, 2)).toBe(3);
19+
expect(fn).toHaveBeenCalledWith(1, 2);
20+
});
21+
22+
test('preHook wraps the function', () => {
23+
const fn = jest.fn((x: number) => x * 2);
24+
const preHook = jest.fn(
25+
({ fn }: { fn: (x: number) => number }, x: number) => fn(x + 10),
26+
);
27+
const wrapped = useHooks(fn, 'Test', {
28+
preTest: preHook,
29+
} as unknown as Hooks.Functions);
30+
expect(wrapped(5)).toBe(30);
31+
expect(preHook).toHaveBeenCalled();
32+
});
33+
34+
test('postHook receives result', () => {
35+
const fn = jest.fn((x: number) => x * 2);
36+
const postHook = jest.fn(
37+
(
38+
{ result }: { fn: (x: number) => number; result?: number },
39+
_x: number,
40+
) => (result || 0) + 1,
41+
);
42+
const wrapped = useHooks(fn, 'Test', {
43+
postTest: postHook,
44+
} as unknown as Hooks.Functions);
45+
expect(wrapped(5)).toBe(11);
46+
expect(postHook).toHaveBeenCalled();
47+
});
48+
49+
test('preHook error falls back to original fn and warns via console', () => {
50+
const fn = jest.fn((x: number) => x * 2);
51+
const preHook = jest.fn(() => {
52+
throw new Error('preHook exploded');
53+
});
54+
const wrapped = useHooks(fn, 'Test', {
55+
preTest: preHook,
56+
} as unknown as Hooks.Functions);
57+
58+
expect(wrapped(5)).toBe(10);
59+
expect(fn).toHaveBeenCalledWith(5);
60+
expect(warnSpy).toHaveBeenCalledWith(
61+
expect.stringContaining('preTest'),
62+
expect.any(Error),
63+
);
64+
});
65+
66+
test('postHook error returns original result and warns via console', () => {
67+
const fn = jest.fn((x: number) => x * 2);
68+
const postHook = jest.fn(() => {
69+
throw new Error('postHook exploded');
70+
});
71+
const wrapped = useHooks(fn, 'Test', {
72+
postTest: postHook,
73+
} as unknown as Hooks.Functions);
74+
75+
expect(wrapped(5)).toBe(10);
76+
expect(fn).toHaveBeenCalledWith(5);
77+
expect(warnSpy).toHaveBeenCalledWith(
78+
expect.stringContaining('postTest'),
79+
expect.any(Error),
80+
);
81+
});
82+
83+
test('both hooks erroring keeps pipeline alive', () => {
84+
const fn = jest.fn((x: number) => x * 2);
85+
const preHook = jest.fn(() => {
86+
throw new Error('pre');
87+
});
88+
const postHook = jest.fn(() => {
89+
throw new Error('post');
90+
});
91+
const wrapped = useHooks(fn, 'Test', {
92+
preTest: preHook,
93+
postTest: postHook,
94+
} as unknown as Hooks.Functions);
95+
96+
expect(wrapped(5)).toBe(10);
97+
expect(warnSpy).toHaveBeenCalledTimes(2);
98+
});
99+
100+
test('uses logger.warn instead of console when logger provided', () => {
101+
const fn = jest.fn((x: number) => x * 2);
102+
const preHook = jest.fn(() => {
103+
throw new Error('pre');
104+
});
105+
const mockLogger = {
106+
warn: jest.fn(),
107+
log: jest.fn(),
108+
debug: jest.fn(),
109+
error: jest.fn(),
110+
throw: jest.fn(),
111+
scope: jest.fn(),
112+
child: jest.fn(),
113+
} as unknown as Logger.Instance;
114+
115+
const wrapped = useHooks(
116+
fn,
117+
'Test',
118+
{ preTest: preHook } as unknown as Hooks.Functions,
119+
mockLogger,
120+
);
121+
122+
expect(wrapped(5)).toBe(10);
123+
expect(mockLogger.warn).toHaveBeenCalledWith(
124+
expect.stringContaining('preTest'),
125+
expect.objectContaining({ error: expect.any(Error) }),
126+
);
127+
expect(warnSpy).not.toHaveBeenCalled();
128+
});
129+
});

0 commit comments

Comments
 (0)