Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1562,9 +1562,9 @@ const logEffect = (level: LogLevel): EffectFactory<Logging> =>
*defaultHandler({ resume }, ...args) {
const logger = yield* askLogger();
const result = logger[level](...args);
// Handle async loggers
if (result instanceof Promise) result.then(resume);
else resume();
// Wait for async loggers before resuming
if (result instanceof Promise) yield* effectify(result);
resume();
},
});

Expand All @@ -1575,7 +1575,7 @@ export const logWarn = logEffect("warn");
export const logError = logEffect("error");
```

We define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime.
We define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime. Asynchronous logger results are yielded through `effectify`, so `runAsync()` waits for them and rejects with the same error if the logger Promise rejects.

**Step 3: Define handlers for common logging features**

Expand Down
4 changes: 2 additions & 2 deletions test/README.example.proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1134,8 +1134,8 @@ test("Example: Build a configurable logging system with effects", () => {
*defaultHandler({ resume }, ...args) {
const logger = yield* askLogger();
const result = logger[level](...args);
if (result instanceof Promise) void result.then(resume);
else resume();
if (result instanceof Promise) yield* effectify(result);
resume();
},
});

Expand Down
70 changes: 68 additions & 2 deletions test/README.example.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1800,8 +1800,8 @@ test("Example: Build a configurable logging system with effects", async () => {
*defaultHandler({ resume }, ...args) {
const logger = yield* askLogger();
const result = logger[level](...args);
if (result instanceof Promise) void result.then(resume);
else resume();
if (result instanceof Promise) yield* effectify(result);
resume();
},
});

Expand Down Expand Up @@ -1961,4 +1961,70 @@ test("Example: Build a configurable logging system with effects", async () => {
]);
logs.length = 0;
fileLogs.length = 0;

// Synchronous loggers still support runSync.
const syncResult = effected(function* () {
yield* logInfo("Sync message");
return "done";
}).runSync();
expect(syncResult).toBe("done");
expect(logs).toEqual([["info", "Sync message"]]);

// A pending logger must finish before the program resumes.
{
let finishWrite!: () => void;
const write = new Promise<void>((resolve) => {
finishWrite = resolve;
});
const logger = { ...mockConsole, info: vi.fn(() => write) };
const continued = vi.fn();
const result = effected(function* () {
yield* logInfo("Async message");
continued();
return "done";
})
.provide("logger", logger)
.runAsync();

expect(logger.info).toHaveBeenCalledExactlyOnceWith("Async message");
expect(continued).not.toHaveBeenCalled();
finishWrite();
await expect(result).resolves.toBe("done");
expect(continued).toHaveBeenCalledOnce();
}

// Logging failures must reject the public Promise without an unhandled derivative.
{
let failWrite!: (reason: Error) => void;
const write = new Promise<void>((_, reject) => {
failWrite = reject;
});
const failure = new Error("Logging failed");
const logger = { ...mockConsole, info: vi.fn(() => write) };
const continued = vi.fn();
const unhandled = vi.fn();
process.on("unhandledRejection", unhandled);
try {
const result = effected(function* () {
yield* logInfo("Failed message");
continued();
})
.provide("logger", logger)
.runAsync();
const fulfilled = vi.fn();
const rejected = vi.fn();
void result.then(fulfilled, rejected);

expect(logger.info).toHaveBeenCalledExactlyOnceWith("Failed message");
failWrite(failure);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(rejected).toHaveBeenCalledOnce();
expect(rejected.mock.calls[0]?.[0]).toBe(failure);
expect(fulfilled).not.toHaveBeenCalled();
expect(continued).not.toHaveBeenCalled();
expect(unhandled).not.toHaveBeenCalled();
} finally {
process.off("unhandledRejection", unhandled);
}
}
});
Loading