bunite-core 0.9.0 → 0.10.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/package.json +1 -1
- package/src/host/core/App.ts +1 -1
- package/src/host/core/BrowserView.ts +18 -12
- package/src/host/core/BrowserWindow.ts +12 -11
- package/src/host/serveWeb.ts +33 -9
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +8 -10
- package/src/rpc/error.ts +25 -15
- package/src/rpc/framework.ts +12 -12
- package/src/rpc/index.ts +13 -4
- package/src/rpc/peer.ts +542 -159
- package/src/rpc/renderer.ts +16 -18
- package/src/rpc/schema.ts +30 -47
- package/src/rpc/wire.ts +18 -4
- package/src/rpc/hash.ts +0 -142
package/src/rpc/renderer.ts
CHANGED
|
@@ -3,23 +3,22 @@ import {
|
|
|
3
3
|
createFrameTransport,
|
|
4
4
|
createWebSocketPipe,
|
|
5
5
|
type Connection,
|
|
6
|
+
type CapDef,
|
|
6
7
|
type Schema,
|
|
7
|
-
type
|
|
8
|
+
type SchemaRoots,
|
|
8
9
|
type ClientOf,
|
|
9
|
-
type ServerDescriptor,
|
|
10
10
|
type WebSocketLike,
|
|
11
11
|
} from "./index";
|
|
12
12
|
|
|
13
13
|
declare global {
|
|
14
14
|
interface Window {
|
|
15
15
|
host?: {
|
|
16
|
-
bootstrap<
|
|
17
|
-
|
|
18
|
-
name: K
|
|
19
|
-
): Promise<ClientOf<S["roots"][K]>>;
|
|
20
|
-
serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): Promise<void>;
|
|
16
|
+
bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
|
|
17
|
+
bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
21
18
|
runtime(): Promise<ClientOf<typeof import("./framework").RuntimeCap>>;
|
|
22
19
|
releaseRef(proxy: unknown): Promise<void>;
|
|
20
|
+
/** Full Connection for renderer-as-server (serve / serveAll / unserve / replace / on). */
|
|
21
|
+
getConnection(): Promise<Connection>;
|
|
23
22
|
};
|
|
24
23
|
}
|
|
25
24
|
}
|
|
@@ -60,23 +59,22 @@ function ensureWebConnection(path = "/rpc"): Promise<Connection> {
|
|
|
60
59
|
return attempt;
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
): Promise<ClientOf<S["roots"][K]>> {
|
|
62
|
+
export function bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
|
|
63
|
+
export function bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
64
|
+
export async function bootstrap(target: CapDef<any, any> | Schema<any>): Promise<unknown> {
|
|
67
65
|
if (isNative()) {
|
|
68
66
|
if (!window.host?.bootstrap) throw new Error("host preload not ready");
|
|
69
|
-
return window.host.bootstrap(
|
|
67
|
+
return (window.host.bootstrap as (t: unknown) => Promise<unknown>)(target);
|
|
70
68
|
}
|
|
71
69
|
const conn = await ensureWebConnection();
|
|
72
|
-
return conn.bootstrap(
|
|
70
|
+
return (conn.bootstrap as (t: unknown) => Promise<unknown>)(target);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
/** Returns the underlying Connection — for renderer-as-server (`conn.serve(cap, impl)`), observability hooks (`conn.on(...)`), etc. */
|
|
74
|
+
export async function getConnection(): Promise<Connection> {
|
|
76
75
|
if (isNative()) {
|
|
77
|
-
if (!window.host?.
|
|
78
|
-
return window.host.
|
|
76
|
+
if (!window.host?.getConnection) throw new Error("host preload not ready");
|
|
77
|
+
return window.host.getConnection();
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
conn.serve(descriptor);
|
|
79
|
+
return ensureWebConnection();
|
|
82
80
|
}
|
package/src/rpc/schema.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
1
|
+
// Symbol.for() — shared identity across separately-built bundles (preload vs renderer).
|
|
2
|
+
const CALL_TAG = Symbol.for("bunite.rpc.CallDef");
|
|
3
|
+
const STREAM_TAG = Symbol.for("bunite.rpc.StreamDef");
|
|
4
|
+
const CAP_TAG = Symbol.for("bunite.rpc.CapDef");
|
|
5
|
+
const CAP_REF_TAG = Symbol.for("bunite.rpc.CapRefToken");
|
|
6
|
+
const CAP_ARRAY_TAG = Symbol.for("bunite.rpc.CapArrayToken");
|
|
7
|
+
const CAP_RECORD_TAG = Symbol.for("bunite.rpc.CapRecordToken");
|
|
8
|
+
const SCHEMA_TAG = Symbol.for("bunite.rpc.Schema");
|
|
8
9
|
declare const EXPORTED_CAP_BRAND: unique symbol;
|
|
9
10
|
|
|
10
11
|
export type ReturnsKind = "type" | "cap" | "capArray" | "capRecord";
|
|
@@ -96,25 +97,35 @@ export function stream<P = void, Y = unknown>(opts?: { hint?: Record<string, unk
|
|
|
96
97
|
return { [STREAM_TAG]: true, hint: opts?.hint };
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// Disposal: method only. Sync Disposable — wire drop is fire-and-forget.
|
|
99
101
|
export interface DisposalSpec<M extends MethodsRecord = MethodsRecord> {
|
|
100
102
|
method: keyof M & string;
|
|
101
|
-
async?: boolean;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export type MethodsRecord = Record<string, MethodDef>;
|
|
105
106
|
|
|
106
107
|
export interface CapDef<M extends MethodsRecord = MethodsRecord, D extends DisposalSpec<M> | undefined = undefined> {
|
|
107
108
|
readonly [CAP_TAG]: true;
|
|
109
|
+
readonly name: string;
|
|
110
|
+
readonly version?: string;
|
|
108
111
|
readonly methods: M;
|
|
109
112
|
readonly disposal: D;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
export interface DefineCapOpts<M extends MethodsRecord, D extends DisposalSpec<M> | undefined> {
|
|
116
|
+
version?: string | number;
|
|
117
|
+
disposal?: D;
|
|
118
|
+
}
|
|
119
|
+
|
|
112
120
|
export function defineCap<M extends MethodsRecord, D extends DisposalSpec<M> | undefined = undefined>(
|
|
121
|
+
name: string,
|
|
113
122
|
methods: M,
|
|
114
|
-
opts?:
|
|
123
|
+
opts?: DefineCapOpts<M, D>
|
|
115
124
|
): CapDef<M, D> {
|
|
116
125
|
return {
|
|
117
126
|
[CAP_TAG]: true,
|
|
127
|
+
name,
|
|
128
|
+
version: opts?.version != null ? String(opts.version) : undefined,
|
|
118
129
|
methods,
|
|
119
130
|
disposal: (opts?.disposal as D) ?? (undefined as D),
|
|
120
131
|
};
|
|
@@ -124,47 +135,20 @@ export function isCapDef(v: unknown): v is CapDef {
|
|
|
124
135
|
return typeof v === "object" && v !== null && (v as any)[CAP_TAG] === true;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
caps?: readonly CapDef<any, any>[];
|
|
130
|
-
}
|
|
138
|
+
// Schema = grouping sugar (Record<rootName, CapDef>). TS atomicity for serveAll.
|
|
139
|
+
export type SchemaRoots = Record<string, CapDef<any, any>>;
|
|
131
140
|
|
|
132
|
-
export interface Schema<
|
|
141
|
+
export interface Schema<R extends SchemaRoots = SchemaRoots> {
|
|
133
142
|
readonly [SCHEMA_TAG]: true;
|
|
134
|
-
readonly roots:
|
|
135
|
-
readonly caps: readonly CapDef<any, any>[];
|
|
136
|
-
topologyHash(): Promise<string>;
|
|
137
|
-
serve(impls: ImplsOf<S>): ServerDescriptor<S>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type ImplsOf<S extends SchemaShape> = {
|
|
141
|
-
[K in keyof S["roots"]]: ImplOf<S["roots"][K]>;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export interface ServerDescriptor<S extends SchemaShape = SchemaShape> {
|
|
145
|
-
readonly schema: Schema<S>;
|
|
146
|
-
readonly impls: ImplsOf<S>;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function defineSchema<S extends SchemaShape>(shape: S): Schema<S> {
|
|
150
|
-
const schema: Schema<S> = {
|
|
151
|
-
[SCHEMA_TAG]: true,
|
|
152
|
-
roots: shape.roots,
|
|
153
|
-
caps: shape.caps ?? [],
|
|
154
|
-
topologyHash: () => topologyHashImpl(schema),
|
|
155
|
-
serve(impls) {
|
|
156
|
-
return { schema, impls };
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
return schema;
|
|
143
|
+
readonly roots: R;
|
|
160
144
|
}
|
|
161
145
|
|
|
162
|
-
|
|
163
|
-
|
|
146
|
+
export type ImplsOf<R extends SchemaRoots> = {
|
|
147
|
+
[K in keyof R]: ImplOf<R[K]>;
|
|
164
148
|
};
|
|
165
149
|
|
|
166
|
-
export function
|
|
167
|
-
|
|
150
|
+
export function defineSchema<R extends SchemaRoots>(roots: R): Schema<R> {
|
|
151
|
+
return { [SCHEMA_TAG]: true, roots };
|
|
168
152
|
}
|
|
169
153
|
|
|
170
154
|
export function isSchema(v: unknown): v is Schema {
|
|
@@ -210,8 +194,7 @@ export type ClientReturn<R> =
|
|
|
210
194
|
R extends CapRefToken<infer C> ? ClientOf<C> :
|
|
211
195
|
R extends CapArrayToken<infer C> ?
|
|
212
196
|
C extends CapDef<any, infer D>
|
|
213
|
-
? [D] extends [
|
|
214
|
-
: [D] extends [DisposalSpec] ? ClientOf<C>[] & Disposable
|
|
197
|
+
? [D] extends [DisposalSpec] ? ClientOf<C>[] & Disposable
|
|
215
198
|
: ClientOf<C>[]
|
|
216
199
|
: ClientOf<C>[] :
|
|
217
200
|
R extends CapRecordToken<infer C> ? Record<string, ClientOf<C>> :
|
|
@@ -228,7 +211,7 @@ export type ClientOf<T> = T extends CapDef<infer M, infer D>
|
|
|
228
211
|
? [P] extends [void] ? () => Stream<Y>
|
|
229
212
|
: (params: P) => Stream<Y>
|
|
230
213
|
: never;
|
|
231
|
-
} & ([D] extends [
|
|
214
|
+
} & ([D] extends [DisposalSpec] ? Disposable : {})
|
|
232
215
|
: never;
|
|
233
216
|
|
|
234
217
|
export type ImplOf<T> = T extends CapDef<infer M, any>
|
package/src/rpc/wire.ts
CHANGED
|
@@ -16,7 +16,6 @@ export interface CallMeta {
|
|
|
16
16
|
parentCallId?: u53;
|
|
17
17
|
deadlineMs?: u32;
|
|
18
18
|
context?: Record<string, string>;
|
|
19
|
-
topologyHash?: string;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export type StreamEvent =
|
|
@@ -39,7 +38,7 @@ export type Frame =
|
|
|
39
38
|
op: "call";
|
|
40
39
|
id: u53;
|
|
41
40
|
target: Target;
|
|
42
|
-
method:
|
|
41
|
+
method: string;
|
|
43
42
|
args: unknown;
|
|
44
43
|
meta?: CallMeta;
|
|
45
44
|
}
|
|
@@ -47,7 +46,8 @@ export type Frame =
|
|
|
47
46
|
| { op: "result"; id: u53; ok: false; error: IpcStatus }
|
|
48
47
|
| { op: "cancel"; id: u53; reason?: string }
|
|
49
48
|
| ({ op: "stream"; id: u53 } & StreamEvent)
|
|
50
|
-
| { op: "drop"; caps: { id: u32; delta: u32 }[] }
|
|
49
|
+
| { op: "drop"; caps: { id: u32; delta: u32 }[] }
|
|
50
|
+
| { op: "cap_revoked"; capIds: u32[] };
|
|
51
51
|
|
|
52
52
|
export type CallFrame = Extract<Frame, { op: "call" }>;
|
|
53
53
|
export type ResultFrame = Extract<Frame, { op: "result" }>;
|
|
@@ -56,6 +56,7 @@ export type CancelFrame = Extract<Frame, { op: "cancel" }>;
|
|
|
56
56
|
export type DropFrame = Extract<Frame, { op: "drop" }>;
|
|
57
57
|
export type HelloFrame = Extract<Frame, { op: "hello" }>;
|
|
58
58
|
export type GoAwayFrame = Extract<Frame, { op: "goaway" }>;
|
|
59
|
+
export type CapRevokedFrame = Extract<Frame, { op: "cap_revoked" }>;
|
|
59
60
|
|
|
60
61
|
const OPS = new Set<Frame["op"]>([
|
|
61
62
|
"hello",
|
|
@@ -65,6 +66,7 @@ const OPS = new Set<Frame["op"]>([
|
|
|
65
66
|
"cancel",
|
|
66
67
|
"stream",
|
|
67
68
|
"drop",
|
|
69
|
+
"cap_revoked",
|
|
68
70
|
]);
|
|
69
71
|
|
|
70
72
|
export function isFrame(value: unknown): value is Frame {
|
|
@@ -79,7 +81,7 @@ export function isFrame(value: unknown): value is Frame {
|
|
|
79
81
|
case "goaway":
|
|
80
82
|
return f.reason === undefined || typeof f.reason === "string";
|
|
81
83
|
case "call":
|
|
82
|
-
return typeof f.id === "number" && typeof f.method === "
|
|
84
|
+
return typeof f.id === "number" && typeof f.method === "string"
|
|
83
85
|
&& typeof f.target === "object" && f.target !== null
|
|
84
86
|
&& (f.target as { kind?: unknown }).kind === "cap"
|
|
85
87
|
&& typeof (f.target as { id?: unknown }).id === "number";
|
|
@@ -92,6 +94,12 @@ export function isFrame(value: unknown): value is Frame {
|
|
|
92
94
|
&& (f.ev === "next" || f.ev === "credit" || f.ev === "end" || f.ev === "error");
|
|
93
95
|
case "drop":
|
|
94
96
|
return Array.isArray(f.caps);
|
|
97
|
+
case "cap_revoked":
|
|
98
|
+
if (!Array.isArray(f.capIds)) return false;
|
|
99
|
+
for (const id of f.capIds) {
|
|
100
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id > 0xFFFFFFFF) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
|
|
@@ -148,3 +156,9 @@ function readVarUint(buf: Buffer | Uint8Array): number {
|
|
|
148
156
|
|
|
149
157
|
export const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
150
158
|
export const PROTOCOL_VERSION = 1;
|
|
159
|
+
|
|
160
|
+
/** Reserved cap-name prefix for framework caps. User caps starting with this prefix are rejected. */
|
|
161
|
+
export const FRAMEWORK_NAME_PREFIX = "bunite.";
|
|
162
|
+
|
|
163
|
+
/** Synthetic method on cap-id 0 — dispatches to the bootstrap registry. */
|
|
164
|
+
export const BOOTSTRAP_METHOD = "bootstrap";
|
package/src/rpc/hash.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Schema,
|
|
3
|
-
type CapDef,
|
|
4
|
-
type MethodDef,
|
|
5
|
-
type DisposalSpec,
|
|
6
|
-
type AnyCapToken,
|
|
7
|
-
type ReturnsKind,
|
|
8
|
-
isCallDef,
|
|
9
|
-
isStreamDef,
|
|
10
|
-
isCapRef,
|
|
11
|
-
isCapArray,
|
|
12
|
-
isCapRecord,
|
|
13
|
-
_bindTopologyHash,
|
|
14
|
-
} from "./schema";
|
|
15
|
-
|
|
16
|
-
type CanonicalReturns =
|
|
17
|
-
| { kind: "type" }
|
|
18
|
-
| { kind: "cap" | "capArray" | "capRecord"; capIndex: number };
|
|
19
|
-
|
|
20
|
-
type CanonicalMethod =
|
|
21
|
-
| { name: string; kind: "call"; idempotent: boolean; returns: CanonicalReturns }
|
|
22
|
-
| { name: string; kind: "stream" };
|
|
23
|
-
|
|
24
|
-
type CanonicalCap = {
|
|
25
|
-
methods: CanonicalMethod[];
|
|
26
|
-
disposal?: { method: string; async: boolean };
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type CanonicalRoot = {
|
|
30
|
-
name: string;
|
|
31
|
-
capIndex: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type CanonicalSchema = {
|
|
35
|
-
v: 1;
|
|
36
|
-
roots: CanonicalRoot[];
|
|
37
|
-
caps: CanonicalCap[];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export function canonicalize(schema: Schema<any>): CanonicalSchema {
|
|
41
|
-
const capIndex = new Map<CapDef<any, any>, number>();
|
|
42
|
-
const caps: CanonicalCap[] = [];
|
|
43
|
-
|
|
44
|
-
for (const declared of schema.caps) {
|
|
45
|
-
if (capIndex.has(declared)) continue;
|
|
46
|
-
capIndex.set(declared, caps.length);
|
|
47
|
-
caps.push(null as unknown as CanonicalCap);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const intern = (c: CapDef<any, any>): number => {
|
|
51
|
-
const existing = capIndex.get(c);
|
|
52
|
-
if (existing !== undefined) return existing;
|
|
53
|
-
const idx = caps.length;
|
|
54
|
-
capIndex.set(c, idx);
|
|
55
|
-
caps.push(null as unknown as CanonicalCap);
|
|
56
|
-
return idx;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < caps.length; i++) {
|
|
60
|
-
if (caps[i] !== null) continue;
|
|
61
|
-
const declared = schema.caps[i];
|
|
62
|
-
caps[i] = capToCanonical(declared, intern);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const roots: CanonicalRoot[] = Object.keys(schema.roots).map((name) => ({
|
|
66
|
-
name,
|
|
67
|
-
capIndex: intern(schema.roots[name]),
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
for (let i = 0; i < caps.length; i++) {
|
|
71
|
-
if (caps[i] !== null) continue;
|
|
72
|
-
const cap = [...capIndex.keys()][i];
|
|
73
|
-
caps[i] = capToCanonical(cap, intern);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return { v: 1, roots, caps };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function capToCanonical(c: CapDef<any, any>, intern: (c: CapDef<any, any>) => number): CanonicalCap {
|
|
80
|
-
const methods: CanonicalMethod[] = Object.keys(c.methods).map((name) => {
|
|
81
|
-
const m = c.methods[name] as MethodDef;
|
|
82
|
-
if (isCallDef(m)) {
|
|
83
|
-
return {
|
|
84
|
-
name,
|
|
85
|
-
kind: "call",
|
|
86
|
-
idempotent: m.idempotent,
|
|
87
|
-
returns: returnsToCanonical(m.returns, intern),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
if (isStreamDef(m)) {
|
|
91
|
-
return { name, kind: "stream" };
|
|
92
|
-
}
|
|
93
|
-
throw new Error(`Unknown method def for "${name}"`);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const result: CanonicalCap = { methods };
|
|
97
|
-
const disposal = c.disposal as DisposalSpec | undefined;
|
|
98
|
-
if (disposal) {
|
|
99
|
-
result.disposal = { method: disposal.method, async: !!disposal.async };
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function returnsToCanonical(
|
|
105
|
-
ret: AnyCapToken | undefined,
|
|
106
|
-
intern: (c: CapDef<any, any>) => number
|
|
107
|
-
): CanonicalReturns {
|
|
108
|
-
if (!ret) return { kind: "type" };
|
|
109
|
-
if (isCapRef(ret)) return { kind: "cap", capIndex: intern(ret.cap) };
|
|
110
|
-
if (isCapArray(ret)) return { kind: "capArray", capIndex: intern(ret.cap) };
|
|
111
|
-
if (isCapRecord(ret)) return { kind: "capRecord", capIndex: intern(ret.cap) };
|
|
112
|
-
return { kind: "type" };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function canonicalJSON(value: unknown): string {
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
return "[" + value.map(canonicalJSON).join(",") + "]";
|
|
118
|
-
}
|
|
119
|
-
if (value && typeof value === "object") {
|
|
120
|
-
const keys = Object.keys(value as object).sort();
|
|
121
|
-
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON((value as Record<string, unknown>)[k])).join(",") + "}";
|
|
122
|
-
}
|
|
123
|
-
return JSON.stringify(value);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function topologyHash(schema: Schema<any>): Promise<string> {
|
|
127
|
-
const canonical = canonicalize(schema);
|
|
128
|
-
const json = canonicalJSON(canonical);
|
|
129
|
-
const bytes = new TextEncoder().encode(json);
|
|
130
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
131
|
-
return toHex(new Uint8Array(digest));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function toHex(bytes: Uint8Array): string {
|
|
135
|
-
let out = "";
|
|
136
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
137
|
-
out += bytes[i].toString(16).padStart(2, "0");
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
_bindTopologyHash(topologyHash);
|