Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Sync API Refactor #87

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 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: 7 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"dictionaries": ["typescript"],
"dictionaries": [
"typescript"
],
"ignorePaths": [
".github",
"CHANGELOG.md",
Expand All @@ -24,13 +26,17 @@
"knip",
"lcov",
"markdownlintignore",
"marshaller",
"npmpackagejsonlintrc",
"openai",
"outro",
"packagejson",
"quickstart",
"streamified",
"streamify",
"stringifier",
"superjson",
"thunkable",
"tson",
"tsup",
"tupleson",
Expand Down
31 changes: 31 additions & 0 deletions src/async/asyncHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export async function* mapIterable<T, TValue>(
iterable: AsyncIterable<T>,
fn: (v: T) => TValue,
): AsyncIterable<TValue> {
for await (const value of iterable) {
yield fn(value);
}
}

export async function reduceIterable<
T,
TInitialValue extends Promise<any> = Promise<T>,
TKey extends PropertyKey | bigint = bigint,
TKeyFn extends (prev?: TKey) => TKey = (prev?: TKey) => TKey,
>(
iterable: Iterable<T>,
fn: (acc: Awaited<TInitialValue>, v: T, i: TKey) => Awaited<TInitialValue>,
initialValue: TInitialValue = Promise.resolve() as TInitialValue,
incrementKey: TKeyFn = ((prev?: bigint) =>
prev === undefined ? 0n : prev + 1n) as TKeyFn,
): Promise<Awaited<TInitialValue>> {
let acc = initialValue;
let i = incrementKey();

for await (const value of iterable) {
acc = fn(await acc, value, i);
i = incrementKey(i);
}

return Promise.resolve(acc);
}

Check warning on line 31 in src/async/asyncHelpers.ts

View check run for this annotation

Codecov / codecov/patch

src/async/asyncHelpers.ts#L1-L31

Added lines #L1 - L31 were not covered by tests
6 changes: 1 addition & 5 deletions src/async/asyncTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,5 @@ export interface TsonAsyncOptions {
/**
* The list of types to use
*/
types: (
| TsonAsyncType<any, any>
| TsonType<any, any>
| TsonType<any, never>
)[];
types: (TsonAsyncType<any, any> | TsonType<any, any>)[];
}
11 changes: 4 additions & 7 deletions src/async/deserializeAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { assert } from "../internals/assert.js";
import { isTsonTuple } from "../internals/isTsonTuple.js";
import { mapOrReturn } from "../internals/mapOrReturn.js";
import {
TsonMarshaller,
TsonNonce,
TsonSerialized,
TsonTransformerSerializeDeserialize,
} from "../sync/syncTypes.js";
import { TsonAbortError, TsonStreamInterruptedError } from "./asyncErrors.js";
import {
Expand All @@ -27,9 +27,7 @@ import { TsonAsyncValueTuple } from "./serializeAsync.js";
type WalkFn = (value: unknown) => unknown;
type WalkerFactory = (nonce: TsonNonce) => WalkFn;

type AnyTsonTransformerSerializeDeserialize =
| TsonAsyncType<any, any>
| TsonTransformerSerializeDeserialize<any, any>;
type AnyTsonMarshaller = TsonAsyncType<any, any> | TsonMarshaller<any, any>;

export interface TsonParseAsyncOptions {
/**
Expand All @@ -56,16 +54,15 @@ type TsonParseAsync = <TValue>(
type TsonDeserializeIterableValue = TsonAsyncValueTuple | TsonSerialized;
type TsonDeserializeIterable = AsyncIterable<TsonDeserializeIterableValue>;
function createTsonDeserializer(opts: TsonAsyncOptions) {
const typeByKey: Record<string, AnyTsonTransformerSerializeDeserialize> = {};
const typeByKey: Record<string, AnyTsonMarshaller> = {};

for (const handler of opts.types) {
if (handler.key) {
if (typeByKey[handler.key]) {
throw new Error(`Multiple handlers for key ${handler.key} found`);
}

typeByKey[handler.key] =
handler as AnyTsonTransformerSerializeDeserialize;
typeByKey[handler.key] = handler as AnyTsonMarshaller;
}
}

Expand Down
96 changes: 96 additions & 0 deletions src/async/iterableUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
NodeJSReadableStreamEsque,
WebReadableStreamEsque,
} from "../internals/esque.js";
import { AsyncGenerator, Generator } from "../iterableTypes.js";

export async function* readableStreamToAsyncIterable<T>(
stream:
Expand Down Expand Up @@ -150,3 +151,98 @@

return `${key}: ${value as any}\n`;
}

export interface AsyncIterableEsque<T = unknown> {
[Symbol.asyncIterator](): AsyncIterator<T>;
}

export function isAsyncIterableEsque(
maybeAsyncIterable: unknown,
): maybeAsyncIterable is AsyncIterableEsque {
return (
!!maybeAsyncIterable &&
(typeof maybeAsyncIterable === "object" ||
typeof maybeAsyncIterable === "function") &&
Symbol.asyncIterator in maybeAsyncIterable
);
}

Check warning on line 168 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L160-L168

Added lines #L160 - L168 were not covered by tests

export interface IterableEsque<T = unknown> {
[Symbol.iterator](): Iterator<T>;
}

export function isIterableEsque(
maybeIterable: unknown,
): maybeIterable is IterableEsque {
return (
!!maybeIterable &&
(typeof maybeIterable === "object" ||
typeof maybeIterable === "function") &&
Symbol.iterator in maybeIterable
);
}

Check warning on line 183 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L175-L183

Added lines #L175 - L183 were not covered by tests

type SyncOrAsyncGeneratorFnEsque = AsyncGeneratorFnEsque | GeneratorFnEsque;

export function isMaybeAsyncGeneratorFn(
maybeAsyncGeneratorFn: unknown,
): maybeAsyncGeneratorFn is SyncOrAsyncGeneratorFnEsque {
return (
typeof maybeAsyncGeneratorFn === "function" &&
["AsyncGeneratorFunction", "GeneratorFunction"].includes(
maybeAsyncGeneratorFn.constructor.name,
)
);
}

Check warning on line 196 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L188-L196

Added lines #L188 - L196 were not covered by tests

export type GeneratorFnEsque = () => Generator;

export function isGeneratorFnEsque(
maybeGeneratorFn: unknown,
): maybeGeneratorFn is GeneratorFnEsque {
return (
typeof maybeGeneratorFn === "function" &&
maybeGeneratorFn.constructor.name === "GeneratorFunction"
);
}

Check warning on line 207 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L201-L207

Added lines #L201 - L207 were not covered by tests

export type AsyncGeneratorFnEsque = () => AsyncGenerator;

export function isAsyncGeneratorFnEsque(
maybeAsyncGeneratorFn: unknown,
): maybeAsyncGeneratorFn is AsyncGeneratorFnEsque {
return (
typeof maybeAsyncGeneratorFn === "function" &&
maybeAsyncGeneratorFn.constructor.name === "AsyncGeneratorFunction"
);
}

Check warning on line 218 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L212-L218

Added lines #L212 - L218 were not covered by tests

export type PromiseEsque = PromiseLike<unknown>;

export function isPromiseEsque(
maybePromise: unknown,
): maybePromise is PromiseEsque {
return (
!!maybePromise &&
typeof maybePromise === "object" &&
"then" in maybePromise &&
typeof maybePromise.then === "function"
);
}

Check warning on line 231 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L223-L231

Added lines #L223 - L231 were not covered by tests

export type ThunkEsque = () => unknown;

export function isThunkEsque(maybeThunk: unknown): maybeThunk is ThunkEsque {
return (
!!maybeThunk && typeof maybeThunk === "function" && maybeThunk.length === 0
);
}

Check warning on line 239 in src/async/iterableUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/async/iterableUtils.ts#L236-L239

Added lines #L236 - L239 were not covered by tests

export type Thunkable =
| AsyncIterableEsque
| IterableEsque
| PromiseEsque
| SyncOrAsyncGeneratorFnEsque
| ThunkEsque;

export type MaybePromise<T> = Promise<T> | T;
41 changes: 21 additions & 20 deletions src/async/serializeAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function walkerFactory(nonce: TsonNonce, types: TsonAsyncOptions["types"]) {

const iterators = new Map<TsonAsyncIndex, AsyncIterator<unknown>>();

const iterator = {
const iterable: AsyncIterable<TsonAsyncValueTuple> = {
async *[Symbol.asyncIterator]() {
// race all active iterators and yield next value as they come
// when one iterator is done, remove it from the list
Expand Down Expand Up @@ -94,24 +94,25 @@ function walkerFactory(nonce: TsonNonce, types: TsonAsyncOptions["types"]) {
walk: WalkFn,
) => TsonSerializedValue;

const $serialize: Serializer = handler.serializeIterator
? (value): TsonTuple => {
const idx = asyncIndex++ as TsonAsyncIndex;

const iterator = handler.serializeIterator({
value,
});
iterators.set(idx, iterator[Symbol.asyncIterator]());

return [handler.key as TsonTypeHandlerKey, idx, nonce];
}
: handler.serialize
? (value, nonce, walk): TsonTuple => [
handler.key as TsonTypeHandlerKey,
walk(handler.serialize(value)),
nonce,
]
: (value, _nonce, walk) => walk(value);
const $serialize: Serializer =
"serializeIterator" in handler
? (value): TsonTuple => {
const idx = asyncIndex++ as TsonAsyncIndex;

const iterator = handler.serializeIterator({
value,
});
iterators.set(idx, iterator[Symbol.asyncIterator]());

return [handler.key as TsonTypeHandlerKey, idx, nonce];
}
: "serialize" in handler
? (value, nonce, walk): TsonTuple => [
handler.key as TsonTypeHandlerKey,
walk(handler.serialize(value)),
nonce,
]
: (value, _nonce, walk) => walk(value);
return {
...handler,
$serialize,
Expand Down Expand Up @@ -185,7 +186,7 @@ function walkerFactory(nonce: TsonNonce, types: TsonAsyncOptions["types"]) {
return cacheAndReturn(mapOrReturn(value, walk));
};

return [walk, iterator] as const;
return [walk, iterable] as const;
}

type TsonAsyncSerializer = <T>(
Expand Down
8 changes: 4 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { TsonOptions, TsonType, createTson } from "./index.js";
import { expectError, waitError } from "./internals/testUtils.js";

test("multiple handlers for primitive string found", () => {
const stringHandler: TsonType<string, never> = {
const stringHandler = {
primitive: "string",
};
} as TsonType<string, string>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent to the other way around. Did you want to use the satisfies operator?

Copy link
Contributor Author

@helmturner helmturner Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is necessary due to the changes in src/sync/syncTypes.ts (sorry for the slow reply!)

const opts: TsonOptions = {
types: [stringHandler, stringHandler],
};
Expand Down Expand Up @@ -98,9 +98,9 @@ test("async: duplicate keys", async () => {
});

test("async: multiple handlers for primitive string found", async () => {
const stringHandler: TsonType<string, never> = {
const stringHandler = {
primitive: "string",
};
} as TsonType<string, string>;

const err = await waitError(async () => {
const iterator = createTsonAsync({
Expand Down
3 changes: 3 additions & 0 deletions src/internals/isComplexValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isComplexValue(arg: unknown): arg is object {
return (arg !== null && typeof arg === "object") || typeof arg === "function";
}
4 changes: 3 additions & 1 deletion src/internals/isPlainObject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const isPlainObject = (obj: unknown): obj is Record<string, unknown> => {
export const isPlainObject = (
obj: unknown,
): obj is Record<PropertyKey, unknown> => {
if (!obj || typeof obj !== "object") {
return false;
}
Expand Down
58 changes: 58 additions & 0 deletions src/iterableTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as v from "vitest";

import { AsyncGenerator, Generator } from "./iterableTypes.js";

v.describe("Async Iterable Types", () => {
v.it("should be interchangeable with the original type signatures", () => {
const generator = (async function* () {
await Promise.resolve();
yield 1;
yield 2;
yield 3;
})();

v.expectTypeOf(generator).toMatchTypeOf<AsyncGenerator<number>>();

const iterable = {
[Symbol.asyncIterator]: () => generator,
};

v.expectTypeOf(iterable).toMatchTypeOf<AsyncIterable<number>>();

const iterableIterator = iterable[Symbol.asyncIterator]();

v.expectTypeOf(iterableIterator).toMatchTypeOf<
AsyncIterableIterator<number>
>();

const iterator = iterableIterator[Symbol.asyncIterator]();

v.expectTypeOf(iterator).toMatchTypeOf<AsyncIterableIterator<number>>();
});
});

v.describe("Iterable Types", () => {
v.it("should be interchangeable with the original type signatures", () => {
const generator = (function* () {
yield 1;
yield 2;
yield 3;
})();

v.expectTypeOf(generator).toMatchTypeOf<Generator<number>>();

const iterable = {
[Symbol.iterator]: () => generator,
};

v.expectTypeOf(iterable).toMatchTypeOf<Iterable<number>>();

const iterableIterator = iterable[Symbol.iterator]();

v.expectTypeOf(iterableIterator).toMatchTypeOf<IterableIterator<number>>();

const iterator = iterableIterator[Symbol.iterator]();

v.expectTypeOf(iterator).toMatchTypeOf<IterableIterator<number>>();
});
});
Loading
Loading