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

Commit

Permalink
promise rejects
Browse files Browse the repository at this point in the history
  • Loading branch information
KATT committed Oct 3, 2023
1 parent 5039eb3 commit 592cc1d
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 32 deletions.
5 changes: 4 additions & 1 deletion src/createTsonAsync.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { createAsyncTsonSerializer } from "./serializeAsync.js";
import { TsonAsyncOptions } from "./types.js";

export const createTsonAsync = (opts: TsonAsyncOptions) => ({});
export const createTsonAsync = (opts: TsonAsyncOptions) => ({
serializeAsync: createAsyncTsonSerializer(opts),
});
12 changes: 7 additions & 5 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
export class TsonError extends Error {
constructor(message: string, opts?: ErrorOptions) {
super(message, opts);
this.name = this.constructor.name;
this.name = "TsonError";

// set prototype
}
}

export class CircularReferenceError extends TsonError {
export class TsonCircularReferenceError extends TsonError {
/**
* The circular reference that was found
*/
public readonly value;

constructor(value: unknown) {
super(`Circular reference detected`);
this.name = this.constructor.name;
this.name = "TsonCircularReferenceError";
this.value = value;
}
}

export class PromiseRejectionError extends TsonError {
export class TsonPromiseRejectionError extends TsonError {
constructor(cause: unknown) {
super(`Promise rejected`, { cause });
this.name = this.constructor.name;
this.name = "TsonPromiseRejectionError";
}
}
142 changes: 137 additions & 5 deletions src/handlers/tsonPromise.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,146 @@
import { test } from "vitest";
import { expect, test } from "vitest";

import { createTson, createTsonAsync, tsonPromise } from "../index.js";
import { createTsonAsync, tsonPromise } from "../index.js";

test("tsonPromise", async () => {
const createPromise = <T>(result: () => T) => {
return new Promise<T>((resolve) => {
setTimeout(() => {
resolve(result());
}, 1);
});
};

test("serialize promise", async () => {
const tson = createTsonAsync({
nonce: () => "__tson",
types: [tsonPromise],
});

const promise = Promise.resolve(42);

const serialized = tson.stringify(promise);
const deserialized = tson.parse(serialized);
const [head, iterator] = tson.serializeAsync(promise);

expect(head).toMatchInlineSnapshot(`
{
"json": [
"Promise",
0,
"__tson",
],
"nonce": "__tson",
}
`);

const values = [];
for await (const value of iterator) {
values.push(value);
}

expect(values).toMatchInlineSnapshot(`
[
[
0,
0,
42,
],
]
`);
});

test("serialize promise that returns a promise", async () => {
const tson = createTsonAsync({
nonce: () => "__tson",
types: [tsonPromise],
});

const obj = {
promise: createPromise(() => {
return {
anotherPromise: createPromise(() => {
return 42;
}),
};
}),
};

const [head, iterator] = tson.serializeAsync(obj);

expect(head).toMatchInlineSnapshot(`
{
"json": {
"promise": [
"Promise",
0,
"__tson",
],
},
"nonce": "__tson",
}
`);

const values = [];
for await (const value of iterator) {
values.push(value);
}

expect(values).toHaveLength(2);

expect(values).toMatchInlineSnapshot(`
[
[
0,
0,
{
"anotherPromise": [
"Promise",
1,
"__tson",
],
},
],
[
1,
0,
42,
],
]
`);
});

test("promise that rejects", async () => {
const tson = createTsonAsync({
nonce: () => "__tson",
types: [tsonPromise],
});

const promise = Promise.reject(new Error("foo"));

const [head, iterator] = tson.serializeAsync(promise);

expect(head).toMatchInlineSnapshot(`
{
"json": [
"Promise",
0,
"__tson",
],
"nonce": "__tson",
}
`);

const values = [];

for await (const value of iterator) {
values.push(value);
}

expect(values).toMatchInlineSnapshot(`
[
[
0,
1,
[TsonPromiseRejectionError: Promise rejected],
],
]
`);
});
4 changes: 2 additions & 2 deletions src/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any, eslint-comments/disable-enable-pair */
import { CircularReferenceError } from "./errors.js";
import { TsonCircularReferenceError } from "./errors.js";
import { GetNonce, getNonce } from "./internals/getNonce.js";
import { mapOrReturn } from "./internals/mapOrReturn.js";
import {
Expand Down Expand Up @@ -85,7 +85,7 @@ export function createTsonSerialize(opts: TsonOptions): TsonSerializeFn {
if (seen.has(value)) {
const cached = cache.get(value);
if (!cached) {
throw new CircularReferenceError(value);
throw new TsonCircularReferenceError(value);
}

return cached;
Expand Down
70 changes: 51 additions & 19 deletions src/serializeAsync.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CircularReferenceError, PromiseRejectionError } from "./errors.js";
import {
TsonCircularReferenceError,
TsonPromiseRejectionError,
} from "./errors.js";
import { getNonce } from "./internals/getNonce.js";
import { mapOrReturn } from "./internals/mapOrReturn.js";
import {
TsonAllTypes,
TsonAsyncIndex,
TsonAsyncOptions,
TsonNonce,
TsonSerialized,
TsonSerializedValue,
TsonTuple,
TsonTypeHandlerKey,
Expand All @@ -14,7 +18,6 @@ import {
} from "./types.js";

type WalkFn = (value: unknown) => unknown;
type WalkerFactory = (nonce: TsonNonce) => WalkFn;

const PROMISE_RESOLVED = 0 as const;
const PROMISE_REJECTED = 1 as const;
Expand All @@ -25,23 +28,27 @@ type TsonAsyncValueTuple = [
unknown,
];

function walkerFactory(opts: TsonAsyncOptions) {
function walkerFactory(nonce: TsonNonce, types: TsonAsyncOptions["types"]) {
// instance variables
let promiseIndex = 0 as TsonAsyncIndex;
const promises = new Map<
TsonAsyncIndex,
[TsonAsyncIndex, Promise<TsonAsyncValueTuple>]
>();
const promises = new Map<TsonAsyncIndex, Promise<TsonAsyncValueTuple>>();
const seen = new WeakSet();
const cache = new WeakMap<object, unknown>();
const nonce: TsonNonce = opts.nonce
? (opts.nonce() as TsonNonce)
: getNonce();

const iterator = {
async *[Symbol.asyncIterator]() {
while (promises.size > 0) {
const tuple = await Promise.race(promises.values());

promises.delete(tuple[0]);
yield walk(tuple) as typeof tuple;
}
},
};
// helper fns
function registerPromise(promise: Promise<unknown>): TsonAsyncIndex {
const index = promiseIndex++ as TsonAsyncIndex;
promises.set(index, [
promises.set(
index,
promise
.then((result) => {
Expand All @@ -50,17 +57,21 @@ function walkerFactory(opts: TsonAsyncOptions) {
})
// ^?
.catch((err) => {
const tuple: TsonAsyncValueTuple = [index, PROMISE_REJECTED, err];
const tuple: TsonAsyncValueTuple = [
index,
PROMISE_REJECTED,
new TsonPromiseRejectionError(err),
];

return tuple;
}),
]);
);

return index;
}

const handlers = (() => {
const types = opts.types.map((handler) => {
const all = types.map((handler) => {
type Serializer = (
value: unknown,
nonce: TsonNonce,
Expand All @@ -79,14 +90,14 @@ function walkerFactory(opts: TsonAsyncOptions) {
$serialize,
};
});
type Handler = (typeof types)[number];
type Handler = (typeof all)[number];

const byPrimitive: Partial<
Record<TsonAllTypes, Extract<Handler, TsonTypeTesterPrimitive>>
> = {};
const nonPrimitive: Extract<Handler, TsonTypeTesterCustom>[] = [];

for (const handler of types) {
for (const handler of all) {
if (handler.primitive) {
if (byPrimitive[handler.primitive]) {
throw new Error(
Expand All @@ -113,7 +124,7 @@ function walkerFactory(opts: TsonAsyncOptions) {
if (seen.has(value)) {
const cached = cache.get(value);
if (!cached) {
throw new CircularReferenceError(value);
throw new TsonCircularReferenceError(value);
}

return cached;
Expand Down Expand Up @@ -147,7 +158,28 @@ function walkerFactory(opts: TsonAsyncOptions) {
return cacheAndReturn(mapOrReturn(value, walk));
};

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

export function createAsyncTsonSerializer(opts: TsonAsyncOptions) {}
type TsonAsyncSerializer = <T>(
value: T,
) => [TsonSerialized<T>, AsyncIterable<TsonAsyncValueTuple>];

export function createAsyncTsonSerializer(
opts: TsonAsyncOptions,
): TsonAsyncSerializer {
return (value) => {
const nonce: TsonNonce = opts.nonce
? (opts.nonce() as TsonNonce)
: getNonce();

Check warning on line 174 in src/serializeAsync.ts

View check run for this annotation

Codecov / codecov/patch

src/serializeAsync.ts#L174

Added line #L174 was not covered by tests
const [walk, iterator] = walkerFactory(nonce, opts.types);

return [
{
json: walk(value),
nonce,
} as TsonSerialized<typeof value>,
iterator,
];
};
}

0 comments on commit 592cc1d

Please sign in to comment.