Skip to content

Commit

Permalink
feat: add postPlugins for encode to handle any values that can not …
Browse files Browse the repository at this point in the history
…be handled natively or by other plugins (#47)
  • Loading branch information
jacob-ebey authored Aug 17, 2024
1 parent e21e228 commit 5fc83c8
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 8 deletions.
41 changes: 35 additions & 6 deletions src/flatten.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function flatten(this: ThisEncode, input: unknown): number | [number] {
}

function stringify(this: ThisEncode, input: unknown, index: number) {
const { deferred, plugins } = this;
const { deferred, plugins, postPlugins } = this;
const str = this.stringified;

const stack: [unknown, number][] = [[input, index]];
Expand All @@ -50,6 +50,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) {
Object.keys(obj)
.map((k) => `"_${flatten.call(this, k)}":${flatten.call(this, obj[k])}`)
.join(",");
let error: Error | null = null;

switch (typeof input) {
case "boolean":
Expand All @@ -62,11 +63,13 @@ function stringify(this: ThisEncode, input: unknown, index: number) {
break;
case "symbol": {
const keyFor = Symbol.keyFor(input);
if (!keyFor)
throw new Error(
if (!keyFor) {
error = new Error(
"Cannot encode symbol unless created with Symbol.for()"
);
str[index] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
} else {
str[index] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
}
break;
}
case "object": {
Expand Down Expand Up @@ -144,7 +147,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) {
} else if (isPlainObject(input)) {
str[index] = `{${partsForObj(input)}}`;
} else {
throw new Error("Cannot encode object with prototype");
error = new Error("Cannot encode object with prototype");
}
}
break;
Expand All @@ -171,10 +174,36 @@ function stringify(this: ThisEncode, input: unknown, index: number) {
}

if (!pluginHandled) {
throw new Error("Cannot encode function or unexpected type");
error = new Error("Cannot encode function or unexpected type");
}
}
}

if (error) {
let pluginHandled = false;

if (postPlugins) {
for (const plugin of postPlugins) {
const pluginResult = plugin(input);
if (Array.isArray(pluginResult)) {
pluginHandled = true;
const [pluginIdentifier, ...rest] = pluginResult;
str[index] = `[${JSON.stringify(pluginIdentifier)}`;
if (rest.length > 0) {
str[index] += `,${rest
.map((v) => flatten.call(this, v))
.join(",")}`;
}
str[index] += "]";
break;
}
}
}

if (!pluginHandled) {
throw error;
}
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/turbo-stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,35 @@ test("should allow plugins to encode and decode functions", async () => {
);
expect(decoded.value).toBeInstanceOf(Function);
expect((decoded.value as typeof input)()).toBe("foo");
await decoded.done;
});

test("should allow postPlugins to handle values that would otherwise throw", async () => {
class Class {}
const input = {
func: () => null,
class: new Class(),
};
const decoded = await decode(
encode(input, {
postPlugins: [
(value) => {
return ["u"];
},
],
}),
{
plugins: [
(type) => {
if (type === "u") {
return { value: undefined };
}
},
],
}
);
expect(decoded.value).toEqual({ func: undefined, class: undefined });
await decoded.done;
});

test("should propagate abort reason to deferred promises for sync resolved promise", async () => {
Expand Down
9 changes: 7 additions & 2 deletions src/turbo-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,21 @@ async function decodeDeferred(

export function encode(
input: unknown,
options?: { plugins?: EncodePlugin[]; signal?: AbortSignal }
options?: {
plugins?: EncodePlugin[];
postPlugins?: EncodePlugin[];
signal?: AbortSignal;
}
) {
const { plugins, signal } = options ?? {};
const { plugins, postPlugins, signal } = options ?? {};

const encoder: ThisEncode = {
deferred: {},
index: 0,
indices: new Map(),
stringified: [],
plugins,
postPlugins,
signal,
};
const textEncoder = new TextEncoder();
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface ThisEncode {
stringified: string[];
deferred: Record<number, Promise<unknown>>;
plugins?: EncodePlugin[];
postPlugins?: EncodePlugin[];
signal?: AbortSignal;
}

Expand Down
Empty file added viewer/scripts/.gitkeep
Empty file.

0 comments on commit 5fc83c8

Please sign in to comment.