@parity/product-sdk-host 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +45 -12
- package/dist/index.js +166 -69
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/accounts.ts +311 -109
- package/src/errors.ts +46 -0
- package/src/index.ts +2 -0
- package/src/truapi.ts +139 -3
package/src/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ export {
|
|
|
70
70
|
HostError,
|
|
71
71
|
HostUnavailableError,
|
|
72
72
|
HostCallFailedError,
|
|
73
|
+
HostResponseDecodeError,
|
|
73
74
|
isHostError,
|
|
74
75
|
formatHostError,
|
|
75
76
|
} from "./errors.js";
|
|
@@ -79,6 +80,7 @@ export type { HostErrorPayload } from "./errors.js";
|
|
|
79
80
|
export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js";
|
|
80
81
|
export type {
|
|
81
82
|
AccountsProvider,
|
|
83
|
+
WithDecodeError,
|
|
82
84
|
DerivationIndex,
|
|
83
85
|
HostAccount,
|
|
84
86
|
ProductAccount,
|
package/src/truapi.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type HostError,
|
|
29
29
|
type HostErrorPayload,
|
|
30
30
|
HostCallFailedError,
|
|
31
|
+
HostResponseDecodeError,
|
|
31
32
|
HostUnavailableError,
|
|
32
33
|
formatHostError,
|
|
33
34
|
} from "./errors.js";
|
|
@@ -37,6 +38,66 @@ import type { HostSubscription, Statement, StatementProof } from "./types.js";
|
|
|
37
38
|
|
|
38
39
|
const log = createLogger("host");
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* `result.match(onOk, onErr)`, but a decode-time rejection is routed through
|
|
43
|
+
* `onDecode` instead of rejecting the returned promise.
|
|
44
|
+
*
|
|
45
|
+
* The truapi client decodes each response inside the value it resolves, and
|
|
46
|
+
* wraps the whole call with `ResultAsync.fromSafePromise`, which installs no
|
|
47
|
+
* rejection handler. So when the host's reply doesn't match the client's codec
|
|
48
|
+
* — a protocol-version skew, or a channel that closed mid-call — the resulting
|
|
49
|
+
* rejection escapes the `Result` channel entirely and `.match` never sees it,
|
|
50
|
+
* surfacing as a raw `RangeError` rather than reaching `onErr`. Catching the
|
|
51
|
+
* `.match` promise re-homes that rejection as a typed
|
|
52
|
+
* {@link HostResponseDecodeError} that names the call. Both
|
|
53
|
+
* {@link unwrapHostResult} and {@link mapHostResult} route through here, so
|
|
54
|
+
* every boundary — throwing and Result-returning — is covered, not just the
|
|
55
|
+
* accounts adapter.
|
|
56
|
+
*/
|
|
57
|
+
async function matchGuarded<T, E, A, B>(
|
|
58
|
+
result: ResultAsync<T, E>,
|
|
59
|
+
label: string,
|
|
60
|
+
onOk: (value: T) => A,
|
|
61
|
+
onErr: (error: E) => B,
|
|
62
|
+
onDecode: (error: HostResponseDecodeError) => B,
|
|
63
|
+
): Promise<A | B> {
|
|
64
|
+
// `.match` rejects for two reasons: the underlying `ResultAsync` rejected (a
|
|
65
|
+
// decode failure — what we want to catch), or `onOk`/`onErr` themselves threw
|
|
66
|
+
// (e.g. `unwrapHostResult`'s err path deliberately throws). Wrap the handler
|
|
67
|
+
// throws in a sentinel so the `catch` can tell them apart and only re-home a
|
|
68
|
+
// genuine underlying rejection; a handler throw is rethrown unchanged.
|
|
69
|
+
try {
|
|
70
|
+
return await result.match(
|
|
71
|
+
(value) => {
|
|
72
|
+
try {
|
|
73
|
+
return onOk(value);
|
|
74
|
+
} catch (thrown) {
|
|
75
|
+
throw new HandlerThrow(thrown);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
(error) => {
|
|
79
|
+
try {
|
|
80
|
+
return onErr(error);
|
|
81
|
+
} catch (thrown) {
|
|
82
|
+
throw new HandlerThrow(thrown);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (cause instanceof HandlerThrow) throw cause.thrown;
|
|
88
|
+
return onDecode(
|
|
89
|
+
cause instanceof HostResponseDecodeError
|
|
90
|
+
? cause
|
|
91
|
+
: new HostResponseDecodeError(label, cause),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Marks a throw that came from a caller's `onOk`/`onErr`, not the underlying `ResultAsync`. */
|
|
97
|
+
class HandlerThrow {
|
|
98
|
+
constructor(readonly thrown: unknown) {}
|
|
99
|
+
}
|
|
100
|
+
|
|
40
101
|
/**
|
|
41
102
|
* Await a host `ResultAsync`, returning its Ok value or throwing a diagnostic
|
|
42
103
|
* `Error` built from the host's error payload (preserved as `cause`).
|
|
@@ -49,11 +110,18 @@ const log = createLogger("host");
|
|
|
49
110
|
* throw convention. The flat public operations use {@link mapHostResult} instead.
|
|
50
111
|
*/
|
|
51
112
|
export function unwrapHostResult<T, E>(result: ResultAsync<T, E>, label: string): Promise<T> {
|
|
52
|
-
return
|
|
113
|
+
return matchGuarded(
|
|
114
|
+
result,
|
|
115
|
+
label,
|
|
53
116
|
(value) => value,
|
|
54
117
|
(error: E) => {
|
|
55
118
|
throw new Error(`${label}: ${formatHostError(error)}`, { cause: error });
|
|
56
119
|
},
|
|
120
|
+
// A response the client can't decode would otherwise reject with a raw
|
|
121
|
+
// `RangeError`; throw it as a typed, named error instead.
|
|
122
|
+
(decodeError) => {
|
|
123
|
+
throw decodeError;
|
|
124
|
+
},
|
|
57
125
|
);
|
|
58
126
|
}
|
|
59
127
|
|
|
@@ -69,9 +137,15 @@ export function mapHostResult<T, U>(
|
|
|
69
137
|
map: (value: T) => U,
|
|
70
138
|
label: string,
|
|
71
139
|
): Promise<Result<U, HostError>> {
|
|
72
|
-
|
|
140
|
+
// A response the client can't decode would otherwise reject this promise
|
|
141
|
+
// with a raw `RangeError`; return it as a typed err instead, matching the
|
|
142
|
+
// `Result` contract these flat public operations advertise.
|
|
143
|
+
return matchGuarded<T, HostErrorPayload, Result<U, HostError>, Result<U, HostError>>(
|
|
144
|
+
result,
|
|
145
|
+
label,
|
|
73
146
|
(value) => ok(map(value)),
|
|
74
147
|
(error) => err(new HostCallFailedError(label, error)),
|
|
148
|
+
(decodeError) => err(decodeError),
|
|
75
149
|
);
|
|
76
150
|
}
|
|
77
151
|
|
|
@@ -271,7 +345,7 @@ export interface ResultAsync<T, E> {
|
|
|
271
345
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
272
346
|
|
|
273
347
|
if (import.meta.vitest) {
|
|
274
|
-
const { test, expect } = import.meta.vitest;
|
|
348
|
+
const { test, expect, describe } = import.meta.vitest;
|
|
275
349
|
|
|
276
350
|
test("getTruApi returns null outside a container", async () => {
|
|
277
351
|
const api = await getTruApi();
|
|
@@ -310,4 +384,66 @@ if (import.meta.vitest) {
|
|
|
310
384
|
test("createProofAuthorized is callable", () => {
|
|
311
385
|
expect(typeof createProofAuthorized).toBe("function");
|
|
312
386
|
});
|
|
387
|
+
|
|
388
|
+
// The decode boundary these two helpers share. The truapi client's real
|
|
389
|
+
// `ResultAsync` *rejects* its underlying promise on a decode failure, which
|
|
390
|
+
// `.match` surfaces as a rejected promise — modelled here by a fake whose
|
|
391
|
+
// `.match` rejects. Ok and typed-err doubles mirror neverthrow's `.match`.
|
|
392
|
+
const okLike = <T>(value: T): ResultAsync<T, never> => ({
|
|
393
|
+
match: async (onOk) => onOk(value),
|
|
394
|
+
});
|
|
395
|
+
const errLike = <E>(error: E): ResultAsync<never, E> => ({
|
|
396
|
+
match: async (_onOk, onErr) => onErr(error),
|
|
397
|
+
});
|
|
398
|
+
const rejectLike = (cause: unknown): ResultAsync<never, never> => ({
|
|
399
|
+
match: () => Promise.reject(cause),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("mapHostResult decode boundary", () => {
|
|
403
|
+
test("a decode rejection becomes err(HostResponseDecodeError) naming the call", async () => {
|
|
404
|
+
const cause = new RangeError("Offset is outside the bounds of the DataView");
|
|
405
|
+
const result = await mapHostResult(rejectLike(cause), (v) => v, "createRingVRFProof");
|
|
406
|
+
expect(result.ok).toBe(false);
|
|
407
|
+
if (!result.ok) {
|
|
408
|
+
expect(result.error).toBeInstanceOf(HostResponseDecodeError);
|
|
409
|
+
expect((result.error as HostResponseDecodeError).call).toBe("createRingVRFProof");
|
|
410
|
+
expect((result.error as HostResponseDecodeError).cause).toBe(cause);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("an ok value maps through", async () => {
|
|
415
|
+
const result = await mapHostResult(okLike(41), (v: number) => v + 1, "getUserId");
|
|
416
|
+
expect(result.ok && result.value).toBe(42);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("a typed host err becomes HostCallFailedError, not a decode error", async () => {
|
|
420
|
+
const result = await mapHostResult(errLike({ tag: "Denied" }), (v) => v, "getUserId");
|
|
421
|
+
expect(result.ok).toBe(false);
|
|
422
|
+
if (!result.ok) {
|
|
423
|
+
expect(result.error).toBeInstanceOf(HostCallFailedError);
|
|
424
|
+
expect(result.error).not.toBeInstanceOf(HostResponseDecodeError);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe("unwrapHostResult decode boundary", () => {
|
|
430
|
+
test("a decode rejection throws HostResponseDecodeError naming the call", async () => {
|
|
431
|
+
const cause = new RangeError("Offset is outside the bounds of the DataView");
|
|
432
|
+
await expect(unwrapHostResult(rejectLike(cause), "signVrf")).rejects.toMatchObject({
|
|
433
|
+
name: "HostResponseDecodeError",
|
|
434
|
+
call: "signVrf",
|
|
435
|
+
cause,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("an ok value passes through", async () => {
|
|
440
|
+
expect(await unwrapHostResult(okLike("hi"), "getUserId")).toBe("hi");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("a typed host err still throws a diagnostic Error, not a decode error", async () => {
|
|
444
|
+
await expect(
|
|
445
|
+
unwrapHostResult(errLike({ tag: "Denied" }), "getUserId"),
|
|
446
|
+
).rejects.not.toBeInstanceOf(HostResponseDecodeError);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
313
449
|
}
|