Skip to content

Commit 00560f0

Browse files
authored
[TS] Throw error objects from syscalls (#4260)
# Description of Changes This lets us avoid the try/catch around every syscall in javascript. # API and ABI breaking changes <!-- If this is an API or ABI breaking change, please apply the corresponding GitHub label. --> # Expected complexity level and risk <!-- How complicated do you think these changes are? Grade on a scale from 1 to 5, where 1 is a trivial change, and 5 is a deep-reaching and complex change. This complexity rating applies not only to the complexity apparent in the diff, but also to its interactions with existing and future code. If you answered more than a 2, explain what is complex about the PR, and what other components it interacts with in potentially concerning ways. --> # Testing <!-- Describe any testing you've done, and any testing you'd like your reviewers to do, so that you're confident that all the changes work as expected! --> - [ ] <!-- maybe a test you want to do --> - [ ] <!-- maybe a test you want a reviewer to do, so they can check it off when they're satisfied. -->
1 parent eb2f7a3 commit 00560f0

13 files changed

Lines changed: 340 additions & 300 deletions

File tree

Lines changed: 40 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,13 @@
11
/**
22
* Base class for all Spacetime host errors (i.e. errors that may be thrown
33
* by database functions).
4-
*
5-
* Instances of SpacetimeError can be created with just an error code,
6-
* which will return the appropriate subclass instance.
74
*/
85
export class SpacetimeHostError extends Error {
9-
public readonly code: number;
10-
public readonly message: string;
11-
constructor(code: number, message?: string) {
12-
super();
13-
const proto = Object.getPrototypeOf(this);
14-
let cls;
15-
if (errorProtoypes.has(proto)) {
16-
cls = proto.constructor;
17-
if (code !== cls.CODE)
18-
throw new TypeError(`invalid error code for ${cls.name}`);
19-
} else if (proto === SpacetimeHostError.prototype) {
20-
cls = errnoToClass.get(code);
21-
if (!cls) throw new RangeError(`unknown error code ${code}`);
22-
} else {
23-
throw new TypeError('cannot subclass SpacetimeError');
24-
}
25-
Object.setPrototypeOf(this, cls.prototype);
26-
this.code = cls.CODE;
27-
this.message = message ?? cls.MESSAGE;
6+
constructor(message: string) {
7+
super(message);
288
}
299
get name(): string {
30-
return errnoToClass.get(this.code)?.name ?? 'SpacetimeHostError';
10+
return 'SpacetimeHostError';
3111
}
3212
}
3313

@@ -50,110 +30,89 @@ const errorData = {
5030
/**
5131
* A generic error class for unknown error codes.
5232
*/
53-
HostCallFailure: [1, 'ABI called by host returned an error'],
33+
HostCallFailure: 1,
5434

5535
/**
5636
* Error indicating that an ABI call was made outside of a transaction.
5737
*/
58-
NotInTransaction: [2, 'ABI call can only be made while in a transaction'],
38+
NotInTransaction: 2,
5939

6040
/**
6141
* Error indicating that BSATN decoding failed.
6242
* This typically means that the data could not be decoded to the expected type.
6343
*/
64-
BsatnDecodeError: [3, "Couldn't decode the BSATN to the expected type"],
44+
BsatnDecodeError: 3,
6545

6646
/**
6747
* Error indicating that a specified table does not exist.
6848
*/
69-
NoSuchTable: [4, 'No such table'],
49+
NoSuchTable: 4,
7050

7151
/**
7252
* Error indicating that a specified index does not exist.
7353
*/
74-
NoSuchIndex: [5, 'No such index'],
54+
NoSuchIndex: 5,
7555

7656
/**
7757
* Error indicating that a specified row iterator is not valid.
7858
*/
79-
NoSuchIter: [6, 'The provided row iterator is not valid'],
59+
NoSuchIter: 6,
8060

8161
/**
8262
* Error indicating that a specified console timer does not exist.
8363
*/
84-
NoSuchConsoleTimer: [7, 'The provided console timer does not exist'],
64+
NoSuchConsoleTimer: 7,
8565

8666
/**
8767
* Error indicating that a specified bytes source or sink is not valid.
8868
*/
89-
NoSuchBytes: [8, 'The provided bytes source or sink is not valid'],
69+
NoSuchBytes: 8,
9070

9171
/**
9272
* Error indicating that a provided sink has no more space left.
9373
*/
94-
NoSpace: [9, 'The provided sink has no more space left'],
74+
NoSpace: 9,
9575

9676
/**
9777
* Error indicating that there is no more space in the database.
9878
*/
99-
BufferTooSmall: [
100-
11,
101-
'The provided buffer is not large enough to store the data',
102-
],
79+
BufferTooSmall: 11,
10380

10481
/**
10582
* Error indicating that a value with a given unique identifier already exists.
10683
*/
107-
UniqueAlreadyExists: [
108-
12,
109-
'Value with given unique identifier already exists',
110-
],
84+
UniqueAlreadyExists: 12,
11185

11286
/**
11387
* Error indicating that the specified delay in scheduling a row was too long.
11488
*/
115-
ScheduleAtDelayTooLong: [
116-
13,
117-
'Specified delay in scheduling row was too long',
118-
],
89+
ScheduleAtDelayTooLong: 13,
11990

12091
/**
12192
* Error indicating that an index was not unique when it was expected to be.
12293
*/
123-
IndexNotUnique: [14, 'The index was not unique'],
94+
IndexNotUnique: 14,
12495

12596
/**
12697
* Error indicating that an index was not unique when it was expected to be.
12798
*/
128-
NoSuchRow: [15, 'The row was not found, e.g., in an update call'],
99+
NoSuchRow: 15,
129100

130101
/**
131102
* Error indicating that an auto-increment sequence has overflowed.
132103
*/
133-
AutoIncOverflow: [16, 'The auto-increment sequence overflowed'],
104+
AutoIncOverflow: 16,
134105

135-
WouldBlockTransaction: [
136-
17,
137-
'Attempted async or blocking op while holding open a transaction',
138-
],
106+
WouldBlockTransaction: 17,
139107

140-
TransactionNotAnonymous: [
141-
18,
142-
'Not in an anonymous transaction. Called by a reducer?',
143-
],
108+
TransactionNotAnonymous: 18,
144109

145-
TransactionIsReadOnly: [
146-
19,
147-
'ABI call can only be made while within a mutable transaction',
148-
],
110+
TransactionIsReadOnly: 19,
149111

150-
TransactionIsMut: [
151-
20,
152-
'ABI call can only be made while within a read-only transaction',
153-
],
112+
TransactionIsMut: 20,
154113

155-
HttpError: [21, 'The HTTP request failed'],
156-
} as const;
114+
HttpError: 21,
115+
};
157116

158117
function mapEntries<const T extends Record<string, any>, U>(
159118
x: T,
@@ -164,30 +123,27 @@ function mapEntries<const T extends Record<string, any>, U>(
164123
) as any;
165124
}
166125

126+
/**
127+
* Map from error codes to their corresponding SpacetimeError subclass.
128+
*/
129+
const errnoToClass = new Map<number, new (msg: string) => Error>();
130+
167131
export const errors = Object.freeze(
168-
mapEntries(errorData, (name, [code, message]) =>
169-
Object.defineProperty(
132+
mapEntries(errorData, (name, code) => {
133+
const cls = Object.defineProperty(
170134
class extends SpacetimeHostError {
171-
static CODE = code;
172-
static MESSAGE = message;
173-
constructor() {
174-
super(code);
135+
get name() {
136+
return name;
175137
}
176138
},
177139
'name',
178140
{ value: name, writable: false }
179-
)
180-
)
141+
);
142+
errnoToClass.set(code, cls);
143+
return cls;
144+
})
181145
);
182146

183-
/**
184-
* Set of prototypes of all SpacetimeError subclasses for quick lookup.
185-
*/
186-
const errorProtoypes = new Set(Object.values(errors).map(cls => cls.prototype));
187-
188-
/**
189-
* Map from error codes to their corresponding SpacetimeError subclass.
190-
*/
191-
const errnoToClass = new Map(
192-
Object.values(errors).map(cls => [cls.CODE as number, cls])
193-
);
147+
export function getErrorConstructor(code: number): new (msg: string) => Error {
148+
return errnoToClass.get(code) ?? SpacetimeHostError;
149+
}

crates/bindings-typescript/src/server/runtime.ts

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ import { hasOwn, toCamelCase } from '../lib/util';
3636
import { type AnonymousViewCtx, type ViewCtx } from './views';
3737
import { isRowTypedQuery, makeQueryBuilder, toSql } from './query';
3838
import type { DbView } from './db_view';
39-
import { SenderError, SpacetimeHostError } from './errors';
39+
import { getErrorConstructor, SenderError } from './errors';
4040
import { Range, type Bound } from './range';
4141
import ViewResultHeader from '../lib/autogen/view_result_header_type';
4242
import { makeRandom, type Random } from './rng';
4343
import type { SchemaInner } from './schema';
4444

4545
const { freeze } = Object;
4646

47-
export const sys = freeze(wrapSyscalls(_syscalls2_0));
47+
export const sys = _syscalls2_0;
4848

4949
export function parseJsonObject(json: string): JsonObject {
5050
let value: unknown;
@@ -312,13 +312,21 @@ class ModuleHooksImpl implements ModuleHooks {
312312
return writer.getBuffer();
313313
}
314314

315+
__get_error_constructor__(code: number): new (msg: string) => Error {
316+
return getErrorConstructor(code);
317+
}
318+
319+
get __sender_error_class__() {
320+
return SenderError;
321+
}
322+
315323
__call_reducer__(
316324
reducerId: u32,
317325
sender: u256,
318326
connId: u128,
319327
timestamp: bigint,
320328
argsBuf: DataView
321-
): undefined | { tag: 'err'; value: string } {
329+
): void {
322330
const moduleCtx = this.#schema;
323331
const deserializeArgs = this.#reducerArgsDeserializers[reducerId];
324332
BINARY_READER.reset(argsBuf);
@@ -331,14 +339,7 @@ class ModuleHooksImpl implements ModuleHooks {
331339
new Timestamp(timestamp),
332340
ConnectionId.nullIfZero(new ConnectionId(connId))
333341
);
334-
try {
335-
callUserFunction(moduleCtx.reducers[reducerId], ctx, args);
336-
} catch (e) {
337-
if (e instanceof SenderError) {
338-
return { tag: 'err', value: e.message };
339-
}
340-
throw e;
341-
}
342+
callUserFunction(moduleCtx.reducers[reducerId], ctx, args);
342343
}
343344

344345
__call_view__(
@@ -906,51 +907,6 @@ class IteratorHandle implements Disposable {
906907
}
907908
}
908909

909-
type Intersections<Ts extends readonly any[]> = Ts extends [
910-
infer T,
911-
...infer Rest,
912-
]
913-
? T & Intersections<Rest>
914-
: unknown;
915-
916-
function wrapSyscalls<
917-
Modules extends Record<string, symbol | ((...args: any[]) => any)>[],
918-
>(...modules: Modules): Intersections<Modules> {
919-
return Object.fromEntries(
920-
modules
921-
.flatMap(Object.entries)
922-
.map(([k, v]) => [k, typeof v === 'function' ? wrapSyscall(v) : v])
923-
) as Intersections<Modules>;
924-
}
925-
926-
function wrapSyscall<F extends (...args: any[]) => any>(
927-
func: F
928-
): (...args: Parameters<F>) => ReturnType<F> {
929-
const name = func.name;
930-
return {
931-
[name](...args: Parameters<F>) {
932-
try {
933-
return func(...args);
934-
} catch (e) {
935-
if (
936-
e !== null &&
937-
typeof e === 'object' &&
938-
hasOwn(e, '__code_error__') &&
939-
typeof e.__code_error__ == 'number'
940-
) {
941-
const message =
942-
hasOwn(e, '__error_message__') &&
943-
typeof e.__error_message__ === 'string'
944-
? e.__error_message__
945-
: undefined;
946-
throw new SpacetimeHostError(e.__code_error__, message);
947-
}
948-
throw e;
949-
}
950-
},
951-
}[name];
952-
}
953-
954910
function fmtLog(...data: any[]) {
955911
return data.join(' ');
956912
}

crates/bindings-typescript/src/server/sys.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ declare module 'spacetime:sys@2.0' {
1515
export interface ModuleHooks {
1616
__describe_module__(): Uint8Array;
1717

18+
__get_error_constructor__(code: number): new (msg: string) => Error;
19+
__sender_error_class__: new (msg: string) => Error;
20+
1821
__call_reducer__(
1922
reducerId: u32,
2023
sender: u256,
2124
connId: u128,
2225
timestamp: bigint,
2326
argsBuf: DataView
24-
): undefined | { tag: 'ok' } | { tag: 'err'; value: string };
27+
): void;
2528

2629
__call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array | object;
2730

crates/core/src/host/v8/builtins/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use super::error::{exception_already_thrown, ExcResult, StringTooLongError, Thro
55
use super::string::{str_from_ident, StringConst};
66
use super::{FnRet, IntoJsString};
77

8-
pub(super) fn evalute_builtins(scope: &mut PinScope<'_, '_>) -> ExcResult<()> {
8+
pub(super) fn evaluate_builtins(scope: &mut PinScope<'_, '_>) -> ExcResult<()> {
99
macro_rules! eval_builtin {
1010
($file:literal) => {
1111
eval_builtin(

0 commit comments

Comments
 (0)