Moved to ReactJS

This commit is contained in:
Alois 2026-03-14 12:35:30 +01:00
commit 1d0ebeb2b7
100 changed files with 1909 additions and 6056 deletions

View file

@ -15,12 +15,13 @@
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@solidjs/router": "^0.15.4",
"@tanstack/react-router": "^1.0.0",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/core-storage": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.10",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.3.6"
},
"devDependencies": {

View file

@ -1,15 +1,5 @@
import * as React from "react";
import { log } from "@tensamin/shared/log";
import {
createContext,
createEffect,
createSignal,
onCleanup,
Show,
untrack,
useContext,
type ParentProps,
} from "solid-js";
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
import {
PING_INTERVAL,
@ -21,17 +11,8 @@ import {
socket as schemas,
type Socket as Schemas,
} from "@tensamin/shared/data";
import { useStorage } from "@tensamin/core-storage/context";
import { useCrypto } from "@tensamin/core-crypto/context";
import Loading from "@tensamin/ui/screens/loading";
import ErrorScreen from "@tensamin/ui/screens/error";
import { useNavigate } from "@solidjs/router";
type OmikronData = {
id: number;
public_key: string;
ip_address: string;
};
type ContextType = {
send: BoundSendFn<Schemas>;
@ -40,273 +21,217 @@ type ContextType = {
iotaPing: () => number;
};
const socketContext = createContext<ContextType>();
const socketContext = React.createContext<ContextType | undefined>(undefined);
export default function Provider(props: ParentProps) {
const [omikron, setOmikron] = createSignal<OmikronData | null>(null);
const [readyState, setReadyState] = createSignal<number>(READY_STATE.CLOSED);
const [identified, setIdentified] = createSignal<boolean>(false);
export default function Provider(props: { children: React.ReactNode }) {
const [readyState, setReadyState] = React.useState<number>(READY_STATE.CLOSED);
const [connected, setConnected] = React.useState<boolean>(false);
const [ownPing, setOwnPing] = createSignal<number>(0);
const [iotaPing, setIotaPing] = createSignal<number>(0);
const [ownPing, setOwnPing] = React.useState<number>(0);
const [iotaPing, setIotaPing] = React.useState<number>(0);
const [error, setError] = createSignal<string>("");
const [errorDescription, setErrorDescription] = createSignal<string>("");
const [error, setError] = React.useState("");
const [errorDescription, setErrorDescription] = React.useState("");
const { load } = useStorage();
const { get_shared_secret, decrypt } = useCrypto();
const clientRef = React.useRef<ReturnType<
typeof createTransportClient<Schemas>
> | null>(null);
const navigate = useNavigate();
const send = React.useCallback<BoundSendFn<Schemas>>(
((
type: string,
data?: Record<string, unknown>,
options?: { id?: number; noResponse?: boolean },
) => {
const client = clientRef.current;
let client: ReturnType<typeof createTransportClient<Schemas>> | null = null;
// Load Omikron
createEffect(() => {
if (omikron()) return;
const controller = new AbortController();
(async () => {
try {
const userId = await load("user_id");
// Redirect to login
if (userId === 0) {
navigate("/login");
return;
}
const res = await fetch(
"https://omega.tensamin.net/api/get/omikron/" + String(userId),
{ signal: controller.signal },
);
const data = await res.json();
setOmikron(data);
} catch (e) {
if (controller.signal.aborted) return;
setError("Failed to load Omikron data");
setErrorDescription(
"An error occurred while fetching the Omikron server data. Please try again later.",
);
log(0, "Socket", "red", "Failed to fetch Omikron data", e);
if (!client) {
return Promise.reject(new Error("Socket is not connected"));
}
})();
onCleanup(() => controller.abort());
});
if (options?.noResponse) {
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: true,
});
}
createEffect(() => {
if (identified()) {
const interval = setInterval(async () => {
try {
const originalNow = Date.now();
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: false,
});
}) as BoundSendFn<Schemas>,
[],
);
const data = await send("ping", {
last_ping: originalNow,
});
const travelTime = Date.now() - originalNow;
setOwnPing(travelTime);
setIotaPing(data.data.ping_iota);
} catch (error) {
log(1, "Socket", "yellow", "Ping failed", error);
}
}, PING_INTERVAL);
onCleanup(() => clearInterval(interval));
React.useEffect(() => {
if (!connected) {
return;
}
});
// Create connection
createEffect(() => {
if (!omikron()) return;
const interval = setInterval(async () => {
try {
const originalNow = Date.now();
const data = await send("ping", {
last_ping: originalNow,
});
const travelTime = Date.now() - originalNow;
setOwnPing(travelTime);
const remotePing = data.data.ping_iota;
if (typeof remotePing === "number") {
setIotaPing(remotePing);
}
} catch (intervalError) {
log(1, "Socket", "yellow", "Ping failed", intervalError);
}
}, PING_INTERVAL);
return () => {
clearInterval(interval);
};
}, [connected, send]);
React.useEffect(() => {
let attempts = 0;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectScheduled = false;
let disposed = false;
const clearReconnectTimer = () => {
if (!reconnectTimer) {
return;
}
clearTimeout(reconnectTimer);
reconnectTimer = null;
reconnectScheduled = false;
};
const scheduleReconnect = (reason?: unknown) => {
if (disposed || reconnectScheduled) {
return;
}
if (attempts >= RETRY_COUNT) {
setError("Connection Failed");
setErrorDescription(
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
);
log(0, "Socket", "red", "Reconnection attempts exhausted", reason);
return;
}
attempts += 1;
reconnectScheduled = true;
reconnectTimer = setTimeout(() => {
reconnectScheduled = false;
reconnectTimer = null;
void connect();
}, RETRY_INTERVAL);
};
const transportClient = createTransportClient(schemas, {
url: TRANSPORT_URL,
onReadyStateChange: setReadyState,
onReadyStateChange: (state) => {
setReadyState(state);
if (state === READY_STATE.OPEN) {
attempts = 0;
clearReconnectTimer();
setConnected(true);
setError("");
setErrorDescription("");
log(1, "Socket", "green", "Connected");
return;
}
setConnected(false);
},
onClose: ({ error: closeError, intentional }) => {
setIdentified(false);
setConnected(false);
if (disposed || intentional) {
return;
}
log(0, "Socket", "red", "Disconnected", closeError);
if (attempts < RETRY_COUNT) {
attempts += 1;
reconnectTimer = setTimeout(() => {
void connect();
}, RETRY_INTERVAL);
return;
}
setError("Connection Failed");
setErrorDescription(
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
);
log(0, "Socket", "red", "Reconnection attempts exhausted", closeError);
scheduleReconnect(closeError);
},
});
client = transportClient;
async function identify(activeClient: typeof transportClient) {
const userId = await load("user_id");
const privateKey = await load("private_key");
const currentOmikron = untrack(() => omikron());
activeClient
.send("identification", { user_id: userId })
.then(async (data) => {
const ownUserData = await activeClient.send("get_user_data", {
user_id: userId,
});
if (!currentOmikron?.public_key) {
setError("Omikron data missing");
setErrorDescription(
"Omikron server data is missing. Please try again later.",
);
log(0, "Socket", "red", "Omikron public key missing");
return;
}
try {
const sharedSecret = await get_shared_secret(
privateKey,
ownUserData.data.public_key,
currentOmikron.public_key,
);
const solvedChallenge = await decrypt(
sharedSecret,
data.data.challenge,
);
activeClient
.send("challenge_response", {
challenge: btoa(solvedChallenge),
})
.then(() => {
if (client !== activeClient || disposed) {
return;
}
log(1, "Socket", "green", "Identification successful");
setIdentified(true);
setError("");
setErrorDescription("");
})
.catch((error) => {
log(0, "Socket", "red", "Challenge failed", error);
setError("Challenge Failed");
setErrorDescription(
"Failed to respond to the server's challenge. Please try again later.",
);
});
} catch (err) {
setError("Validation Failed");
setErrorDescription(
"Failed to validate the server's identity. Please try again later.",
);
log(0, "Socket", "red", "Server identity validation failed", err);
}
})
.catch((e) => {
log(0, "Socket", "red", "Identification failed", e);
setError("Identification Failed");
setErrorDescription(
"Failed to identify with the server. Please try again later.",
);
});
}
clientRef.current = transportClient;
async function connect() {
if (disposed) return;
if (disposed) {
return;
}
try {
await transportClient.connect(TRANSPORT_URL);
attempts = 0;
await identify(transportClient);
} catch (error) {
if (!disposed) {
log(0, "Socket", "red", "Connection attempt failed", error);
} catch (connectError) {
if (disposed) {
return;
}
log(0, "Socket", "red", "Connection attempt failed", connectError);
scheduleReconnect(connectError);
}
}
void connect();
onCleanup(() => {
return () => {
disposed = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
if (client === transportClient) {
client = null;
clearReconnectTimer();
if (clientRef.current === transportClient) {
clientRef.current = null;
}
void transportClient.close("context-dispose");
setReadyState(READY_STATE.CLOSED);
setIdentified(false);
});
});
setConnected(false);
};
}, []);
// Create Send Function
const send: BoundSendFn<Schemas> = ((
type: string,
data?: Record<string, unknown>,
options?: { id?: number; noResponse?: boolean },
) => {
if (!client) {
return Promise.reject(new Error("Socket is not connected"));
}
if (options?.noResponse) {
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: true,
});
}
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: false,
});
}) as BoundSendFn<Schemas>;
const progress = () => {
if (!omikron()) return 40;
if (readyState() !== READY_STATE.OPEN) return 70;
if (!identified()) return 90;
const progress = React.useMemo(() => {
if (readyState === READY_STATE.CONNECTING) return 70;
if (!connected) return 90;
return 100;
};
}, [connected, readyState]);
const contextValue = React.useMemo<ContextType>(
() => ({
send,
readyState: () => readyState,
ownPing: () => ownPing,
iotaPing: () => iotaPing,
}),
[iotaPing, ownPing, readyState, send],
);
if (error !== "" && errorDescription !== "") {
return <ErrorScreen error={error} description={errorDescription} />;
}
if (!connected) {
return <Loading progress={progress} />;
}
return (
<Show
when={error() === "" && errorDescription() === ""}
fallback={
<ErrorScreen error={error()} description={errorDescription()} />
}
>
<Show
when={omikron() && identified()}
fallback={<Loading progress={progress()} />}
>
<socketContext.Provider value={{ send, readyState, ownPing, iotaPing }}>
{props.children}
</socketContext.Provider>
</Show>
</Show>
<socketContext.Provider value={contextValue}>
{props.children}
</socketContext.Provider>
);
}
export function useSocket(): ContextType {
const context = useContext(socketContext);
if (!context)
const context = React.useContext(socketContext);
if (!context) {
throw new Error("useSocket must be used within a SocketProvider");
}
return context;
}
}

