From bfed75e70f5c57d28da2342ed62075eea828a67b Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:28:58 +1000 Subject: [PATCH 01/12] create a utility method to turn a node style callback into a promise --- src/util.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 312997c..70e66d3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,4 @@ -import Stream from "."; +import Stream, { type Atom } from "."; /** * Maybe it's a promise. Maybe it's not. Who's to say. @@ -30,3 +30,34 @@ export async function exhaust(iterable: AsyncIterable) { } } } + +/** + * Creates a `next` function and associated promise to promise-ify a node style callback. The + * `next` function must be passed as the callback to a function, and the resulting error or value + * will be emitted from the promise. The promise will always resolve. + * + * The error value of the callback (first parameter) will be emitted as an `Error` atom from the + * promise, whilst the value of the callback (second parameter) will be emitted as an `Ok` atom on + * the promise. + */ +export function createNodeCallback(): [Promise>, (error: E, value: T) => void] { + // Resolve function to be hoisted out of the promise + let resolve: (atom: Atom) => void; + + // Create the prom + const promise = new Promise>((res) => { + resolve = res; + }); + + // Create the next callback + const next = (err: E, value: T) => { + if (err) { + resolve(Stream.error(err)); + } else { + resolve(Stream.ok(value)); + } + }; + + // Return a tuple of the promise and next function + return [promise, next]; +} From 395784861e59d23f5fae0fe740e3fb55101a2079 Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:29:06 +1000 Subject: [PATCH 02/12] implement `fromCallback` --- src/stream/base.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/stream/base.ts b/src/stream/base.ts index 7fd3293..c635990 100644 --- a/src/stream/base.ts +++ b/src/stream/base.ts @@ -1,6 +1,7 @@ import { normalise, type Atom, type MaybeAtom, error, unknown } from "../atom"; import { Stream } from "."; import { Readable, Writable } from "stream"; +import { createNodeCallback } from "../util"; /** * Marker for the end of a stream. @@ -84,6 +85,25 @@ export class StreamBase { throw new TypeError("expected a promise, (async) iterator, or (async) iterable"); } + /** + * Create a stream from a node-style callback. A node-compatible callback function will be + * passed as the first parameter to the callback of this function. + * + * The first parameter provided to the callback (the `error`) will be emitted as an `Error` + * atom, whilst the second parameter (the `value`) will be emitted as an `Ok` atom. + * + * @group Creation + */ + static fromCallback(cb: (next: (error: E, value: T) => unknown) => void): Stream { + // Set up a next function + const [promise, next] = createNodeCallback(); + + // Run the callback + cb(next); + + return StreamBase.fromPromise(promise); + } + /** * Create a stream from a promise. The promise will be `await`ed, and the resulting value only * ever emitted once. From 064cf11660b12d1fd9caa9d5ee620a8491a4f0af Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:43:17 +1000 Subject: [PATCH 03/12] add some tests for `fromCallback` --- test/creation.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/creation.test.ts b/test/creation.test.ts index 60315f6..ac31cde 100644 --- a/test/creation.test.ts +++ b/test/creation.test.ts @@ -60,4 +60,43 @@ describe.concurrent("stream creation", () => { expect(await s.toArray({ atoms: true })).toEqual([$.ok(1), $.ok(2), $.ok(3)]); }); }); + + describe.concurrent("from callback", () => { + /** + * Sample function that accepts a node-style callback. + * + * @param success - Whether the method should succeed or fail. + * @param cb - Node-style callback to pass error or value to. + */ + function someNodeCallback( + success: boolean, + cb: (error: string | undefined, value?: number) => void, + ) { + if (success) { + cb(undefined, 123); + } else { + cb("an error"); + } + } + + test("value returned from callback", async ({ expect }) => { + expect.assertions(1); + + const s = $.fromCallback((next) => { + someNodeCallback(true, next); + }); + + expect(await s.toArray({ atoms: true })).toEqual([$.ok(123)]); + }); + + test("error returned from callback", async ({ expect }) => { + expect.assertions(1); + + const s = $.fromCallback((next) => { + someNodeCallback(false, next); + }); + + expect(await s.toArray({ atoms: true })).toEqual([$.error("an error")]); + }); + }); }); From 3ce4ff3320daf7a38348e965914b567eb3689283 Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:46:13 +1000 Subject: [PATCH 04/12] add changeset --- .changeset/green-suns-boil.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/green-suns-boil.md diff --git a/.changeset/green-suns-boil.md b/.changeset/green-suns-boil.md new file mode 100644 index 0000000..3f6c474 --- /dev/null +++ b/.changeset/green-suns-boil.md @@ -0,0 +1,5 @@ +--- +"windpipe": minor +--- + +implement `fromCallback` for stream creation From 7fe9df1d67443001b14ea6ff700b9a3d8beb7d6f Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 15:18:21 +1000 Subject: [PATCH 05/12] add an example to `fromCallback` --- src/stream/base.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stream/base.ts b/src/stream/base.ts index c635990..f7a202a 100644 --- a/src/stream/base.ts +++ b/src/stream/base.ts @@ -92,6 +92,9 @@ export class StreamBase { * The first parameter provided to the callback (the `error`) will be emitted as an `Error` * atom, whilst the second parameter (the `value`) will be emitted as an `Ok` atom. * + * @example + * $.fromCallback((next) => someAsyncMethod(paramA, paramB, next)); + * * @group Creation */ static fromCallback(cb: (next: (error: E, value: T) => unknown) => void): Stream { From d7a7c42df66d4bd83ad2d62e27077575ee8eeac7 Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 15:27:37 +1000 Subject: [PATCH 06/12] Apply suggestions from code review Co-authored-by: Ewan Breakey --- .changeset/green-suns-boil.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/green-suns-boil.md b/.changeset/green-suns-boil.md index 3f6c474..ab57108 100644 --- a/.changeset/green-suns-boil.md +++ b/.changeset/green-suns-boil.md @@ -2,4 +2,4 @@ "windpipe": minor --- -implement `fromCallback` for stream creation +Implement `fromCallback` for stream creation From 224c819c4fb34781cf5047539bb0976ebd1ae87c Mon Sep 17 00:00:00 2001 From: mdboon Date: Tue, 7 May 2024 16:01:02 +1000 Subject: [PATCH 07/12] Adds cachedFlatMap operator --- .changeset/tame-geese-allow.md | 5 ++ src/stream/higher-order.ts | 48 +++++++++++++++ test/higher-order.test.ts | 105 +++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 .changeset/tame-geese-allow.md diff --git a/.changeset/tame-geese-allow.md b/.changeset/tame-geese-allow.md new file mode 100644 index 0000000..94f72ea --- /dev/null +++ b/.changeset/tame-geese-allow.md @@ -0,0 +1,5 @@ +--- +"windpipe": minor +--- + +Adds the `cachedFlatMap` operator diff --git a/src/stream/higher-order.ts b/src/stream/higher-order.ts index 3aeb3b8..d8833f5 100644 --- a/src/stream/higher-order.ts +++ b/src/stream/higher-order.ts @@ -125,6 +125,54 @@ export class HigherOrderStream extends StreamTransforms { ); } + /** + * Map over each value in the stream, produce a stream from it, cache the resultant stream + * and flatten all the value streams together + * + * @group Higher Order + */ + cachedFlatMap( + cb: (value: T) => MaybePromise>, + keyFn: (value: T) => string | number | symbol, + ): Stream { + const trace = this.trace("cachedFlatMap"); + + return this.consume(async function* (it) { + const cache = new Map[]>(); + + for await (const atom of it) { + if (!isOk(atom)) { + yield atom; + continue; + } + + const key = keyFn(atom.value); + const cachedValues = cache.get(key); + + if (cachedValues !== undefined) { + yield* cachedValues; + continue; + } + + // Run the flat map handler + const streamAtom = await run(() => cb(atom.value), trace); + + // If an error was emitted whilst initialising the new stream, return it + if (!isOk(streamAtom)) { + yield streamAtom; + continue; + } + + // Otherwise, consume the iterator + const values = await streamAtom.value.toArray({ atoms: true }); + + cache.set(key, values); + + yield* values; + } + }); + } + /** * Produce a new stream from the stream that has any nested streams flattened * diff --git a/test/higher-order.test.ts b/test/higher-order.test.ts index 6957b32..3d6701a 100644 --- a/test/higher-order.test.ts +++ b/test/higher-order.test.ts @@ -127,6 +127,111 @@ describe.concurrent("higher order streams", () => { }); }); + describe.concurrent("cachedFlatMap", () => { + test("lookup non-repeating strings returning single atom", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((param: string) => $.of(param)); + + const s = $.from(["a", "b", "c"]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([$.ok("a"), $.ok("b"), $.ok("c")]); + expect(lookup).toBeCalledTimes(3); + }); + + test("lookup repeating strings returning single atom", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((param: string) => $.of(param)); + + const s = $.from(["a", "b", "c", "a", "a"]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok("a"), + $.ok("b"), + $.ok("c"), + $.ok("a"), + $.ok("a"), + ]); + expect(lookup).toBeCalledTimes(3); + }); + + test("lookup repeating numbers returning multiple atoms", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((n: number) => $.fromArray([n, n * 2, n * 4])); + + const s = $.from([1, 100, 200, 1, 10]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(1), + $.ok(2), + $.ok(4), + $.ok(100), + $.ok(200), + $.ok(400), + $.ok(200), + $.ok(400), + $.ok(800), + $.ok(1), + $.ok(2), + $.ok(4), + $.ok(10), + $.ok(20), + $.ok(40), + ]); + expect(lookup).toBeCalledTimes(4); + }); + + test("lookup repeating numbers returning multiple atoms", async ({ expect }) => { + expect.assertions(2); + + const oneHundredDividedBy = vi.fn((n: number) => { + if (n === 0) { + throw "Cannot divide by zero!"; + } + + return $.of(100 / n); + }); + + const s = $.from([5, 0, 50, 5, 5]).cachedFlatMap(oneHundredDividedBy, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(20), + $.unknown("Cannot divide by zero!", ["cachedFlatMap"]), + $.ok(2), + $.ok(20), + $.ok(20), + ]); + expect(oneHundredDividedBy).toBeCalledTimes(3); + }); + + test("lookup repeating numbers, including an error, returning multiple atoms", async ({ + expect, + }) => { + expect.assertions(2); + + const lookup = vi.fn((n: number) => $.of(n)); + + const s = $.from([ + $.ok(1), + $.ok(2), + $.error("oh no!"), + $.ok(2), + $.ok(1), + ]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(1), + $.ok(2), + $.error("oh no!"), + $.ok(2), + $.ok(1), + ]); + expect(lookup).toBeCalledTimes(2); + }); + }); + describe.concurrent("flatten", () => { test("simple nested stream", async ({ expect }) => { expect.assertions(1); From 83ce7eeb6f40ec277e161ac0df3e45467dd12b02 Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:09:08 +1000 Subject: [PATCH 08/12] catch unhandled errors in `fromNext` implementation --- src/index.ts | 2 +- src/stream/base.ts | 18 ++++++++----- src/stream/index.ts | 2 +- test/creation.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index a4e9d0a..23ae95a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,6 @@ export type { export type { MaybePromise, Truthy, CallbackOrStream } from "./util"; // Export the `StreamEnd` type -export type { StreamEnd } from "./stream"; +export { StreamEnd } from "./stream"; export default Stream; diff --git a/src/stream/base.ts b/src/stream/base.ts index f7a202a..21198a0 100644 --- a/src/stream/base.ts +++ b/src/stream/base.ts @@ -195,12 +195,18 @@ export class StreamBase { new Readable({ objectMode: true, async read() { - const value = await next(); - - if (value === StreamEnd) { - this.push(null); - } else { - this.push(normalise(value)); + try { + const value = await next(); + + // Promise returned as normal + if (value === StreamEnd) { + this.push(null); + } else { + this.push(normalise(value)); + } + } catch (e) { + // Promise was rejected, add as an unknown error + this.push(unknown(e, [])); } }, }), diff --git a/src/stream/index.ts b/src/stream/index.ts index af53647..0c317aa 100644 --- a/src/stream/index.ts +++ b/src/stream/index.ts @@ -12,7 +12,7 @@ import { } from "../atom"; import { HigherOrderStream } from "./higher-order"; -export type { StreamEnd } from "./base"; +export { StreamEnd } from "./base"; /** * @template T - Type of the 'values' on the stream. diff --git a/test/creation.test.ts b/test/creation.test.ts index ac31cde..dbb07a3 100644 --- a/test/creation.test.ts +++ b/test/creation.test.ts @@ -1,5 +1,5 @@ import { describe, test } from "vitest"; -import $ from "../src"; +import $, { StreamEnd } from "../src"; import { Readable } from "stream"; describe.concurrent("stream creation", () => { @@ -99,4 +99,65 @@ describe.concurrent("stream creation", () => { expect(await s.toArray({ atoms: true })).toEqual([$.error("an error")]); }); }); + + describe.concurrent("from next function", () => { + test("simple count up", async ({ expect }) => { + expect.assertions(1); + + let i = 0; + const s = $.fromNext(async () => { + if (i < 4) { + return i++; + } else { + return StreamEnd; + } + }); + + expect(await s.toArray({ atoms: true })).toEqual([$.ok(0), $.ok(1), $.ok(2), $.ok(3)]); + }); + + test("next atoms produces atoms", async ({ expect }) => { + expect.assertions(1); + + const atoms = [$.ok(0), $.error("some error"), $.ok(1), $.unknown("unknown error", [])]; + const s = $.fromNext(async () => { + if (atoms.length > 0) { + return atoms.shift(); + } else { + return StreamEnd; + } + }); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(0), + $.error("some error"), + $.ok(1), + $.unknown("unknown error", []), + ]); + }); + + test("next catches unhandled errors", async ({ expect }) => { + expect.assertions(1); + + let i = 0; + const s = $.fromNext(async () => { + i += 1; + + if (i === 1) { + throw "some error"; + } + + if (i == 2) { + return i; + } + + return StreamEnd; + }); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.unknown("some error", []), + $.ok(2), + ]); + }); + }); }); From af01d2f94abe27d13f84e58dbbeed2c4550ddb0c Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Tue, 7 May 2024 11:46:38 +1000 Subject: [PATCH 09/12] add changeset --- .changeset/bright-kangaroos-change.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-kangaroos-change.md diff --git a/.changeset/bright-kangaroos-change.md b/.changeset/bright-kangaroos-change.md new file mode 100644 index 0000000..0f3eb4a --- /dev/null +++ b/.changeset/bright-kangaroos-change.md @@ -0,0 +1,5 @@ +--- +"windpipe": patch +--- + +catch unhandled errors in `fromNext` stream creation From b42d3bdd04a698eace966e1c8247ceae3f7a25a5 Mon Sep 17 00:00:00 2001 From: Mark Boon <42232000+mdboon@users.noreply.github.com> Date: Tue, 7 May 2024 16:13:31 +1000 Subject: [PATCH 10/12] Update src/stream/higher-order.ts Co-authored-by: Ewan Breakey --- src/stream/higher-order.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stream/higher-order.ts b/src/stream/higher-order.ts index d8833f5..a2e8a95 100644 --- a/src/stream/higher-order.ts +++ b/src/stream/higher-order.ts @@ -138,7 +138,7 @@ export class HigherOrderStream extends StreamTransforms { const trace = this.trace("cachedFlatMap"); return this.consume(async function* (it) { - const cache = new Map[]>(); + const cache = new Map[]>(); for await (const atom of it) { if (!isOk(atom)) { From 909d5a1dcc823b0e8c853fc7cf453b010cbc7ee5 Mon Sep 17 00:00:00 2001 From: mdboon Date: Tue, 7 May 2024 16:01:02 +1000 Subject: [PATCH 11/12] Adds cachedFlatMap operator --- .changeset/tame-geese-allow.md | 5 ++ src/stream/higher-order.ts | 48 +++++++++++++++ test/higher-order.test.ts | 105 +++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 .changeset/tame-geese-allow.md diff --git a/.changeset/tame-geese-allow.md b/.changeset/tame-geese-allow.md new file mode 100644 index 0000000..94f72ea --- /dev/null +++ b/.changeset/tame-geese-allow.md @@ -0,0 +1,5 @@ +--- +"windpipe": minor +--- + +Adds the `cachedFlatMap` operator diff --git a/src/stream/higher-order.ts b/src/stream/higher-order.ts index 3aeb3b8..d8833f5 100644 --- a/src/stream/higher-order.ts +++ b/src/stream/higher-order.ts @@ -125,6 +125,54 @@ export class HigherOrderStream extends StreamTransforms { ); } + /** + * Map over each value in the stream, produce a stream from it, cache the resultant stream + * and flatten all the value streams together + * + * @group Higher Order + */ + cachedFlatMap( + cb: (value: T) => MaybePromise>, + keyFn: (value: T) => string | number | symbol, + ): Stream { + const trace = this.trace("cachedFlatMap"); + + return this.consume(async function* (it) { + const cache = new Map[]>(); + + for await (const atom of it) { + if (!isOk(atom)) { + yield atom; + continue; + } + + const key = keyFn(atom.value); + const cachedValues = cache.get(key); + + if (cachedValues !== undefined) { + yield* cachedValues; + continue; + } + + // Run the flat map handler + const streamAtom = await run(() => cb(atom.value), trace); + + // If an error was emitted whilst initialising the new stream, return it + if (!isOk(streamAtom)) { + yield streamAtom; + continue; + } + + // Otherwise, consume the iterator + const values = await streamAtom.value.toArray({ atoms: true }); + + cache.set(key, values); + + yield* values; + } + }); + } + /** * Produce a new stream from the stream that has any nested streams flattened * diff --git a/test/higher-order.test.ts b/test/higher-order.test.ts index 6957b32..3d6701a 100644 --- a/test/higher-order.test.ts +++ b/test/higher-order.test.ts @@ -127,6 +127,111 @@ describe.concurrent("higher order streams", () => { }); }); + describe.concurrent("cachedFlatMap", () => { + test("lookup non-repeating strings returning single atom", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((param: string) => $.of(param)); + + const s = $.from(["a", "b", "c"]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([$.ok("a"), $.ok("b"), $.ok("c")]); + expect(lookup).toBeCalledTimes(3); + }); + + test("lookup repeating strings returning single atom", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((param: string) => $.of(param)); + + const s = $.from(["a", "b", "c", "a", "a"]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok("a"), + $.ok("b"), + $.ok("c"), + $.ok("a"), + $.ok("a"), + ]); + expect(lookup).toBeCalledTimes(3); + }); + + test("lookup repeating numbers returning multiple atoms", async ({ expect }) => { + expect.assertions(2); + + const lookup = vi.fn((n: number) => $.fromArray([n, n * 2, n * 4])); + + const s = $.from([1, 100, 200, 1, 10]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(1), + $.ok(2), + $.ok(4), + $.ok(100), + $.ok(200), + $.ok(400), + $.ok(200), + $.ok(400), + $.ok(800), + $.ok(1), + $.ok(2), + $.ok(4), + $.ok(10), + $.ok(20), + $.ok(40), + ]); + expect(lookup).toBeCalledTimes(4); + }); + + test("lookup repeating numbers returning multiple atoms", async ({ expect }) => { + expect.assertions(2); + + const oneHundredDividedBy = vi.fn((n: number) => { + if (n === 0) { + throw "Cannot divide by zero!"; + } + + return $.of(100 / n); + }); + + const s = $.from([5, 0, 50, 5, 5]).cachedFlatMap(oneHundredDividedBy, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(20), + $.unknown("Cannot divide by zero!", ["cachedFlatMap"]), + $.ok(2), + $.ok(20), + $.ok(20), + ]); + expect(oneHundredDividedBy).toBeCalledTimes(3); + }); + + test("lookup repeating numbers, including an error, returning multiple atoms", async ({ + expect, + }) => { + expect.assertions(2); + + const lookup = vi.fn((n: number) => $.of(n)); + + const s = $.from([ + $.ok(1), + $.ok(2), + $.error("oh no!"), + $.ok(2), + $.ok(1), + ]).cachedFlatMap(lookup, (v) => v); + + expect(await s.toArray({ atoms: true })).toEqual([ + $.ok(1), + $.ok(2), + $.error("oh no!"), + $.ok(2), + $.ok(1), + ]); + expect(lookup).toBeCalledTimes(2); + }); + }); + describe.concurrent("flatten", () => { test("simple nested stream", async ({ expect }) => { expect.assertions(1); From 82ca434a459ac9d1ec64e8118d34b33454a2888d Mon Sep 17 00:00:00 2001 From: Mark Boon <42232000+mdboon@users.noreply.github.com> Date: Tue, 7 May 2024 16:13:31 +1000 Subject: [PATCH 12/12] Update src/stream/higher-order.ts Co-authored-by: Ewan Breakey --- src/stream/higher-order.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stream/higher-order.ts b/src/stream/higher-order.ts index d8833f5..a2e8a95 100644 --- a/src/stream/higher-order.ts +++ b/src/stream/higher-order.ts @@ -138,7 +138,7 @@ export class HigherOrderStream extends StreamTransforms { const trace = this.trace("cachedFlatMap"); return this.consume(async function* (it) { - const cache = new Map[]>(); + const cache = new Map[]>(); for await (const atom of it) { if (!isOk(atom)) {