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 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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,27 @@ const tson = createTson({
// Pick which types you want to support
tsonSet,
],
// 🫷 Guard against unwanted values
guards: [tsonNumberGuard, tsonUnknownObjectGuard],
});

const scarryClass = new (class ScarryClass {
foo = "bar";
})();

const invalidNumber = 1 / 0;

const myObj = {
foo: "bar",
set: new Set([1, 2, 3]),
};

tson.stringify(scarryClass);
// -> throws, since we didn't register a serializer for `ScarryClass`!

tson.stringify(invalidNumber);
// -> throws, since we didn't register a serializer for `Infinity`!

const str = tson.stringify(myObj, 2);
console.log(str);
// (👀 All non-JSON values are replaced with a tuple, hence the name)
Expand Down
9 changes: 8 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 @@ -9,6 +11,7 @@
"pnpm-lock.yaml"
],
"words": [
"asyncs",
"clsx",
"Codecov",
"codespace",
Expand All @@ -24,13 +27,17 @@
"knip",
"lcov",
"markdownlintignore",
"marshaller",
"npmpackagejsonlintrc",
"openai",
"outro",
"packagejson",
"quickstart",
"streamified",
"streamify",
"stringifier",
"superjson",
"thunkable",
"tson",
"tsup",
"tupleson",
Expand Down
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>)[];
}
152 changes: 152 additions & 0 deletions src/async/asyncTypes2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
SerializedType,
TsonNonce,
TsonType,
TsonTypeTesterCustom,
} from "../sync/syncTypes.js";
import { TsonGuard } from "../tsonAssert.js";
import {
TsonAsyncUnfolderFactory,
createTsonAsyncUnfoldFn,
} from "./createUnfoldAsyncFn.js";

export const ChunkTypes = {
BODY: "BODY",
ERROR: "ERROR",
HEAD: "HEAD",
LEAF: "LEAF",
REF: "REF",
TAIL: "TAIL",
} as const;

export type ChunkTypes = {
[key in keyof typeof ChunkTypes]: (typeof ChunkTypes)[key];
};

export const TsonStatus = {
//MULTI_STATUS: 207,
ERROR: 500,
INCOMPLETE: 203,
OK: 200,
} as const;

export type TsonStatus = {
[key in keyof typeof TsonStatus]: (typeof TsonStatus)[key];
};

export const TsonStructures = {
ARRAY: 0,
ITERABLE: 2,
POJO: 1,
} as const;

export type TsonStructures = typeof TsonStructures;

export interface TsonAsyncChunk<T = unknown> {
chunk: T;
key?: null | number | string | undefined;
}

export interface TsonAsyncMarshaller<
TValue,
TSerializedType extends SerializedType,
> {
async: true;
fold: (
iter: AsyncGenerator<
TsonAsyncChunk<TSerializedType>,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
number | undefined | void,
undefined
>,
) => Promise<Awaited<TValue>>;
key: string;
unfold: ReturnType<
typeof createTsonAsyncUnfoldFn<TsonAsyncUnfolderFactory<TValue>>
>;
}

export type TsonAsyncType<
/**
* The type of the value
*/
TValue,
/**
* JSON-serializable value how it's stored after it's serialized
*/
TSerializedType extends SerializedType,
> = TsonTypeTesterCustom & TsonAsyncMarshaller<TValue, TSerializedType>;

export interface TsonAsyncOptions {
/**
* A list of guards to apply to every value
*/
guards?: TsonGuard<any>[];
/**
* The nonce function every time we start serializing a new object
* Should return a unique value every time it's called
* @default `${crypto.randomUUID} if available, otherwise a random string generated by Math.random`
*/
nonce?: () => string;
/**
* The list of types to use
*/
types: (TsonAsyncType<any, any> | TsonType<any, any>)[];
}

export type TsonAsyncTupleHeader = [
Id: `${TsonNonce}${number}`,
ParentId: `${TsonNonce}${"" | number}`,
Key?: null | number | string | undefined,
];

export type TsonAsyncLeafTuple = [
ChunkType: ChunkTypes["LEAF"],
Header: TsonAsyncTupleHeader,
Value: unknown,
TypeHandlerKey?: string | undefined,
];

export type TsonAsyncBodyTuple = [
ChunkType: ChunkTypes["BODY"],
Header: TsonAsyncTupleHeader,
Head: TsonAsyncHeadTuple,
TypeHandlerKey?: string | undefined,
];

export type TsonAsyncHeadTuple = [
ChunkType: ChunkTypes["HEAD"],
Header: TsonAsyncTupleHeader,
TypeHandlerKey?: TsonStructures[keyof TsonStructures] | string | undefined,
];

