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 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
- [![Watch the video](https://github.com/caiiiycuk/js-dos/assets/1727152/a2b481cb-43b1-44aa-8b71-2181d351bb1a)](https://youtu.be/lhFrAe5YrJE)
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
+ [![Watch the video](https://github.com/caiiiycuk/js-dos/assets/1727152/a2b481cb-43b1-44aa-8b71-2181d351bb1a)](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://backend.make-vm.com:8001 dos.zone diablo_109_c
100
- imgmount 3 sockdrive wss://backend.make-vm.com:8001 dos.zone diablo_109_d
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-dos",
3
- "version": "8.1.0",
3
+ "version": "8.1.2",
4
4
  "description": "Full-featured DOS player with multiple emulator backends",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -237,7 +237,7 @@ scaler=none
237
237
 
238
238
  [autoexec]
239
239
  echo off
240
- imgmount 2 sockdrive {wss-makevm} dos.zone dos7.1
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} dos.zone win95
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} dos.zone win98
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
- const newName = prompt("Enter drive name");
80
- if (newName && newName.length > 0) {
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
- // eslint-disable-next-line max-len
104
- fetch(`${sockdriveEndpoint}/fork/delete/${owner}/${name}/${account.token.access_token}`, { method: "POST" })
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
- </div>;
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={1} bgcolor="bg-primary" textcolor="text-primary-content" />
13
- <LoadButton label={1} bgcolor="bg-primary" />
14
- <SaveButton disabled={!premium} label={2} bgcolor="bg-secondary" textcolor="text-secondary-content" />
15
- <LoadButton disabled={!premium} label={2} bgcolor="bg-secondary" />
16
- <SaveButton disabled={!premium} label={3} bgcolor="bg-accent" textcolor="text-accent-content" />
17
- <LoadButton disabled={!premium} label={3} bgcolor="bg-accent" />
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" ||
@@ -20,6 +20,10 @@ export interface DosOptions {
20
20
  server: "netherlands" | "newyork" | "singapore",
21
21
  room: string,
22
22
  fullScreen: boolean,
23
+ sockdriveBackend: {
24
+ name: string,
25
+ host: string,
26
+ }
23
27
  }
24
28
 
25
29
  export interface DosProps {
@@ -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
- const offset = sector - origin;
27
- return cached.slice(offset * this.sectorSize, (offset + 1) * this.sectorSize);
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, preloadQueue: number[]) => void) {
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
- if (sync) {
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
- this.module.HEAPU8.set(this.readAheadBuffer.slice(offset * this.sectorSize,
367
- (offset + 1) * this.sectorSize), buffer as number);
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(3, stats.cacheMiss, "cache miss");
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 >= 3);
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 && _8448);
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://backend.make-vm.com:8001",
14
- sockdriveWssEndpoint: "wss://backend.make-vm.com:8001",
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
@@ -16,3 +16,5 @@ export const cancelSubscriptionPage = {
16
16
  en: "https://v8.js-dos.com/cancel-your-subscription/",
17
17
  ru: "https://v8.js-dos.com/ru/cancel-your-subscription/",
18
18
  };
19
+
20
+ export const actualWsVersion = 2;
@@ -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), { token });
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
 
@@ -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, aheadRange: number): Promise<Handle> => {
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
- memory[seq] = new Uint8Array(sectorSize + sectorSize * aheadRange);
54
- mapping[seq] = new Drive(url, owner, name, token, stats, { HEAPU8: memory[seq] });
55
- mapping[seq].onOpen((read, write) => {
56
- onOpen(owner + "/" + name, read, write);
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
- return (mapping[handle].read(sector, 0, false) as Promise<number>).then((code) => {
64
- return {
65
- code,
66
- buffer: memory[handle].slice(templates[seq].sectorSize),
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), this.onSockdriveError.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
- // delay ws-server-ready until ws-sound-init
128
- if (message !== "ws-server-ready") {
129
- this.handler(message, {});
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.readUint32(payload[4]!, 0);
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 * 6);
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) => {