Skip to content

Commit 1623e18

Browse files
committed
Greatly expand the serializable types using superjson
1 parent a8ec500 commit 1623e18

7 files changed

Lines changed: 161 additions & 44 deletions

File tree

.changeset/wet-jobs-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/twister": minor
3+
---
4+
5+
Added: Support for more serializable types, especially Date

twister/src/common/serializable.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Types supported by SuperJSON serialization.
3+
*
4+
* SuperJSON extends standard JSON serialization to support additional JavaScript types
5+
* while maintaining type safety and preventing common serialization errors.
6+
*
7+
* Supported types:
8+
* - Primitives: string, number, boolean, null, undefined
9+
* - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt
10+
* - Collections: Arrays and objects (recursively)
11+
*
12+
* NOT supported (will throw validation errors):
13+
* - Functions
14+
* - Symbols
15+
* - Circular references
16+
* - Custom class instances (unless explicitly registered)
17+
*/
18+
export type Serializable =
19+
| string
20+
| number
21+
| boolean
22+
| null
23+
| undefined
24+
| Date
25+
| RegExp
26+
| Error
27+
| URL
28+
| bigint
29+
| SerializableArray
30+
| SerializableObject
31+
| SerializableMap
32+
| SerializableSet;
33+
34+
/**
35+
* Array of serializable values
36+
*/
37+
export interface SerializableArray extends Array<Serializable> {}
38+
39+
/**
40+
* Object with string keys and serializable values
41+
*/
42+
export interface SerializableObject {
43+
[key: string]: Serializable;
44+
}
45+
46+
/**
47+
* Map with serializable keys and values
48+
*/
49+
export interface SerializableMap extends Map<Serializable, Serializable> {}
50+
51+
/**
52+
* Set with serializable values
53+
*/
54+
export interface SerializableSet extends Set<Serializable> {}

twister/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from "./plot";
33
export * from "./tag";
44
export * from "./tool";
55
export * from "./tools";
6+
export * from "./common/serializable";
67
export { getBuilderDocumentation } from "./creator-docs";

twister/src/plot.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export type Priority = {
9191
title: string;
9292
/** Whether this priority has been archived */
9393
archived: boolean;
94+
/**
95+
* Optional key for referencing this priority.
96+
* Keys are unique per priority tree (a user's personal priorities or the root of a shared priority).
97+
*/
98+
key: string | null;
9499
};
95100