View file

@ -15,6 +15,14 @@ const APPLICATION_CLOSE_CODE = 0;
const APPLICATION_CLOSE_REASON = "epsilon-close";
const MAX_REQUEST_ID = 0xffff_fffe;
const DATA_VALUE_KIND_BOOL_TRUE = 0x01;
const DATA_VALUE_KIND_BOOL_FALSE = 0x02;
const DATA_VALUE_KIND_NUMBER = 0x03;
const DATA_VALUE_KIND_STRING = 0x04;
const DATA_VALUE_KIND_ARRAY = 0x05;
const DATA_VALUE_KIND_CONTAINER = 0x06;
const DATA_VALUE_KIND_NULL = 0x07;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
@ -966,34 +974,40 @@ function encodeCommunicationMessage(
message: TypedMessage<Record<string, unknown>>,
) {
const typeIndex = parseCommunicationType(message.type);
const dataBuffer = encodeValueForKind("container", message.data, "payload");
const hasId = message.id !== 0;
const totalLength = 2 + (hasId ? 4 : 0) + 4 + dataBuffer.byteLength;
const buffer = new Uint8Array(totalLength);
const dataBuffer = encodeContainerPayload(message.data, "payload");
const payloadLength = 2 + (hasId ? 4 : 0) + dataBuffer.byteLength;
const buffer = new Uint8Array(4 + payloadLength);
buffer[0] = typeIndex;
buffer[1] = hasId ? 0b0010_0000 : 0;
writeU32(buffer, 0, payloadLength);
buffer[4] = typeIndex;
buffer[5] = hasId ? 0b0000_0100 : 0;
let offset = 2;
let offset = 6;
if (hasId) {
writeU32(buffer, offset, message.id);
offset += 4;
}
writeU32(buffer, offset, dataBuffer.byteLength);
offset += 4;
buffer.set(dataBuffer, offset);
return buffer;
}
function decodeCommunicationMessage(payload: Uint8Array): TypedMessage {
const reader = new ByteReader(payload);
function decodeCommunicationMessage(frame: Uint8Array): TypedMessage {
const reader = new ByteReader(frame);
const payloadLength = reader.readU32();
if (payloadLength !== frame.byteLength - 4) {
throw new Error(
`Communication payload length mismatch: expected ${payloadLength}, received ${frame.byteLength - 4}`,
);
}
const typeIndex = reader.readU8();
const flags = reader.readU8();
const hasSender = (flags & 0b1000_0000) !== 0;
const hasReceiver = (flags & 0b0100_0000) !== 0;
const hasId = (flags & 0b0010_0000) !== 0;
const hasSender = (flags & 0b0000_0001) !== 0;
const hasReceiver = (flags & 0b0000_0010) !== 0;
const hasId = (flags & 0b0000_0100) !== 0;
const id = hasId ? reader.readU32() : 0;
if (hasSender) {
@ -1003,11 +1017,18 @@ function decodeCommunicationMessage(payload: Uint8Array): TypedMessage {
reader.readU48();
}
const dataLength = reader.readU32();
const dataBytes = reader.readBytes(dataLength);
const decodedData = decodeValue(new ByteReader(dataBytes));
if (!isPlainObject(decodedData)) {
throw new Error("Protocol payload container was not decoded as an object");
const consumedHeaderBytes =
2 + (hasId ? 4 : 0) + (hasSender ? 6 : 0) + (hasReceiver ? 6 : 0);
if (consumedHeaderBytes > payloadLength) {
throw new Error("Communication header exceeds payload length");
}
const dataLength = payloadLength - consumedHeaderBytes;
const dataReader = new ByteReader(reader.readBytes(dataLength));
const decodedData = decodeContainerPayload(dataReader);
if (!dataReader.isAtEnd()) {
throw new Error("Trailing bytes found after communication data payload");
}
if (!reader.isAtEnd()) {
@ -1051,13 +1072,21 @@ function getExpectedKind(type: string) {
return kind;
}
function encodeValueForKind(
type EncodedDataValue = {
kind: number;
payload: Uint8Array;
};
function encodeDataValueForKind(
kind: DataKind,
value: unknown,
path: string,
): Uint8Array {
): EncodedDataValue {
if (typeof kind === "object") {
return encodeArrayValue(kind.array, value, path);
return {
kind: DATA_VALUE_KIND_ARRAY,
payload: encodeArrayPayload(kind.array, value, path),
};
}
switch (kind) {
@ -1066,35 +1095,50 @@ function encodeValueForKind(
throw new Error(`Expected boolean at "${path}"`);
}
return Uint8Array.of(0x03, value ? 1 : 0);
return {
kind: value ? DATA_VALUE_KIND_BOOL_TRUE : DATA_VALUE_KIND_BOOL_FALSE,
payload: new Uint8Array(0),
};
case "number":
return encodeNumberValue(value, path);
return {
kind: DATA_VALUE_KIND_NUMBER,
payload: encodeNumberPayload(value, path),
};
case "string":
if (typeof value !== "string") {
throw new Error(`Expected string at "${path}"`);
}
return encodeStringValue(value);
return {
kind: DATA_VALUE_KIND_STRING,
payload: textEncoder.encode(value),
};
case "container":
if (!isPlainObject(value)) {
throw new Error(`Expected object at "${path}"`);
}
return encodeContainerValue(value, path);
return {
kind: DATA_VALUE_KIND_CONTAINER,
payload: encodeContainerPayload(value, path),
};
case "null":
if (value !== null && value !== undefined) {
throw new Error(`Expected null at "${path}"`);
}
return Uint8Array.of(0x06);
return {
kind: DATA_VALUE_KIND_NULL,
payload: new Uint8Array(0),
};
}
}
function encodeNumberValue(value: unknown, path: string) {
function encodeNumberPayload(value: unknown, path: string) {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
@ -1103,22 +1147,12 @@ function encodeNumberValue(value: unknown, path: string) {
throw new Error(`Expected safe integer at "${path}"`);
}
const buffer = new Uint8Array(9);
buffer[0] = 0x01;
writeI64(buffer, 1, value);
const buffer = new Uint8Array(8);
writeI64(buffer, 0, value);
return buffer;
}
function encodeStringValue(value: string) {
const bytes = textEncoder.encode(value);
const buffer = new Uint8Array(5 + bytes.byteLength);
buffer[0] = 0x02;
writeU32(buffer, 1, bytes.byteLength);
buffer.set(bytes, 5);
return buffer;
}
function encodeArrayValue(
function encodeArrayPayload(
innerKind: PrimitiveDataKind | "container" | "null",
value: unknown,
path: string,
@ -1132,28 +1166,44 @@ function encodeArrayValue(
}
const encodedItems = value.map((entry, index) =>
encodeValueForKind(innerKind, entry, `${path}[${index}]`),
encodeDataValueForKind(innerKind, entry, `${path}[${index}]`),
);
const byteLength = encodedItems.reduce(
(sum, item) => sum + item.byteLength,
0,
);
const buffer = new Uint8Array(4 + byteLength);
buffer[0] = 0x04;
buffer[1] = value.length === 0 ? 0x00 : kindToMarker(innerKind);
writeU16(buffer, 2, value.length);
let totalLength = 2;
for (const encodedItem of encodedItems) {
totalLength += 1;
let offset = 4;
if (!isBoolKindMarker(encodedItem.kind)) {
if (encodedItem.payload.byteLength > 0xffff) {
throw new Error(`Array item at "${path}" is too large for protocol encoding`);
}
totalLength += 2 + encodedItem.payload.byteLength;
}
}
const buffer = new Uint8Array(totalLength);
writeU16(buffer, 0, value.length);
let offset = 2;
for (const item of encodedItems) {
buffer.set(item, offset);
offset += item.byteLength;
buffer[offset] = item.kind;
offset += 1;
if (isBoolKindMarker(item.kind)) {
continue;
}
writeU16(buffer, offset, item.payload.byteLength);
offset += 2;
buffer.set(item.payload, offset);
offset += item.payload.byteLength;
}
return buffer;
}
function encodeContainerValue(value: Record<string, unknown>, path: string) {
function encodeContainerPayload(value: Record<string, unknown>, path: string) {
const normalizedEntries = new Map<
string,
{ index: number; value: unknown }
@ -1171,82 +1221,103 @@ function encodeContainerValue(value: Record<string, unknown>, path: string) {
([, left], [, right]) => left.index - right.index,
);
const encodedEntries: Uint8Array[] = [];
let totalLength = 3;
if (entries.length > 0xffff) {
throw new Error(`Container at "${path}" has too many entries for protocol encoding`);
}
const encodedEntries: Array<{ keyIndex: number; value: EncodedDataValue }> = [];
let totalLength = 2;
for (const [name, entry] of entries) {
const expectedKind = getExpectedKind(name);
const pathForEntry = `${path}.${name}`;
if (expectedKind === "bool") {
if (typeof entry.value !== "boolean") {
throw new Error(`Expected boolean at "${pathForEntry}"`);
}
const encoded = Uint8Array.of(entry.index, entry.value ? 1 : 0);
encodedEntries.push(encoded);
totalLength += encoded.byteLength;
continue;
}
const encodedValue = encodeValueForKind(
const encodedValue = encodeDataValueForKind(
expectedKind,
entry.value,
pathForEntry,
);
const body = encodedValue.subarray(1);
const encoded = new Uint8Array(5 + body.byteLength);
encoded[0] = entry.index;
writeU32(encoded, 1, body.byteLength);
encoded.set(body, 5);
encodedEntries.push(encoded);
totalLength += encoded.byteLength;
if (isBoolKindMarker(encodedValue.kind)) {
totalLength += 2;
} else {
if (encodedValue.payload.byteLength > 0xffff) {
throw new Error(`Container entry "${pathForEntry}" is too large for protocol encoding`);
}
totalLength += 4 + encodedValue.payload.byteLength;
}
encodedEntries.push({
keyIndex: entry.index,
value: encodedValue,
});
}
const buffer = new Uint8Array(totalLength);
buffer[0] = 0x05;
writeU16(buffer, 1, entries.length);
writeU16(buffer, 0, entries.length);
let offset = 3;
for (const encoded of encodedEntries) {
buffer.set(encoded, offset);
offset += encoded.byteLength;
let offset = 2;
for (const entry of encodedEntries) {
buffer[offset] = entry.value.kind;
offset += 1;
if (isBoolKindMarker(entry.value.kind)) {
buffer[offset] = entry.keyIndex;
offset += 1;
continue;
}
writeU16(buffer, offset, entry.value.payload.byteLength);
offset += 2;
buffer[offset] = entry.keyIndex;
offset += 1;
buffer.set(entry.value.payload, offset);
offset += entry.value.payload.byteLength;
}
return buffer;
}
function decodeValue(reader: ByteReader): unknown {
const marker = reader.readU8();
function decodeValuePayload(marker: number, payload: Uint8Array): unknown {
const reader = new ByteReader(payload);
switch (marker) {
case 0x01:
return reader.readI64();
case 0x02: {
const length = reader.readU32();
return textDecoder.decode(reader.readBytes(length));
}
case 0x03:
return reader.readU8() !== 0;
case 0x04: {
reader.readU8();
const length = reader.readU16();
const values: unknown[] = [];
for (let index = 0; index < length; index += 1) {
values.push(decodeValue(reader));
case DATA_VALUE_KIND_BOOL_TRUE:
if (!reader.isAtEnd()) {
throw new Error("Unexpected payload for boolean true value");
}
return values;
}
return true;
case 0x05:
return decodeContainer(reader);
case DATA_VALUE_KIND_BOOL_FALSE:
if (!reader.isAtEnd()) {
throw new Error("Unexpected payload for boolean false value");
}
return false;
case DATA_VALUE_KIND_NUMBER:
if (payload.byteLength !== 8) {
throw new Error(`Invalid number payload length ${payload.byteLength}`);
}
return reader.readI64();
case DATA_VALUE_KIND_STRING:
return textDecoder.decode(payload);
case DATA_VALUE_KIND_ARRAY:
return decodeArrayPayload(reader);
case DATA_VALUE_KIND_CONTAINER:
return decodeContainerPayload(reader);
case DATA_VALUE_KIND_NULL:
if (!reader.isAtEnd()) {
throw new Error("Unexpected payload for null value");
}
case 0x06:
return null;
default:
@ -1254,39 +1325,86 @@ function decodeValue(reader: ByteReader): unknown {
}
}
function decodeContainer(reader: ByteReader) {
const length = reader.readU16();
const value: Record<string, unknown> = {};
function decodeArrayPayload(reader: ByteReader) {
const itemCount = reader.readU16();
const values: unknown[] = [];
for (let index = 0; index < length; index += 1) {
const keyIndex = reader.readU8();
const key = DATA_TYPES[keyIndex];
if (!key) {
throw new Error(`Unknown data type index ${keyIndex}`);
}
for (let index = 0; index < itemCount; index += 1) {
const marker = reader.readU8();
const expectedKind = getExpectedKind(key);
if (expectedKind === "bool") {
value[key] = normalizeIncomingValue(key, reader.readU8() !== 0);
if (isBoolKindMarker(marker)) {
values.push(marker === DATA_VALUE_KIND_BOOL_TRUE);
continue;
}
const encodedLength = reader.readU32();
const encodedValue = reader.readBytes(encodedLength);
const fullValue = new Uint8Array(1 + encodedValue.byteLength);
fullValue[0] = kindToMarker(expectedKind);
fullValue.set(encodedValue, 1);
const payloadLength = reader.readU16();
const payload = reader.readBytes(payloadLength);
values.push(decodeValuePayload(marker, payload));
}
value[key] = normalizeIncomingValue(
key,
decodeValue(new ByteReader(fullValue)),
);
return values;
}
function decodeContainerPayload(reader: ByteReader) {
const entryCount = reader.readU16();
const value: Record<string, unknown> = {};
for (let index = 0; index < entryCount; index += 1) {
const marker = reader.readU8();
const payloadLength = isBoolKindMarker(marker) ? 0 : reader.readU16();
const keyIndex = reader.readU8();
const key = getDataTypeNameByIndex(keyIndex);
const payload = isBoolKindMarker(marker)
? new Uint8Array(0)
: reader.readBytes(payloadLength);
const expectedKind = getExpectedKind(key);
if (!isMarkerCompatibleWithKind(marker, expectedKind)) {
throw new Error(
`Unexpected marker 0x${marker.toString(16)} for data type "${key}"`,
);
}
value[key] = normalizeIncomingValue(key, decodeValuePayload(marker, payload));
}
return value;
}
function getDataTypeNameByIndex(index: number) {
const key = DATA_TYPES[index];
if (!key) {
throw new Error(`Unknown data type index ${index}`);
}
return key;
}
function isBoolKindMarker(marker: number) {
return (
marker === DATA_VALUE_KIND_BOOL_TRUE || marker === DATA_VALUE_KIND_BOOL_FALSE
);
}
function isMarkerCompatibleWithKind(marker: number, kind: DataKind) {
if (typeof kind === "object") {
return marker === DATA_VALUE_KIND_ARRAY;
}
switch (kind) {
case "bool":
return isBoolKindMarker(marker);
case "number":
return marker === DATA_VALUE_KIND_NUMBER;
case "string":
return marker === DATA_VALUE_KIND_STRING;
case "container":
return marker === DATA_VALUE_KIND_CONTAINER;
case "null":
return marker === DATA_VALUE_KIND_NULL;
}
}
function normalizeOutgoingValue(type: string, value: unknown) {
if (SCALAR_NUMBER_ARRAY_DATA_TYPES.has(type) && typeof value === "number") {
return [value];
@ -1308,25 +1426,6 @@ function normalizeIncomingValue(type: string, value: unknown) {
return value;
}
function kindToMarker(kind: DataKind) {
if (typeof kind === "object") {
return 0x04;
}
switch (kind) {
case "number":
return 0x01;
case "string":
return 0x02;
case "bool":
return 0x03;
case "container":
return 0x05;
case "null":
return 0x06;
}
}
function writeU16(buffer: Uint8Array, offset: number, value: number) {
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).setUint16(
offset,

View file

@ -3,8 +3,7 @@
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true