fix: запуск занятия по HTTP в локальной сети без crypto.randomUUID
This commit is contained in:
parent
35ff14f2ea
commit
92b4684871
4 changed files with 34 additions and 5 deletions
|
|
@ -5,7 +5,7 @@ import { transform } from "esbuild";
|
|||
const source = await readFile(new URL("../src/shared/api/ws.ts", import.meta.url), "utf8");
|
||||
const { code } = await transform(source, { loader: "ts", format: "esm" });
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
||||
const { callChannel, stationChannel } = await import(moduleUrl);
|
||||
const { callChannel, randomUuid, stationChannel } = await import(moduleUrl);
|
||||
const mergeSource = await readFile(new URL("../src/features/kio-card/merge.ts", import.meta.url), "utf8");
|
||||
const { code: mergeCode } = await transform(mergeSource, { loader: "ts", format: "esm" });
|
||||
const { applyPatch, applyState, edit, empty } = await import(
|
||||
|
|
@ -119,4 +119,19 @@ socket.receive({ type: "station.state", snapshot: { card_id: "card-1", reply_tex
|
|||
socket = reconnect(socket);
|
||||
assert.deepEqual(socket.sent, [], "matching persisted station snapshot clears the reply");
|
||||
|
||||
// Стенд по http://<IP> — не защищённый контекст: randomUUID у браузера нет.
|
||||
const nativeRandomUuid = Object.getOwnPropertyDescriptor(globalThis.crypto, "randomUUID");
|
||||
Object.defineProperty(globalThis.crypto, "randomUUID", { value: undefined, configurable: true });
|
||||
try {
|
||||
const ids = Array.from({ length: 200 }, () => randomUuid());
|
||||
for (const id of ids) {
|
||||
assert.match(id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, "UUID v4 without randomUUID");
|
||||
}
|
||||
assert.equal(new Set(ids).size, ids.length, "UUIDs without randomUUID do not repeat");
|
||||
} finally {
|
||||
if (nativeRandomUuid) Object.defineProperty(globalThis.crypto, "randomUUID", nativeRandomUuid);
|
||||
else delete globalThis.crypto.randomUUID;
|
||||
}
|
||||
assert.match(randomUuid(), /^[0-9a-f-]{36}$/, "native randomUUID is used when present");
|
||||
|
||||
console.log("WebSocket durable outbox replay/ack tests passed");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { randomUuid } from "@/shared/api/ws";
|
||||
import type { ErrorCode, FindingDecision, SessionReport } from "@/shared/types/generated";
|
||||
|
||||
const DDS_CODES: ErrorCode[] = ["D1", "D2", "D3", "D4", "D5", "D6"];
|
||||
|
|
@ -77,7 +78,7 @@ export function FindingReview({ report, onChange }: {
|
|||
setError("Для отметки нужны факт и норма.");
|
||||
return;
|
||||
}
|
||||
pendingId.current ??= crypto.randomUUID();
|
||||
pendingId.current ??= randomUuid();
|
||||
const clientId = pendingId.current;
|
||||
void run(async () => {
|
||||
const updated = await send(base, {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "@/shared/api/http";
|
||||
import type { Signal } from "@/shared/api/http";
|
||||
import { sessionIdFromUrl } from "@/shared/api/session";
|
||||
import { randomUuid } from "@/shared/api/ws";
|
||||
import type { Level, SessionMode, SessionReport } from "@/shared/types/generated";
|
||||
|
||||
const MODES: { value: SessionMode; label: string; hint: string }[] = [
|
||||
|
|
@ -214,7 +215,7 @@ export function Instructor() {
|
|||
function startLesson() {
|
||||
if (startPendingId || toStart.current || startDisabledReason) return;
|
||||
if (trainees.data?.length && (!traineeId || (traineeId === "__demo__" && !DEMO_MODE))) return;
|
||||
const id = crypto.randomUUID();
|
||||
const id = randomUuid();
|
||||
// Номер занятия живёт в адресе: монитор и АРМ курсанта открываются ссылкой.
|
||||
window.history.replaceState(null, "", `/instructor?session=${id}`);
|
||||
toStart.current = id;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ const BUFFER_MAX_ITEMS = 100;
|
|||
|
||||
interface Buffered<Out> { key: string; commandId: string; event: Out; at: number }
|
||||
|
||||
/** UUID v4 для номера занятия и команд. `crypto.randomUUID` браузер отдаёт только
|
||||
* в защищённом контексте (HTTPS, localhost): стенд по `http://<IP>` в классе
|
||||
* его не видит, и запуск занятия молча падал. `getRandomValues` есть везде. */
|
||||
export function randomUuid(): string {
|
||||
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
export class Channel<In extends { type: string }, Out extends { type: string }> {
|
||||
private socket: WebSocket | null = null;
|
||||
private attempt = 0;
|
||||
|
|
@ -127,7 +139,7 @@ export class Channel<In extends { type: string }, Out extends { type: string }>
|
|||
existing.at = Date.now();
|
||||
pending = existing;
|
||||
} else {
|
||||
pending = { key, commandId: crypto.randomUUID(), event, at: Date.now() };
|
||||
pending = { key, commandId: randomUuid(), event, at: Date.now() };
|
||||
this.buffered.push(pending);
|
||||
if (this.buffered.length > BUFFER_MAX_ITEMS) this.buffered.shift();
|
||||
}
|
||||
|
|
@ -196,7 +208,7 @@ export const stationChannel = (
|
|||
if (event.type === "card.reply") return `card.reply:${event.card_id}`;
|
||||
// Non-replacement station actions are each their own replayable command.
|
||||
// The server deduplicates by this ID inside its persisted session snapshot.
|
||||
return `command:${crypto.randomUUID()}`;
|
||||
return `command:${randomUuid()}`;
|
||||
},
|
||||
isAcknowledged: (incoming, pending) => incoming.type === "station.state"
|
||||
&& pending.type === "card.reply"
|
||||
|
|
|
|||
Loading…
Reference in a new issue