96101
/**

twister/src/tool.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -127,44 +127,52 @@ export abstract class Tool<TSelf> implements ITool {
127127
* @param args - Optional arguments to pass to the callback
128128
* @returns Promise resolving to the callback result
129129
*/
130-
protected async run(token: Callback, args?: any): Promise<any> {
131-
return this.tools.callbacks.run(token, args);
130+
protected async run(token: Callback, ...args: any[]): Promise<any> {
131+
return this.tools.callbacks.run(token, ...args);
132132
}
133133

134134
/**
135135
* Retrieves a value from persistent storage by key.
136136
*
137-
* @template T - The expected type of the stored value
137+
* Values are automatically deserialized using SuperJSON, which
138+
* properly restores Date objects, Maps, Sets, and other complex types.
139+
*
140+
* @template T - The expected type of the stored value (must be Serializable)
138141
* @param key - The storage key to retrieve
139142
* @returns Promise resolving to the stored value or null
140143
*/
141-
protected async get<T>(key: string): Promise<T | null> {
144+
protected async get<T extends import("./index").Serializable>(key: string): Promise<T | null> {
142145
return this.tools.store.get(key);
143146
}
144147

145148
/**
146149
* Stores a value in persistent storage.
147150
*
148-
* **Important**: Values must be JSON-serializable. Functions and Symbols cannot be stored.
149-
*
150-
* **Handling undefined values:**
151-
* - Object keys with undefined values are automatically removed
152-
* - Arrays with undefined elements will throw a validation error
153-
* - Use null instead of undefined for array elements
151+
* The value will be serialized using SuperJSON and stored persistently.
152+
* SuperJSON automatically handles Date objects, Maps, Sets, undefined values,
153+
* and other complex types that standard JSON doesn't support.
154154
*
155+
* **Important**: Functions and Symbols cannot be stored.
155156
* **For function references**: Use callbacks instead of storing functions directly.
156157
*
157158
* @example
158159
* ```typescript
159-
* // ✅ Object keys with undefined are automatically removed
160+
* // ✅ Date objects are preserved
161+
* await this.set("sync_state", {
162+
* lastSync: new Date(),
163+
* minDate: new Date(2024, 0, 1)
164+
* });
165+
*
166+
* // ✅ undefined is now supported
160167
* await this.set("data", { name: "test", optional: undefined });
161-
* // Stores: { name: "test" }
162168
*
163-
* // ✅ Arrays: use null for optional values
164-
* await this.set("items", [1, null, 3]);
169+
* // ✅ Arrays with undefined are supported
170+
* await this.set("items", [1, undefined, 3]);
171+
* await this.set("items", [1, null, 3]); // Also works
165172
*
166-
* // ❌ Arrays with undefined throw errors
167-
* await this.set("items", [1, undefined, 3]); // Error!
173+
* // ✅ Maps and Sets are supported
174+
* await this.set("mapping", new Map([["key", "value"]]));
175+
* await this.set("tags", new Set(["tag1", "tag2"]));
168176
*
169177
* // ❌ WRONG: Cannot store functions directly
170178
* await this.set("handler", this.myHandler);
@@ -178,12 +186,12 @@ export abstract class Tool<TSelf> implements ITool {
178186
* await this.run(token, args);
179187
* ```
180188
*
181-
* @template T - The type of value being stored
189+
* @template T - The type of value being stored (must be Serializable)
182190
* @param key - The storage key to use
183-
* @param value - The value to store (must be JSON-serializable)
191+
* @param value - The value to store (must be SuperJSON-serializable)
184192
* @returns Promise that resolves when the value is stored
185193
*/
186-
protected async set<T>(key: string, value: T): Promise<void> {
194+
protected async set<T extends import("./index").Serializable>(key: string, value: T): Promise<void> {
187195
return this.tools.store.set(key, value);
188196
}
189197

twister/src/tools/store.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ITool } from "..";
1+
import { ITool, type Serializable } from "..";
22

33
/**
44
* Built-in tool for persistent key-value storage.
@@ -14,9 +14,20 @@ import { ITool } from "..";
1414
* **Storage Characteristics:**
1515
* - Persistent across worker restarts
1616
* - Isolated per twist/tool instance
17-
* - Supports any JSON-serializable data
17+
* - Supports SuperJSON-serializable data (see below)
1818
* - Async operations for scalability
1919
*
20+
* **Supported Data Types (via SuperJSON):**
21+
* - Primitives: string, number, boolean, null, undefined
22+
* - Complex types: Date, RegExp, Map, Set, Error, URL, BigInt
23+
* - Collections: Arrays and objects (recursively)
24+
*
25+
* **NOT Supported (will throw validation errors):**
26+
* - Functions (use callback tokens instead - see Callbacks tool)
27+
* - Symbols
28+
* - Circular references
29+
* - Custom class instances
30+
*
2031
* **Use Cases:**
2132
* - Storing authentication tokens
2233
* - Caching configuration data
@@ -51,42 +62,56 @@ export abstract class Store extends ITool {
5162
* Returns the stored value deserialized to the specified type,
5263
* or null if the key doesn't exist or the value is null.
5364
*
54-
* @template T - The expected type of the stored value
65+
* Values are automatically deserialized using SuperJSON, which
66+
* properly restores Date objects, Maps, Sets, and other complex types.
67+
*
68+
* @template T - The expected type of the stored value (must be Serializable)
5569
* @param key - The storage key to retrieve
5670
* @returns Promise resolving to the stored value or null
5771
*/
5872
// eslint-disable-next-line @typescript-eslint/no-unused-vars
59-
abstract get<T>(key: string): Promise<T | null>;
73+
abstract get<T extends Serializable>(key: string): Promise<T | null>;
6074

6175
/**
6276
* Stores a value in persistent storage.
6377
*
64-
* The value will be JSON-serialized and stored persistently.
78+
* The value will be serialized using SuperJSON and stored persistently.
6579
* Any existing value at the same key will be overwritten.
6680
*
67-
* **Handling undefined values:**
68-
* - Object keys with undefined values are automatically removed
69-
* - Arrays with undefined elements will throw a validation error
70-
* - Use null instead of undefined for array elements
81+
* SuperJSON automatically handles Date objects, Maps, Sets, undefined values,
82+
* and other complex types that standard JSON doesn't support.
7183
*
72-
* @template T - The type of value being stored
84+
* @template T - The type of value being stored (must be Serializable)
7385
* @param key - The storage key to use
74-
* @param value - The value to store (must be JSON-serializable)
86+
* @param value - The value to store (must be SuperJSON-serializable)
7587
* @returns Promise that resolves when the value is stored
7688
*
7789
* @example
7890
* ```typescript
79-
* // Object keys with undefined are removed
80-
* await this.set('data', { name: 'test', optional: undefined });
81-
* // Stores: { name: 'test' }
91+
* // Date objects are preserved
92+
* await this.set('sync_state', {
93+
* lastSync: new Date(),
94+
* minDate: new Date(2024, 0, 1)
95+
* });
96+
*
97+
* // undefined is now supported
98+
* await this.set('data', { name: 'test', optional: undefined }); // ✅ Works
99+
*
100+
* // Arrays with undefined are supported
101+
* await this.set('items', [1, undefined, 3]); // ✅ Works
102+
* await this.set('items', [1, null, 3]); // ✅ Also works
103+
*
104+
* // Maps and Sets are supported
105+
* await this.set('mapping', new Map([['key', 'value']])); // ✅ Works
106+
* await this.set('tags', new Set(['tag1', 'tag2'])); // ✅ Works
82107
*
83-
* // Arrays with undefined throw errors - use null instead
84-
* await this.set('items', [1, null, 3]); // ✅ Works
85-
* await this.set('items', [1, undefined, 3]); // ❌ Throws error
108+
* // Functions are NOT supported - use callback tokens instead
109+
* const token = await this.callback(this.myFunction);
110+
* await this.set('callback_ref', token); // ✅ Use callback token
86111
* ```
87112
*/
88113
// eslint-disable-next-line @typescript-eslint/no-unused-vars
89-
abstract set<T>(key: string, value: T): Promise<void>;
114+
abstract set<T extends Serializable>(key: string, value: T): Promise<void>;
90115

91116
/**
92117
* Removes a specific key from storage.

twister/src/twist.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,40 @@ export abstract class Twist<TSelf> {
114114
/**
115115
* Retrieves a value from persistent storage by key.
116116
*
117-
* @template T - The expected type of the stored value
117+
* Values are automatically deserialized using SuperJSON, which
118+
* properly restores Date objects, Maps, Sets, and other complex types.
119+
*
120+
* @template T - The expected type of the stored value (must be Serializable)
118121
* @param key - The storage key to retrieve
119122
* @returns Promise resolving to the stored value or null
120123
*/
121-
protected async get<T>(key: string): Promise<T | null> {
124+
protected async get<T extends import("./index").Serializable>(
125+
key: string
126+
): Promise<T | null> {
122127
return this.tools.store.get(key);
123128
}
124129

125130
/**
126131
* Stores a value in persistent storage.
127132
*
128-
* **Important**: Values must be JSON-serializable. Functions, Symbols, and undefined values
129-
* cannot be stored directly.
133+
* The value will be serialized using SuperJSON and stored persistently.
134+
* SuperJSON automatically handles Date objects, Maps, Sets, undefined values,
135+
* and other complex types that standard JSON doesn't support.
130136
*
137+
* **Important**: Functions and Symbols cannot be stored.
131138
* **For function references**: Use callbacks instead of storing functions directly.
132139
*
133140
* @example
134141
* ```typescript
142+
* // ✅ Date objects are preserved
143+
* await this.set("sync_state", {
144+
* lastSync: new Date(),
145+
* minDate: new Date(2024, 0, 1)
146+
* });
147+
*
148+
* // ✅ undefined is now supported
149+
* await this.set("data", { name: "test", optional: undefined });
150+
*
135151
* // ❌ WRONG: Cannot store functions directly
136152
* await this.set("handler", this.myHandler);
137153
*
@@ -144,12 +160,15 @@ export abstract class Twist<TSelf> {
144160
* await this.run(token, args);
145161
* ```
146162
*
147-
* @template T - The type of value being stored
163+
* @template T - The type of value being stored (must be Serializable)
148164
* @param key - The storage key to use
149-
* @param value - The value to store (must be JSON-serializable)
165+
* @param value - The value to store (must be SuperJSON-serializable)
150166
* @returns Promise that resolves when the value is stored
151167
*/
152-
protected async set<T>(key: string, value: T): Promise<void> {
168+
protected async set<T extends import("./index").Serializable>(
169+
key: string,
170+
value: T
171+
): Promise<void> {
153172
return this.tools.store.set(key, value);
154173
}
155174

0 commit comments

Comments
 (0)