export type TsonAsyncReferenceTuple = [
ChunkType: ChunkTypes["REF"],
Header: TsonAsyncTupleHeader,
OriginalNodeId: `${TsonNonce}${number}`,
];

export type TsonAsyncErrorTuple = [
ChunkType: ChunkTypes["ERROR"],
Header: TsonAsyncTupleHeader,
Error: unknown,
];

export type TsonAsyncTailTuple = [
ChunkType: ChunkTypes["TAIL"],
Header: [
Id: TsonAsyncTupleHeader[0],
ParentId: TsonAsyncTupleHeader[1],
Key?: null | undefined,
],
StatusCode: number,
];

export type TsonAsyncTuple =
| TsonAsyncBodyTuple
| TsonAsyncErrorTuple
| TsonAsyncHeadTuple
| TsonAsyncLeafTuple
| TsonAsyncReferenceTuple
| TsonAsyncTailTuple;

120 changes: 120 additions & 0 deletions src/async/createFoldAsyncFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { TsonAbortError } from "./asyncErrors.js";
import {
TsonAsyncBodyTuple,
TsonAsyncTailTuple,
TsonAsyncTuple,
} from "./asyncTypes2.js";
import { TsonAsyncUnfoldedValue } from "./createUnfoldAsyncFn.js";
import { MaybePromise } from "./iterableUtils.js";

export type TsonAsyncReducer<TInitial> = (
ctx: TsonReducerCtx<TInitial>,
) => Promise<TsonAsyncReducerResult<TInitial>>;

export interface TsonAsyncReducerResult<TInitial> {
abort?: boolean;
accumulator: MaybePromise<TInitial>;
error?: any;
return?: TsonAsyncTailTuple | undefined;
}

export type TsonAsyncFoldFn = <TInitial>({
initialAccumulator,
reduce,
}: {
initialAccumulator: TInitial;
reduce: TsonAsyncReducer<TInitial>;
}) => (sequence: TsonAsyncUnfoldedValue) => Promise<TInitial>;

export type TsonReducerCtx<TAccumulator> =
| TsonAsyncReducerReturnCtx<TAccumulator>
| TsonAsyncReducerYieldCtx<TAccumulator>;

export type TsonAsyncFoldFnFactory = <TInitial>(opts: {
initialAccumulator?: TInitial | undefined;
}) => TsonAsyncFoldFn;

export const createTsonAsyncFoldFn = <TInitial>({
initializeAccumulator,
reduce,
}: {
initializeAccumulator: () => MaybePromise<TInitial>;
reduce: TsonAsyncReducer<TInitial>;
}) => {
//TODO: would it be better to use bigint for generator indexes? Can one imagine a request that long, with that many items?
let i = 0;

return async function fold(sequence: TsonAsyncUnfoldedValue) {
let result: {
abort?: boolean;
accumulator: MaybePromise<TInitial>;
error?: any;
return?: TsonAsyncTailTuple | undefined;
} = {
accumulator: initializeAccumulator(),
};

let current = await sequence.next();

if (current.done) {
const output = await reduce({
accumulator: await result.accumulator,
current,
key: i++,
source: sequence,
});

return output.accumulator;
}

while (!current.done) {
result = await reduce({
accumulator: await result.accumulator,
current,
key: i++,
source: sequence,
});

if (result.abort) {
if (result.return) {
current = await sequence.return(result.return);
}

current = await sequence.throw(result.error);

if (!current.done) {
throw new TsonAbortError(
"Unexpected result from `throw` in reducer: expected done",
);
}
} else {
current = await sequence.next();
}
}

const output = await reduce({
accumulator: await result.accumulator,
current,
key: i++,
source: sequence,
});

return output.accumulator;
};
};

interface TsonAsyncReducerYieldCtx<TAccumulator> {
accumulator: TAccumulator;
current: MaybePromise<
IteratorYieldResult<Exclude<TsonAsyncTuple, TsonAsyncBodyTuple>>
>;
key?: null | number | string | undefined;
source: TsonAsyncUnfoldedValue;
}

interface TsonAsyncReducerReturnCtx<TAccumulator> {
accumulator: TAccumulator;
current: MaybePromise<IteratorReturnResult<TsonAsyncTailTuple>>;
key?: null | number | string | undefined;
source?: TsonAsyncUnfoldedValue | undefined;
}

Check warning on line 120 in src/async/createFoldAsyncFn.ts

View check run for this annotation

Codecov / codecov/patch

src/async/createFoldAsyncFn.ts#L1-L120

Added lines #L1 - L120 were not covered by tests
Loading
Loading