js-dos 8.1.0 → 8.1.2
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/README.md +7 -6
- package/index.html +6 -2
- package/package.json +1 -1
- package/src/frame/editor/defaults.ts +3 -3
- package/src/frame/editor/editor-conf-frame.tsx +2 -1
- package/src/frame/fat-drives-frame.tsx +104 -42
- package/src/frame/quick-save-frame.tsx +12 -10
- package/src/i18n.ts +6 -0
- package/src/main.tsx +1 -1
- package/src/public/types.ts +4 -0
- package/src/sidebar/cloud-save-button.tsx +1 -1
- package/src/sidebar/sidebar-button.tsx +1 -1
- package/src/sockdrive/js/src/sockdrive/cache.ts +8 -85
- package/src/sockdrive/js/src/sockdrive/drive.ts +20 -10
- package/src/sockdrive/js/src/test/test.ts +6 -5
- package/src/store/init.ts +2 -2
- package/src/store/ui.ts +5 -0
- package/src/store.tsx +10 -2
- package/src/ui.tsx +36 -0
- package/src/v8/config.ts +2 -0
- package/src/window/dos/dos-window.tsx +8 -1
- package/src/ws/ws-sockdrive.ts +38 -16
- package/src/ws/ws-transport-layer.ts +31 -18
package/README.md
CHANGED
|
@@ -14,9 +14,14 @@ The key features:
|
|
|
14
14
|
* Cloud storage
|
|
15
15
|
* WebAssembly and pure JS versions
|
|
16
16
|
|
|
17
|
+
## Demo
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
[
|
|
19
|
+
* [Win 95](https://v8.js-dos.com) - plain Windows 95 with D3D & 3Dfx
|
|
20
|
+
* [DOS.Zone](https://dos.zone) - community portal with 1900+ dos games
|
|
21
|
+
* [Multiplayer](https://dos.zone/multiplayer) - multiplayer games (Doom, Heroes 2, etc.)
|
|
22
|
+
* [Diablo I](https://dos.zone/diablo-1996/)
|
|
23
|
+
* Dune 2000
|
|
24
|
+
[](https://youtu.be/lhFrAe5YrJE)
|
|
20
25
|
|
|
21
26
|
## Documentation
|
|
22
27
|
|
|
@@ -26,10 +31,6 @@ The key features:
|
|
|
26
31
|
* [js-dos 3.xx](https://js-dos.com/index_v3.html)
|
|
27
32
|
* [Repository](https://github.com/caiiiycuk/js-dos-docs)
|
|
28
33
|
|
|
29
|
-
Demo:
|
|
30
|
-
|
|
31
|
-
[DOS.Zone](https://dos.zone)
|
|
32
|
-
|
|
33
34
|
## Development
|
|
34
35
|
|
|
35
36
|
1. You need to install node dependencies and put `emulators@beta` into `public/emulators`.
|
package/index.html
CHANGED
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
// console.log("Hardware backend for", backend);
|
|
43
43
|
// return Promise.resolve("ws://127.0.0.1:8080");
|
|
44
44
|
//},
|
|
45
|
+
// sockdriveBackend: {
|
|
46
|
+
// name: "makevm",
|
|
47
|
+
// host: "backend.make-vm.com:8001",
|
|
48
|
+
// },
|
|
45
49
|
onEvent: (event) => {
|
|
46
50
|
console.log("Event", event)
|
|
47
51
|
if (event === "ci-ready") {
|
|
@@ -96,8 +100,8 @@ scaler=none
|
|
|
96
100
|
|
|
97
101
|
[autoexec]
|
|
98
102
|
echo off
|
|
99
|
-
imgmount 2 sockdrive wss://
|
|
100
|
-
imgmount 3 sockdrive wss://
|
|
103
|
+
imgmount 2 sockdrive wss://sockdrive.js-dos.com:8001 dos.zone diablo_109_c
|
|
104
|
+
imgmount 3 sockdrive wss://sockdrive.js-dos.com:8001 dos.zone diablo_109_d
|
|
101
105
|
boot c:
|
|
102
106
|
`;
|
|
103
107
|
</script>
|
package/package.json
CHANGED
|
@@ -237,7 +237,7 @@ scaler=none
|
|
|
237
237
|
|
|
238
238
|
[autoexec]
|
|
239
239
|
echo off
|
|
240
|
-
imgmount 2 sockdrive {wss-makevm}
|
|
240
|
+
imgmount 2 sockdrive {wss-makevm} system dos7.1-v1
|
|
241
241
|
echo Please visit our website:
|
|
242
242
|
echo
|
|
243
243
|
echo _ __
|
|
@@ -312,7 +312,7 @@ scaler=none
|
|
|
312
312
|
|
|
313
313
|
[autoexec]
|
|
314
314
|
echo off
|
|
315
|
-
imgmount 2 sockdrive {wss-makevm}
|
|
315
|
+
imgmount 2 sockdrive {wss-makevm} system win95-v1
|
|
316
316
|
echo Please visit our website:
|
|
317
317
|
echo
|
|
318
318
|
echo _ __
|
|
@@ -387,7 +387,7 @@ scaler=none
|
|
|
387
387
|
|
|
388
388
|
[autoexec]
|
|
389
389
|
echo off
|
|
390
|
-
imgmount 2 sockdrive {wss-makevm}
|
|
390
|
+
imgmount 2 sockdrive {wss-makevm} system win98-v1
|
|
391
391
|
echo Please visit our website:
|
|
392
392
|
echo
|
|
393
393
|
echo _ __
|
|
@@ -24,7 +24,8 @@ export function EditorConf() {
|
|
|
24
24
|
const account = useSelector((state: State) => state.auth.account);
|
|
25
25
|
const backend = useSelector((state: State) => state.dos.backend);
|
|
26
26
|
const { sockdriveEndpoint, sockdriveWssEndpoint } = useSelector((state: State) =>
|
|
27
|
-
sockdriveBackend[state.init.sockdriveBackendName]
|
|
27
|
+
sockdriveBackend[state.init.sockdriveBackendName] ??
|
|
28
|
+
sockdriveBackend["js-dos"]);
|
|
28
29
|
const dispatch = useDispatch();
|
|
29
30
|
const [myDrives, setMyDrives] = useState<{ name: string, owner: string }[]>([]);
|
|
30
31
|
|
|
@@ -3,7 +3,7 @@ import { SockdriveBackend } from "../components/dos-option-select";
|
|
|
3
3
|
import { useDispatch, useSelector } from "react-redux";
|
|
4
4
|
import { sockdriveBackend } from "../store/init";
|
|
5
5
|
import { State } from "../store";
|
|
6
|
-
import { useEffect, useState } from "preact/hooks";
|
|
6
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
7
7
|
import { dispatchLoginAction, uiSlice } from "../store/ui";
|
|
8
8
|
import { LockBadge } from "../components/lock";
|
|
9
9
|
|
|
@@ -25,17 +25,27 @@ export function FatDrivesFrame() {
|
|
|
25
25
|
const preimium = account?.premium ?? false;
|
|
26
26
|
const me = account?.email ?? "";
|
|
27
27
|
const [filter, setFilter] = useState<string>("");
|
|
28
|
+
const [busy, setBusy] = useState<boolean>(true);
|
|
29
|
+
const forkDialogRef = useRef<HTMLDialogElement>(null);
|
|
30
|
+
const deleteDialogRef = useRef<HTMLDialogElement>(null);
|
|
31
|
+
const [forkName, setForkName] = useState<string>("");
|
|
32
|
+
const [deleteConfirm, setDeleteConfirm] = useState<string>("");
|
|
33
|
+
const [dialogError, setDialogError] = useState<string | null>(null);
|
|
34
|
+
const [dialogIndex, setDialogIndex] = useState<number>(-1);
|
|
35
|
+
|
|
28
36
|
|
|
29
37
|
function reloadDrives() {
|
|
30
38
|
if (!account || !account.token) {
|
|
31
39
|
setMyDrives([]);
|
|
32
40
|
} else {
|
|
41
|
+
setBusy(true);
|
|
33
42
|
fetch(sockdriveEndpoint + "/list/drives/" + account.token.access_token)
|
|
34
43
|
.then((r) => r.json())
|
|
35
44
|
.then((drives: Drive[]) => {
|
|
36
45
|
setMyDrives(drives.sort((a, b) => a.name.localeCompare(b.name)));
|
|
37
46
|
})
|
|
38
|
-
.catch(console.error)
|
|
47
|
+
.catch(console.error)
|
|
48
|
+
.finally(() => setBusy(false));
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
|
|
@@ -48,7 +58,7 @@ export function FatDrivesFrame() {
|
|
|
48
58
|
<input class="input input-bordered input-sm w-full" placeholder={t("filter")}
|
|
49
59
|
value={filter} onChange={(e) => setFilter(e.currentTarget.value ?? "")}></input>
|
|
50
60
|
<div class="w-full">
|
|
51
|
-
<table class="table w-full">
|
|
61
|
+
{!busy && <table class="table w-full">
|
|
52
62
|
<thead>
|
|
53
63
|
<tr>
|
|
54
64
|
<th>{t("net_drives")}</th>
|
|
@@ -76,47 +86,13 @@ export function FatDrivesFrame() {
|
|
|
76
86
|
{preimium && <td>
|
|
77
87
|
{account?.token && <div class="btn btn-ghost btn-xs"
|
|
78
88
|
onClick={() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// eslint-disable-next-line max-len
|
|
82
|
-
fetch(`${sockdriveEndpoint}/fork/drive/${owner}/${name}/${newName}/${account.token.access_token}`, {
|
|
83
|
-
method: "POST",
|
|
84
|
-
})
|
|
85
|
-
.then((r) => r.json())
|
|
86
|
-
.then((response) => {
|
|
87
|
-
if (response.error) {
|
|
88
|
-
throw new Error(response.error);
|
|
89
|
-
} else {
|
|
90
|
-
setMyDrives([]);
|
|
91
|
-
reloadDrives();
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
.catch((e) => {
|
|
95
|
-
console.error(e.message);
|
|
96
|
-
dispatch(uiSlice.actions
|
|
97
|
-
.showToast({ message: e.message, intent: "error" }));
|
|
98
|
-
});
|
|
99
|
-
}
|
|
89
|
+
setDialogIndex(index);
|
|
90
|
+
forkDialogRef.current?.showModal();
|
|
100
91
|
}}>Fork</div>}
|
|
101
92
|
{account?.token && owner === me && <div class="btn btn-xs btn-ghost"
|
|
102
93
|
onClick={() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.then((r) => r.json())
|
|
106
|
-
.then((response) => {
|
|
107
|
-
if (response.error) {
|
|
108
|
-
throw new Error(response.error);
|
|
109
|
-
} else {
|
|
110
|
-
const newDrives = [...myDrives];
|
|
111
|
-
newDrives.splice(index, 1);
|
|
112
|
-
setMyDrives(newDrives);
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
.catch((e) => {
|
|
116
|
-
console.error(e.message);
|
|
117
|
-
dispatch(uiSlice.actions
|
|
118
|
-
.showToast({ message: e.message, intent: "error" }));
|
|
119
|
-
});
|
|
94
|
+
setDialogIndex(index);
|
|
95
|
+
deleteDialogRef.current?.showModal();
|
|
120
96
|
}}>{t("delete")}</div>}
|
|
121
97
|
</td>}
|
|
122
98
|
{!preimium && <td>
|
|
@@ -130,6 +106,92 @@ export function FatDrivesFrame() {
|
|
|
130
106
|
})}
|
|
131
107
|
</tbody>
|
|
132
108
|
</table>
|
|
109
|
+
}
|
|
110
|
+
{busy && <span class="loading loading-spinner loading-lg"></span>}
|
|
133
111
|
</div>
|
|
134
|
-
|
|
112
|
+
<dialog ref={forkDialogRef} class="modal">
|
|
113
|
+
{!busy && dialogIndex >= 0 && account !== null && <div class="modal-box">
|
|
114
|
+
<h3 class="font-bold text-lg">{t("enter_name_of_drive")}:</h3>
|
|
115
|
+
<p class="text-xs text-neutral-content mt-2">
|
|
116
|
+
fork of: {myDrives[dialogIndex].owner}/{myDrives[dialogIndex].name}
|
|
117
|
+
</p>
|
|
118
|
+
<input type="text" placeholder="name" value={forkName}
|
|
119
|
+
class="mt-6 input input-bordered input-primary w-full" onChange={(e) =>
|
|
120
|
+
setForkName(e.currentTarget.value)} />
|
|
121
|
+
{dialogError !== null &&
|
|
122
|
+
<p class="mt-2 text-error">{t("error")}: {dialogError}</p>}
|
|
123
|
+
<div class="modal-action">
|
|
124
|
+
{forkName.length > 0 && <button class="btn mr-2 btn-primary" onClick={() => {
|
|
125
|
+
setDialogIndex(-1);
|
|
126
|
+
setBusy(true);
|
|
127
|
+
// eslint-disable-next-line max-len
|
|
128
|
+
fetch(`${sockdriveEndpoint}/fork/drive/${myDrives[dialogIndex].owner}/${myDrives[dialogIndex].name}/${forkName}/${account.token.access_token}`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
})
|
|
131
|
+
.then((r) => r.json())
|
|
132
|
+
.then((response) => {
|
|
133
|
+
if (response.error) {
|
|
134
|
+
throw new Error(response.error);
|
|
135
|
+
} else {
|
|
136
|
+
setMyDrives([]);
|
|
137
|
+
reloadDrives();
|
|
138
|
+
}
|
|
139
|
+
forkDialogRef.current?.close();
|
|
140
|
+
})
|
|
141
|
+
.catch((e) => {
|
|
142
|
+
console.error(e.message);
|
|
143
|
+
dispatch(uiSlice.actions
|
|
144
|
+
.showToast({ message: e.message, intent: "error" }));
|
|
145
|
+
})
|
|
146
|
+
.finally(() => {
|
|
147
|
+
setBusy(false);
|
|
148
|
+
});
|
|
149
|
+
}}>{t("fork")}</button>}
|
|
150
|
+
<button class="btn" onClick={() => forkDialogRef.current?.close()}>{t("close")}</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>}
|
|
153
|
+
</dialog>
|
|
154
|
+
<dialog ref={deleteDialogRef} class="modal">
|
|
155
|
+
{!busy && dialogIndex >= 0 && account !== null && <div class="modal-box">
|
|
156
|
+
<h3 class="font-bold text-lg">{t("enter_name_of_drive")}:</h3>
|
|
157
|
+
<p class="text-xs text-neutral mt-2">{myDrives[dialogIndex].owner}/{myDrives[dialogIndex].name}</p>
|
|
158
|
+
<input type="text" placeholder="name" value={deleteConfirm}
|
|
159
|
+
class="mt-6 input input-bordered input-primary w-full"
|
|
160
|
+
onChange={(e) => setDeleteConfirm(e.currentTarget.value)} />
|
|
161
|
+
{dialogError !== null &&
|
|
162
|
+
<p class="mt-2 text-error">{t("error")}: {setDialogError}</p>}
|
|
163
|
+
<div class="modal-action">
|
|
164
|
+
{deleteConfirm === myDrives[dialogIndex].owner + "/" + myDrives[dialogIndex].name &&
|
|
165
|
+
<button class="btn mr-2 btn-primary" onClick={() => {
|
|
166
|
+
setDialogIndex(-1);
|
|
167
|
+
setBusy(true);
|
|
168
|
+
// eslint-disable-next-line max-len
|
|
169
|
+
fetch(`${sockdriveEndpoint}/fork/delete/${myDrives[dialogIndex].owner}/${myDrives[dialogIndex].name}/${account.token.access_token}`, { method: "POST" })
|
|
170
|
+
.then((r) => r.json())
|
|
171
|
+
.then((response) => {
|
|
172
|
+
if (response.error) {
|
|
173
|
+
throw new Error(response.error);
|
|
174
|
+
} else {
|
|
175
|
+
const newDrives = [...myDrives];
|
|
176
|
+
newDrives.splice(dialogIndex, 1);
|
|
177
|
+
setMyDrives(newDrives);
|
|
178
|
+
}
|
|
179
|
+
deleteDialogRef.current?.close();
|
|
180
|
+
})
|
|
181
|
+
.catch((e) => {
|
|
182
|
+
console.error(e.message);
|
|
183
|
+
setDialogError(e.message);
|
|
184
|
+
dispatch(uiSlice.actions
|
|
185
|
+
.showToast({ message: e.message, intent: "error" }));
|
|
186
|
+
})
|
|
187
|
+
.finally(() => {
|
|
188
|
+
setBusy(false);
|
|
189
|
+
});
|
|
190
|
+
}}>{t("delete")}</button>}
|
|
191
|
+
<button class="btn" onClick={() => deleteDialogRef.current?.close()}>{t("close")}</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
}
|
|
195
|
+
</dialog >
|
|
196
|
+
</div >;
|
|
135
197
|
}
|
|
@@ -6,20 +6,22 @@ import { State, useNonSerializableStore } from "../store";
|
|
|
6
6
|
import { dispatchLoginAction } from "../store/ui";
|
|
7
7
|
|
|
8
8
|
export function QuickSaveFrame() {
|
|
9
|
-
const premium = useSelector((state: State) => state.auth.account)?.premium;
|
|
9
|
+
const premium = useSelector((state: State) => state.auth.account)?.premium && false;
|
|
10
10
|
return <div class="quick-save-frame flex flex-col justify-center items-center -my-2">
|
|
11
11
|
<CloudSaveButton />
|
|
12
|
-
<SaveButton label=
|
|
13
|
-
<LoadButton label=
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
<SaveButton label="quick" bgcolor="bg-primary" textcolor="text-primary-content" />
|
|
13
|
+
<LoadButton label="quick" bgcolor="bg-primary" />
|
|
14
|
+
{premium && <>
|
|
15
|
+
<SaveButton disabled={!premium} label={2} bgcolor="bg-secondary" textcolor="text-secondary-content" />
|
|
16
|
+
<LoadButton disabled={!premium} label={2} bgcolor="bg-secondary" />
|
|
17
|
+
<SaveButton disabled={!premium} label={3} bgcolor="bg-accent" textcolor="text-accent-content" />
|
|
18
|
+
<LoadButton disabled={!premium} label={3} bgcolor="bg-accent" />
|
|
19
|
+
</>}
|
|
18
20
|
</div>;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function SaveButton(props: {
|
|
22
|
-
label: number,
|
|
24
|
+
label: number | string,
|
|
23
25
|
bgcolor: string,
|
|
24
26
|
textcolor: string,
|
|
25
27
|
disabled?: boolean,
|
|
@@ -59,7 +61,7 @@ function SaveButton(props: {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function LoadButton(props: {
|
|
62
|
-
label: number,
|
|
64
|
+
label: number | string,
|
|
63
65
|
bgcolor: string,
|
|
64
66
|
disabled?: boolean,
|
|
65
67
|
}) {
|
|
@@ -84,7 +86,7 @@ function LoadButton(props: {
|
|
|
84
86
|
|
|
85
87
|
return <div class={"sidebar-button flex justify-center " + (props.disabled ? " opacity-50" : "")}
|
|
86
88
|
onClick={onClick}>
|
|
87
|
-
<DisketteIcon class="opacity-10"/>
|
|
89
|
+
<DisketteIcon class="opacity-10" />
|
|
88
90
|
{props.disabled === true && <LockBadge />}
|
|
89
91
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
90
92
|
viewBox="0 0 24 24" stroke-width="1.5"
|
package/src/i18n.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { State } from "./store";
|
|
|
4
4
|
|
|
5
5
|
const translations: {[lang: string]: {[key: string]: string} } = {
|
|
6
6
|
ru: {
|
|
7
|
+
enter_name_of_drive: "Введите название диска",
|
|
8
|
+
ws_outdated: "Необходимо обновить DOS Браузер",
|
|
9
|
+
update: "Обновить",
|
|
7
10
|
preloading_sockdrive: "Загрузка FAT",
|
|
8
11
|
filter: "Фильтр",
|
|
9
12
|
add: "Добавить",
|
|
@@ -75,6 +78,9 @@ const translations: {[lang: string]: {[key: string]: string} } = {
|
|
|
75
78
|
fat_write: "FAT запись",
|
|
76
79
|
},
|
|
77
80
|
en: {
|
|
81
|
+
enter_name_of_drive: "Enter name of drive",
|
|
82
|
+
ws_outdated: "Must update DOS Browser App",
|
|
83
|
+
update: "Update",
|
|
78
84
|
preloading_sockdrive: "Preloading FAT",
|
|
79
85
|
filter: "Filter",
|
|
80
86
|
add: "Add",
|
package/src/main.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { NonSerializableStore, Store, makeNonSerializableStore, makeStore, postJ
|
|
|
18
18
|
export const Dos: DosFn = (element: HTMLDivElement,
|
|
19
19
|
options: Partial<DosOptions> = {}): DosProps => {
|
|
20
20
|
const nonSerializableStore = makeNonSerializableStore(options);
|
|
21
|
-
const store = makeStore(nonSerializableStore);
|
|
21
|
+
const store = makeStore(nonSerializableStore, options);
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
if (store.getState().auth.account?.email === "dz.caiiiycuk@gmail.com" ||
|
package/src/public/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function CloudSaveButton(props: {
|
|
|
12
12
|
}) {
|
|
13
13
|
const [busy, setBusy] = useState<boolean>(false);
|
|
14
14
|
const account = useSelector((state: State) => state.auth.account);
|
|
15
|
-
const disabled = account === null;
|
|
15
|
+
const disabled = account === null && false;
|
|
16
16
|
const dispatch = useDispatch();
|
|
17
17
|
const t = useT();
|
|
18
18
|
const cloudSaves = useSelector((state: State) => state.ui.cloudSaves);
|
|
@@ -31,7 +31,7 @@ export function ImageRenderingButton(props: {
|
|
|
31
31
|
export function QuickSaveButton(props: {
|
|
32
32
|
class?: string,
|
|
33
33
|
}) {
|
|
34
|
-
const disabled = useSelector((state: State) => state.auth.account) === null;
|
|
34
|
+
const disabled = useSelector((state: State) => state.auth.account) === null && false;
|
|
35
35
|
const action = disabled ?
|
|
36
36
|
uiSlice.actions.modalLogin() :
|
|
37
37
|
uiSlice.actions.frameQuickSave();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LRUMap } from "./lru";
|
|
2
2
|
|
|
3
3
|
export interface Cache {
|
|
4
|
-
read(sector: number): Uint8Array | null;
|
|
4
|
+
read(sector: number, originReadMode: boolean): Uint8Array | null;
|
|
5
5
|
write(sector: number, buffer: Uint8Array): boolean;
|
|
6
6
|
create(origin: number, buffer: Uint8Array): void;
|
|
7
7
|
getOrigin(sector: number): number;
|
|
@@ -19,12 +19,16 @@ export class BlockCache implements Cache {
|
|
|
19
19
|
this.lru = new LRUMap(Math.floor(memoryLimit / (aheadRange * sectorSize)));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
public read(sector: number): Uint8Array | null {
|
|
22
|
+
public read(sector: number, originReadMode: boolean): Uint8Array | null {
|
|
23
23
|
const origin = this.getOrigin(sector);
|
|
24
24
|
const cached = this.lru.get(origin) as Uint8Array;
|
|
25
25
|
if (cached) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (originReadMode) {
|
|
27
|
+
return cached;
|
|
28
|
+
} else {
|
|
29
|
+
const offset = sector - origin;
|
|
30
|
+
return cached.slice(offset * this.sectorSize, (offset + 1) * this.sectorSize);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
return null;
|
|
30
34
|
}
|
|
@@ -50,84 +54,3 @@ export class BlockCache implements Cache {
|
|
|
50
54
|
return this.lru.size * this.aheadRange * this.sectorSize;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
export class SimpleCache implements Cache {
|
|
55
|
-
aheadRange: number;
|
|
56
|
-
sectorSize: number;
|
|
57
|
-
lru: LRUMap;
|
|
58
|
-
|
|
59
|
-
constructor(sectorSize: number, aheadRange: number, memoryLimit: number) {
|
|
60
|
-
this.aheadRange = aheadRange;
|
|
61
|
-
this.sectorSize = sectorSize;
|
|
62
|
-
this.lru = new LRUMap(Math.floor(memoryLimit / sectorSize));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public read(sector: number): Uint8Array | null {
|
|
66
|
-
const cached = this.lru.get(sector) as Uint8Array;
|
|
67
|
-
if (cached) {
|
|
68
|
-
return cached;
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public write(sector: number, buffer: Uint8Array) {
|
|
74
|
-
this.lru.set(sector, buffer);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
public create(sector: number, buffer: Uint8Array) {
|
|
79
|
-
for (let i = 0; i < this.aheadRange; ++i) {
|
|
80
|
-
this.lru.set(sector + i, buffer.slice(i * this.sectorSize, (i + 1) * this.sectorSize));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
public getOrigin(sector: number) {
|
|
85
|
-
return sector;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public memUsed() {
|
|
89
|
-
return this.lru.size * this.sectorSize;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class BlockAndWriteCache implements Cache {
|
|
94
|
-
aheadRange: number;
|
|
95
|
-
sectorSize: number;
|
|
96
|
-
blockCache: BlockCache;
|
|
97
|
-
writeCache: SimpleCache;
|
|
98
|
-
|
|
99
|
-
constructor(sectorSize: number, aheadRange: number, memoryLimit: number) {
|
|
100
|
-
this.aheadRange = aheadRange;
|
|
101
|
-
this.sectorSize = sectorSize;
|
|
102
|
-
this.blockCache = new BlockCache(sectorSize, aheadRange, memoryLimit);
|
|
103
|
-
this.writeCache = new SimpleCache(sectorSize, aheadRange, Math.floor(memoryLimit / 10));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
read(sector: number) {
|
|
107
|
-
return this.blockCache.read(sector) || this.writeCache.read(sector);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
write(sector: number, buffer: Uint8Array) {
|
|
111
|
-
if (!this.blockCache.write(sector, buffer)) {
|
|
112
|
-
this.writeCache.write(sector, buffer);
|
|
113
|
-
}
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
create(origin: number, buffer: Uint8Array): void {
|
|
118
|
-
this.blockCache.create(origin, buffer);
|
|
119
|
-
for (let i = 0; i < this.aheadRange; ++i) {
|
|
120
|
-
if (this.writeCache.read(origin + i) != null) {
|
|
121
|
-
this.writeCache.write(origin + i, buffer.slice(i * this.sectorSize, (i + 1) * this.sectorSize));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
getOrigin(sector: number): number {
|
|
127
|
-
return this.blockCache.getOrigin(sector);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
memUsed(): number {
|
|
131
|
-
return this.blockCache.memUsed() + this.writeCache.memUsed();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -39,7 +39,8 @@ export class Drive {
|
|
|
39
39
|
cache: Cache | null = null;
|
|
40
40
|
cleanup = () => {/**/};
|
|
41
41
|
|
|
42
|
-
openFn = (read: boolean, write: boolean, size: number, preloadQueue: number[]
|
|
42
|
+
openFn = (read: boolean, write: boolean, size: number, preloadQueue: number[],
|
|
43
|
+
aheadRange: number) => {/**/};
|
|
43
44
|
preloadProgressFn = (restBytes: number) => {/**/};
|
|
44
45
|
errorFn = (e: Error) => {/**/};
|
|
45
46
|
|
|
@@ -48,13 +49,16 @@ export class Drive {
|
|
|
48
49
|
preloadSectors: boolean;
|
|
49
50
|
preloadQueue: number[] = [];
|
|
50
51
|
|
|
52
|
+
originReadMode: boolean;
|
|
53
|
+
|
|
51
54
|
public constructor(endpoint: string,
|
|
52
55
|
owner: string,
|
|
53
56
|
drive: string,
|
|
54
57
|
token: string,
|
|
55
58
|
stats: Stats,
|
|
56
59
|
module: EmModule,
|
|
57
|
-
preloadSectors = true
|
|
60
|
+
preloadSectors = true,
|
|
61
|
+
originReadMode = false) {
|
|
58
62
|
this.sectorSize = 512;
|
|
59
63
|
this.module = module;
|
|
60
64
|
this.endpoint = endpoint;
|
|
@@ -67,6 +71,7 @@ export class Drive {
|
|
|
67
71
|
this.stats = stats;
|
|
68
72
|
this.retries = 3;
|
|
69
73
|
this.preloadSectors = preloadSectors;
|
|
74
|
+
this.originReadMode = originReadMode;
|
|
70
75
|
|
|
71
76
|
this.reconnect();
|
|
72
77
|
}
|
|
@@ -75,7 +80,8 @@ export class Drive {
|
|
|
75
80
|
this.errorFn = errorFn;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
public onOpen(openFn: (read: boolean, write: boolean, imageSize: number,
|
|
83
|
+
public onOpen(openFn: (read: boolean, write: boolean, imageSize: number,
|
|
84
|
+
preloadQueue: number[], aheadRange: number) => void) {
|
|
79
85
|
this.openFn = openFn;
|
|
80
86
|
}
|
|
81
87
|
|
|
@@ -136,7 +142,8 @@ export class Drive {
|
|
|
136
142
|
socket.addEventListener("message", onMessage);
|
|
137
143
|
this.openFn(true, mode === "write",
|
|
138
144
|
(Number.parseInt(sizeStr) ?? 2 * 1024 * 1024) * 1024,
|
|
139
|
-
this.preloadQueue
|
|
145
|
+
this.preloadQueue,
|
|
146
|
+
aheadRange);
|
|
140
147
|
resolve(socket);
|
|
141
148
|
|
|
142
149
|
if (this.preloadQueue.length > 0) {
|
|
@@ -216,11 +223,9 @@ export class Drive {
|
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
public read(sector: number, buffer: Ptr, sync: boolean): Promise<number> | number {
|
|
219
|
-
const cached = this.cache!.read(sector);
|
|
226
|
+
const cached = this.cache!.read(sector, this.originReadMode);
|
|
220
227
|
if (cached !== null) {
|
|
221
|
-
|
|
222
|
-
this.stats.cacheHit++;
|
|
223
|
-
}
|
|
228
|
+
this.stats.cacheHit++;
|
|
224
229
|
this.module.HEAPU8.set(cached, buffer);
|
|
225
230
|
return sync ? 0 : Promise.resolve(0);
|
|
226
231
|
} else if (sync) {
|
|
@@ -361,10 +366,15 @@ export class Drive {
|
|
|
361
366
|
this.cache!.create(origin,
|
|
362
367
|
this.readAheadBuffer.slice(aheadOffset, aheadOffset + this.aheadSize));
|
|
363
368
|
if (i == 0 && buffer as number >= 0) {
|
|
369
|
+
if (this.originReadMode && sector !== origin) {
|
|
370
|
+
throw new Error("Sector must be one of origins in originReadMode");
|
|
371
|
+
}
|
|
364
372
|
const offset = sector - origin;
|
|
365
373
|
// aheadOffset is zero
|
|
366
|
-
|
|
367
|
-
|
|
374
|
+
const from = offset * this.sectorSize;
|
|
375
|
+
this.module.HEAPU8.set(this.readAheadBuffer
|
|
376
|
+
.slice(from, from + (this.originReadMode ? this.aheadRange : 1) * this.sectorSize),
|
|
377
|
+
buffer as number);
|
|
368
378
|
}
|
|
369
379
|
}
|
|
370
380
|
|
|
@@ -182,14 +182,17 @@ async function runTests() {
|
|
|
182
182
|
assert.equal(await drive.read(0, 0, false), 0);
|
|
183
183
|
assert.deepEqual(module.HEAPU8.slice(0, 10), new Uint8Array([51, 192, 142, 208, 188, 0, 124, 251, 80, 7]));
|
|
184
184
|
assert.equal(1, stats.cacheMiss, "cache miss");
|
|
185
|
+
assert.equal(0, stats.cacheHit, "cache hit");
|
|
185
186
|
|
|
186
187
|
assert.equal(await drive.read(8192, 0, false), 0);
|
|
187
188
|
assert.deepEqual(module.HEAPU8.slice(0, 10), new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
|
|
188
189
|
assert.equal(2, stats.cacheMiss, "cache miss");
|
|
190
|
+
assert.equal(0, stats.cacheHit, "cache hit");
|
|
189
191
|
|
|
190
192
|
assert.equal(await drive.read(8448, 0, false), 0);
|
|
191
193
|
assert.deepEqual(module.HEAPU8.slice(256, 256 + 10), new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]));
|
|
192
|
-
assert.equal(
|
|
194
|
+
assert.equal(2, stats.cacheMiss, "cache miss");
|
|
195
|
+
assert.equal(1, stats.cacheHit, "cache hit");
|
|
193
196
|
});
|
|
194
197
|
|
|
195
198
|
testDrive("read cache", async (drive, module, stats) => {
|
|
@@ -213,16 +216,14 @@ async function runTests() {
|
|
|
213
216
|
});
|
|
214
217
|
|
|
215
218
|
testDrive("preload sectors", async (drive, module, stats, preloadQueue) => {
|
|
216
|
-
assert.ok(preloadQueue.length >=
|
|
219
|
+
assert.ok(preloadQueue.length >= 2);
|
|
217
220
|
let _0 = false;
|
|
218
|
-
let _8448 = false;
|
|
219
221
|
let _8192 = false;
|
|
220
222
|
for (const next of preloadQueue) {
|
|
221
223
|
_0 = _0 || next === 0;
|
|
222
224
|
_8192 = _8192 || next === 8192;
|
|
223
|
-
_8448 = _8448 || next === 8448;
|
|
224
225
|
}
|
|
225
|
-
assert.ok(_0 && _8192
|
|
226
|
+
assert.ok(_0 && _8192);
|
|
226
227
|
|
|
227
228
|
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
|
228
229
|
|
package/src/store/init.ts
CHANGED
|
@@ -10,8 +10,8 @@ export const sockdriveBackend: {
|
|
|
10
10
|
}
|
|
11
11
|
} = {
|
|
12
12
|
"js-dos": {
|
|
13
|
-
sockdriveEndpoint: "https://
|
|
14
|
-
sockdriveWssEndpoint: "wss://
|
|
13
|
+
sockdriveEndpoint: "https://sockdrive.js-dos.com:8001",
|
|
14
|
+
sockdriveWssEndpoint: "wss://sockdrive.js-dos.com:8001",
|
|
15
15
|
},
|
|
16
16
|
"localhost": {
|
|
17
17
|
sockdriveEndpoint: "http://localhost:8001",
|
package/src/store/ui.ts
CHANGED
|
@@ -27,6 +27,7 @@ const initialState: {
|
|
|
27
27
|
toastTimeoutId: number,
|
|
28
28
|
background: string | null,
|
|
29
29
|
readOnlyWarning: boolean,
|
|
30
|
+
updateWsWarning: boolean,
|
|
30
31
|
cloudSaves: boolean,
|
|
31
32
|
} = {
|
|
32
33
|
modal: "none",
|
|
@@ -42,6 +43,7 @@ const initialState: {
|
|
|
42
43
|
toastTimeoutId: 0,
|
|
43
44
|
background: null,
|
|
44
45
|
readOnlyWarning: false,
|
|
46
|
+
updateWsWarning: false,
|
|
45
47
|
cloudSaves: true,
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -136,6 +138,9 @@ export const uiSlice = createSlice({
|
|
|
136
138
|
readOnlyWarning: (state, a: { payload: boolean }) => {
|
|
137
139
|
state.readOnlyWarning = a.payload;
|
|
138
140
|
},
|
|
141
|
+
updateWsWarning: (state, a: { payload: boolean }) => {
|
|
142
|
+
state.updateWsWarning = a.payload;
|
|
143
|
+
},
|
|
139
144
|
cloudSaves: (state, a: { payload: boolean }) => {
|
|
140
145
|
state.cloudSaves = a.payload;
|
|
141
146
|
},
|
package/src/store.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { DosEvent, DosOptions } from "./public/types";
|
|
|
9
9
|
import { CommandInterface, InitFs } from "emulators";
|
|
10
10
|
import { useStore } from "react-redux";
|
|
11
11
|
import { Cache, CacheNoop } from "./host/lcache";
|
|
12
|
-
import { InitState, createInitSlice } from "./store/init";
|
|
12
|
+
import { InitState, createInitSlice, initSlice, sockdriveBackend, sockdriveBackendNames } from "./store/init";
|
|
13
13
|
|
|
14
14
|
export interface LoadedBundle {
|
|
15
15
|
bundleUrl: string | null,
|
|
@@ -53,7 +53,7 @@ export function makeNonSerializableStore(options: Partial<DosOptions>): NonSeria
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function makeStore(nonSerializableStore: NonSerializableStore) {
|
|
56
|
+
export function makeStore(nonSerializableStore: NonSerializableStore, options: Partial<DosOptions>) {
|
|
57
57
|
const { storeUid, slice } = createInitSlice();
|
|
58
58
|
const store = configureStore({
|
|
59
59
|
reducer: {
|
|
@@ -72,6 +72,14 @@ export function makeStore(nonSerializableStore: NonSerializableStore) {
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
nonSerializableStoreMap[storeUid] = nonSerializableStore;
|
|
75
|
+
if (options.sockdriveBackend && sockdriveBackendNames.indexOf(options.sockdriveBackend.name) === -1) {
|
|
76
|
+
sockdriveBackendNames.push(options.sockdriveBackend.name);
|
|
77
|
+
sockdriveBackend[options.sockdriveBackend.name] = {
|
|
78
|
+
sockdriveEndpoint: "https://" + options.sockdriveBackend.host,
|
|
79
|
+
sockdriveWssEndpoint: "wss://" + options.sockdriveBackend.host,
|
|
80
|
+
};
|
|
81
|
+
store.dispatch(initSlice.actions.setSockdriveBackendName(options.sockdriveBackend.name));
|
|
82
|
+
}
|
|
75
83
|
return store;
|
|
76
84
|
};
|
|
77
85
|
|
package/src/ui.tsx
CHANGED
|
@@ -49,6 +49,7 @@ export function Ui() {
|
|
|
49
49
|
<Login />
|
|
50
50
|
<Toast />
|
|
51
51
|
<ReadOnlyWarning />
|
|
52
|
+
<UpdateWsWarning />
|
|
52
53
|
</div>;
|
|
53
54
|
};
|
|
54
55
|
|
|
@@ -102,3 +103,38 @@ function ReadOnlyWarning() {
|
|
|
102
103
|
</div>
|
|
103
104
|
</div>;
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
function UpdateWsWarning() {
|
|
108
|
+
const updateWsWarning = useSelector((state: State) => state.ui.updateWsWarning);
|
|
109
|
+
const t = useT();
|
|
110
|
+
const dispatch = useDispatch();
|
|
111
|
+
|
|
112
|
+
if (!updateWsWarning) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fix() {
|
|
117
|
+
window.open("https://dos.zone/download/", "_blank");
|
|
118
|
+
dispatch(uiSlice.actions.updateWsWarning(false));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function close() {
|
|
122
|
+
dispatch(uiSlice.actions.updateWsWarning(false));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return <div class="absolute left-20 right-10 top-10 flex justify-center">
|
|
126
|
+
<div class="alert w-auto">
|
|
127
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
128
|
+
stroke-width="1.5" stroke="currentColor" class="size-6 text-error">
|
|
129
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217
|
|
130
|
+
3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898
|
|
131
|
+
0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
|
132
|
+
</svg>
|
|
133
|
+
<span>{t("ws_outdated")}</span>
|
|
134
|
+
<div>
|
|
135
|
+
<button class="btn btn-sm btn-primary mr-2" onClick={fix}>{t("update")}</button>
|
|
136
|
+
<button class="btn btn-sm" onClick={close}>{t("close")}</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>;
|
|
140
|
+
}
|
package/src/v8/config.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { State, useNonSerializableStore, postJsDosEvent } from "../../store";
|
|
|
6
6
|
import { useDosRuntime } from "./dos-runtime";
|
|
7
7
|
import { dhry2Bundle, Dhry2Results } from "./dos-dhry2";
|
|
8
8
|
import { createWsTransportLayer } from "../../ws/ws-transport-layer";
|
|
9
|
+
import { actualWsVersion } from "../../v8/config";
|
|
10
|
+
import { uiSlice } from "../../store/ui";
|
|
9
11
|
|
|
10
12
|
declare const emulators: Emulators;
|
|
11
13
|
|
|
@@ -35,7 +37,12 @@ export function DosWindow(props: {
|
|
|
35
37
|
if (backendHardware && nonSerializableStore.options.backendHardware) {
|
|
36
38
|
const ws = await nonSerializableStore.options.backendHardware(backend);
|
|
37
39
|
if (ws !== null) {
|
|
38
|
-
return emulators.backend(bundles as any, await createWsTransportLayer(ws
|
|
40
|
+
return emulators.backend(bundles as any, await createWsTransportLayer(ws, (version) => {
|
|
41
|
+
if (version < actualWsVersion) {
|
|
42
|
+
dispatch(uiSlice.actions.updateWsWarning(true));
|
|
43
|
+
}
|
|
44
|
+
console.log("wsServer:", version, " expected:", actualWsVersion);
|
|
45
|
+
}), { token });
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
|
package/src/ws/ws-sockdrive.ts
CHANGED
|
@@ -16,8 +16,10 @@ export interface ReadResponse {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function createSockdrive(
|
|
19
|
-
onOpen: (drive: string, read: boolean, write: boolean) => void,
|
|
20
|
-
onError: (e: Error) => void
|
|
19
|
+
onOpen: (drive: string, read: boolean, write: boolean, imageSize: number, preloadQueue: number[]) => void,
|
|
20
|
+
onError: (e: Error) => void,
|
|
21
|
+
onPreloadProgress: (drive: string, restBytes: number) => void,
|
|
22
|
+
) {
|
|
21
23
|
let seq = 0;
|
|
22
24
|
const mapping: { [handle: Handle]: Drive } = {};
|
|
23
25
|
const templates: { [handle: number]: Template } = {};
|
|
@@ -32,7 +34,10 @@ export function createSockdrive(
|
|
|
32
34
|
};
|
|
33
35
|
const sockdrive = {
|
|
34
36
|
stats,
|
|
35
|
-
open: async (url: string, owner: string, name: string, token: string
|
|
37
|
+
open: async (url: string, owner: string, name: string, token: string): Promise<{
|
|
38
|
+
handle: Handle,
|
|
39
|
+
aheadRange: number,
|
|
40
|
+
}> => {
|
|
36
41
|
const response = await fetch(url.replace("wss://", "https://")
|
|
37
42
|
.replace("ws://", "http://") + "/template/" + owner + "/" + name);
|
|
38
43
|
const template = await response.json();
|
|
@@ -50,22 +55,39 @@ export function createSockdrive(
|
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
const sectorSize = templates[seq].sectorSize;
|
|
53
|
-
|
|
54
|
-
mapping[seq] = new Drive(url, owner, name, token, stats,
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const module = { HEAPU8: new Uint8Array(0) };
|
|
59
|
+
mapping[seq] = new Drive(url, owner, name, token, stats, module, true, true);
|
|
60
|
+
return new Promise<{ handle: Handle, aheadRange: number }>((resolve, reject) => {
|
|
61
|
+
const drive = owner + "/" + name;
|
|
62
|
+
mapping[seq].onOpen((read, write, imageSize, preloadQueue, aheadRange) => {
|
|
63
|
+
memory[seq] = new Uint8Array(sectorSize /* write */ + sectorSize * aheadRange);
|
|
64
|
+
module.HEAPU8 = memory[seq];
|
|
65
|
+
onOpen(drive, read, write, imageSize, preloadQueue);
|
|
66
|
+
resolve({
|
|
67
|
+
handle: seq,
|
|
68
|
+
aheadRange,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
mapping[seq].onPreloadProgress((restBytes) => {
|
|
72
|
+
onPreloadProgress(drive, restBytes);
|
|
73
|
+
});
|
|
74
|
+
mapping[seq].onError((e) => {
|
|
75
|
+
onError(e);
|
|
76
|
+
reject(e);
|
|
77
|
+
});
|
|
57
78
|
});
|
|
58
|
-
mapping[seq].onError(onError);
|
|
59
|
-
return seq;
|
|
60
79
|
},
|
|
61
|
-
read: (handle: Handle, sector: number): Promise<ReadResponse> => {
|
|
80
|
+
read: async (handle: Handle, sector: number): Promise<ReadResponse> => {
|
|
62
81
|
if (mapping[handle]) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
const ptr = templates[seq].sectorSize;
|
|
83
|
+
let code = mapping[handle].read(sector, ptr, true) as number;
|
|
84
|
+
if (code = 255) {
|
|
85
|
+
code = await mapping[handle].read(sector, ptr, false);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
code,
|
|
89
|
+
buffer: memory[handle].slice(ptr),
|
|
90
|
+
};
|
|
69
91
|
}
|
|
70
92
|
|
|
71
93
|
console.error("ERROR! sockdrive handle", handle, "not found");
|
|
@@ -48,9 +48,11 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
48
48
|
socket: WebSocket;
|
|
49
49
|
sessionId: string = Date.now() + "";
|
|
50
50
|
hardware: Hardware;
|
|
51
|
+
onInit: (version: number) => void = {/**/};
|
|
51
52
|
|
|
52
53
|
private cycles = 0;
|
|
53
|
-
private sockdrive = createSockdrive(this.onSockdriveOpen.bind(this),
|
|
54
|
+
private sockdrive = createSockdrive(this.onSockdriveOpen.bind(this),
|
|
55
|
+
this.onSockdriveError.bind(this), this.onSockdrivePreloadProgress.bind(this));
|
|
54
56
|
|
|
55
57
|
private handler: MessageHandler = () => {/**/};
|
|
56
58
|
|
|
@@ -120,15 +122,17 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
120
122
|
const { id, payload } = this.readMessage(msg);
|
|
121
123
|
const message = serverMessageEnum[id];
|
|
122
124
|
switch (message) {
|
|
123
|
-
case "ws-ready":
|
|
125
|
+
case "ws-ready": {
|
|
126
|
+
this.onInit((payload && payload[0] && payload.length > 0) ? payload[0][0] : 0);
|
|
127
|
+
this.handler(message, {});
|
|
128
|
+
} break;
|
|
124
129
|
case "ws-server-ready":
|
|
125
|
-
case "ws-exit":
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} break;
|
|
130
|
+
case "ws-exit": {
|
|
131
|
+
// delay ws-server-ready until ws-sound-init
|
|
132
|
+
if (message !== "ws-server-ready") {
|
|
133
|
+
this.handler(message, {});
|
|
134
|
+
}
|
|
135
|
+
} break;
|
|
132
136
|
case "ws-stdout":
|
|
133
137
|
case "ws-log":
|
|
134
138
|
case "ws-warn":
|
|
@@ -267,17 +271,17 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
267
271
|
const owner = textDecoder.decode(payload[1]!);
|
|
268
272
|
const name = textDecoder.decode(payload[2]!);
|
|
269
273
|
const token = textDecoder.decode(payload[3]!);
|
|
270
|
-
const aheadRange = this.
|
|
271
|
-
const handle = await this.sockdrive.open(url, owner, name, token, aheadRange);
|
|
274
|
+
const { handle, aheadRange } = await this.sockdrive.open(url, owner, name, token);
|
|
272
275
|
const template = this.sockdrive.template(handle);
|
|
273
|
-
const packet = new Uint8Array(4 *
|
|
276
|
+
const packet = new Uint8Array(4 * 7);
|
|
274
277
|
let offset = 0;
|
|
275
278
|
offset = this.writeUint32(packet, handle, offset);
|
|
276
279
|
offset = this.writeUint32(packet, template.size, offset);
|
|
277
280
|
offset = this.writeUint32(packet, template.heads, offset);
|
|
278
281
|
offset = this.writeUint32(packet, template.cylinders, offset);
|
|
279
282
|
offset = this.writeUint32(packet, template.sectors, offset);
|
|
280
|
-
this.writeUint32(packet, template.sectorSize, offset);
|
|
283
|
+
offset = this.writeUint32(packet, template.sectorSize, offset);
|
|
284
|
+
this.writeUint32(packet, aheadRange, offset);
|
|
281
285
|
this.sendMessageToSocket(100, packet);
|
|
282
286
|
} break;
|
|
283
287
|
case 101/* ws-sockdrive-read */: {
|
|
@@ -317,7 +321,7 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
317
321
|
}
|
|
318
322
|
}
|
|
319
323
|
|
|
320
|
-
constructor(socket: WebSocket) {
|
|
324
|
+
constructor(socket: WebSocket, onInit: (version: number) => void) {
|
|
321
325
|
this.socket = socket;
|
|
322
326
|
this.socket.addEventListener("error", (e) => {
|
|
323
327
|
this.handler("ws-err", { tag: "ws", message: (e as any).message ?? "Unknown transport layer error" });
|
|
@@ -330,6 +334,7 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
330
334
|
});
|
|
331
335
|
this.sendMessageToSocket("wc-install");
|
|
332
336
|
this.hardware = (null) as any;
|
|
337
|
+
this.onInit = onInit;
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
async sendMessageToServer(name: ClientMessage, props?: { [key: string]: any; }) {
|
|
@@ -411,10 +416,11 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
411
416
|
this.sendMessageToSocket("wc-exit");
|
|
412
417
|
}
|
|
413
418
|
|
|
414
|
-
onSockdriveOpen(drive: string, read: boolean, write: boolean) {
|
|
419
|
+
onSockdriveOpen(drive: string, read: boolean, write: boolean, imageSize: number, preloadQueue: number[]) {
|
|
415
420
|
this.handler("ws-log", {
|
|
416
421
|
tag: "worker",
|
|
417
|
-
message: "sockdrive: " + drive + ", read=" + read + ", write=" + write
|
|
422
|
+
message: "sockdrive: " + drive + ", read=" + read + ", write=" + write +
|
|
423
|
+
", imageSize=" + Math.round(imageSize / 1024 / 1024) + "Mb" + ", preloadQueue=" + preloadQueue.length,
|
|
418
424
|
});
|
|
419
425
|
}
|
|
420
426
|
|
|
@@ -424,9 +430,16 @@ export class WsTransportLayer implements TransportLayer {
|
|
|
424
430
|
message: e.message ?? "unable to open sockdrive",
|
|
425
431
|
});
|
|
426
432
|
}
|
|
433
|
+
|
|
434
|
+
onSockdrivePreloadProgress(drive: string, restBytes: number) {
|
|
435
|
+
this.handler("ws-log", {
|
|
436
|
+
tag: "worker",
|
|
437
|
+
message: "sockdrive: " + drive + ", preload=" + restBytes,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
427
440
|
}
|
|
428
441
|
|
|
429
|
-
export function createWsTransportLayer(url: string): Promise<TransportLayer> {
|
|
442
|
+
export function createWsTransportLayer(url: string, onInit: (version: number) => void): Promise<TransportLayer> {
|
|
430
443
|
return new Promise<TransportLayer>((resolve) => {
|
|
431
444
|
let locked = false;
|
|
432
445
|
const inervalId = setInterval(() => {
|
|
@@ -439,7 +452,7 @@ export function createWsTransportLayer(url: string): Promise<TransportLayer> {
|
|
|
439
452
|
const onSuccess = () => {
|
|
440
453
|
clearInterval(inervalId);
|
|
441
454
|
console.log("Connected to", url);
|
|
442
|
-
resolve(new WsTransportLayer(ws));
|
|
455
|
+
resolve(new WsTransportLayer(ws, onInit));
|
|
443
456
|
};
|
|
444
457
|
|
|
445
458
|
ws.addEventListener("error", (error) => {
|