Skip to content

Commit 6623daa

Browse files
committed
More sync engine tests + fixes
1 parent d2e24eb commit 6623daa

File tree

10 files changed

+952
-315
lines changed

10 files changed

+952
-315
lines changed

client/codemirror/task.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { syntaxTree } from "@codemirror/language";
2-
import { startCompletion, completionStatus } from "@codemirror/autocomplete";
2+
import { startCompletion } from "@codemirror/autocomplete";
33
import { Decoration, type EditorView, WidgetType } from "@codemirror/view";
44
import type { NodeType } from "@lezer/common";
55
import { decoratorStateField, isCursorInRange } from "./util.ts";

client/plugos/syscalls/sync.ts

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,63 @@ import type { SysCallMapping } from "../system.ts";
22
import type { Client } from "../../client.ts";
33

44
export function syncSyscalls(client: Client): SysCallMapping {
5+
const syncTimeoutMs = 30000;
6+
7+
function waitForServiceWorkerActivation(path?: string): Promise<any> {
8+
return new Promise<any>((resolve, reject) => {
9+
const timeout = setTimeout(() => {
10+
cleanup();
11+
reject(new Error(`Sync timeout after ${syncTimeoutMs / 1000}s`));
12+
}, syncTimeoutMs);
13+
14+
function cleanup() {
15+
clearTimeout(timeout);
16+
client.eventHook.removeLocalListener(
17+
"service-worker:file-sync-complete",
18+
eventHandler,
19+
);
20+
client.eventHook.removeLocalListener(
21+
"service-worker:space-sync-complete",
22+
eventHandler,
23+
);
24+
client.eventHook.removeLocalListener(
25+
"service-worker:sync-error",
26+
errorHandler,
27+
);
28+
}
29+
30+
client.eventHook.addLocalListener(
31+
"service-worker:file-sync-complete",
32+
eventHandler,
33+
);
34+
client.eventHook.addLocalListener(
35+
"service-worker:space-sync-complete",
36+
eventHandler,
37+
);
38+
client.eventHook.addLocalListener(
39+
"service-worker:sync-error",
40+
errorHandler,
41+
);
42+
43+
function eventHandler(data: any) {
44+
if (data.path && path && data.path !== path) {
45+
return;
46+
}
47+
cleanup();
48+
resolve(data);
49+
}
50+
51+
function errorHandler(e: any) {
52+
// Only reject if the error is for our specific path, or if no path was specified
53+
if (e.path && path && e.path !== path) {
54+
return;
55+
}
56+
cleanup();
57+
reject(e);
58+
}
59+
});
60+
}
61+
562
return {
663
"sync.hasInitialSyncCompleted": (): boolean => {
764
return client.fullSyncCompleted;
@@ -14,61 +71,16 @@ export function syncSyscalls(client: Client): SysCallMapping {
1471
// postServiceWorkerMessage returns silently if no SW, so only wait if SW is active
1572
const registration = await navigator.serviceWorker.getRegistration();
1673
if (registration?.active) {
17-
return waitForServiceWorkerActivation(client, path);
74+
return waitForServiceWorkerActivation(path);
1875
}
1976
},
2077
"sync.performSpaceSync": async (): Promise<number> => {
2178
await client.postServiceWorkerMessage({ type: "perform-space-sync" });
2279
const registration = await navigator.serviceWorker.getRegistration();
2380
if (registration?.active) {
24-
return waitForServiceWorkerActivation(client);
81+
return waitForServiceWorkerActivation();
2582
}
2683
return 0;
2784
},
2885
};
2986
}
30-
31-
function waitForServiceWorkerActivation(
32-
client: Client,
33-
path?: string,
34-
): Promise<any> {
35-
return new Promise<any>((resolve, reject) => {
36-
client.eventHook.addLocalListener(
37-
"service-worker:file-sync-complete",
38-
eventHandler,
39-
);
40-
client.eventHook.addLocalListener(
41-
"service-worker:space-sync-complete",
42-
eventHandler,
43-
);
44-
client.eventHook.addLocalListener(
45-
"service-worker:sync-error",
46-
errorHandler,
47-
);
48-
function eventHandler(data: any) {
49-
if (data.path && path && data.path !== path) {
50-
return;
51-
}
52-
resolve(data);
53-
cleanup();
54-
}
55-
function errorHandler(e: any) {
56-
reject(e);
57-
cleanup();
58-
}
59-
function cleanup() {
60-
client.eventHook.removeLocalListener(
61-
"service-worker:file-sync-complete",
62-
eventHandler,
63-
);
64-
client.eventHook.removeLocalListener(
65-
"service-worker:space-sync-complete",
66-
eventHandler,
67-
);
68-
client.eventHook.removeLocalListener(
69-
"service-worker:sync-error",
70-
errorHandler,
71-
);
72-
}
73-
});
74-
}

client/service_worker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,11 @@ self.addEventListener("message", async (event: any) => {
288288
operations,
289289
});
290290
},
291-
syncError: (error) => {
291+
syncError: (error, path) => {
292292
broadcastMessage({
293293
type: "sync-error",
294294
message: error.message,
295+
path,
295296
});
296297
},
297298
});

client/service_worker/proxy_router.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,9 @@ export class ProxyRouter extends EventEmitter<ProxyRouterEvents> {
233233

234234
const files = await this.localSpacePrimitives.fetchFileList();
235235
// Now augment this with non-synced file metadata
236+
const localFileNames = new Set(files.map((f) => f.name));
236237
for (const nonSyncedFile of this.nonSyncedFiles.values()) {
237-
const existingFile = files.find(
238-
(file) => file.name === nonSyncedFile.name,
239-
);
240-
if (!existingFile) {
238+
if (!localFileNames.has(nonSyncedFile.name)) {
241239
files.push(nonSyncedFile);
242240
}
243241
}

client/service_worker/sync_engine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type SyncEngineEvents = {
1717
// A single file syncle has completed
1818
fileSyncComplete: (path: string, operations: number) => void | Promise<void>;
1919

20-
syncError: (error: Error) => void | Promise<void>;
20+
syncError: (error: Error, path?: string) => void | Promise<void>;
2121

2222
// Sync conflict occurred
2323
syncConflict: (path: string) => void | Promise<void>;
@@ -139,7 +139,7 @@ export class SyncEngine extends EventEmitter<SyncEngineEvents> {
139139
void this.emit("fileSyncComplete", path, operations);
140140
return operations;
141141
} catch (e) {
142-
void this.emit("syncError", e);
142+
void this.emit("syncError", e, path);
143143
throw e;
144144
}
145145
}

client/spaces/checked_space_primitives.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ export class CheckedSpacePrimitives implements SpacePrimitives {
2121

2222
readFile(path: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
2323
if (!this.isReadable(path)) {
24-
throw new Error("Couldn't write file, path isn't writable");
24+
throw new Error("Couldn't read file, path isn't readable");
2525
}
2626
return this.wrapped.readFile(path);
2727
}
2828

2929
getFileMeta(path: string, observing?: boolean): Promise<FileMeta> {
3030
if (!this.isReadable(path)) {
31-
throw new Error("Couldn't get file meta, path isn't writable");
31+
throw new Error("Couldn't get file meta, path isn't readable");
3232
}
3333
return this.wrapped.getFileMeta(path, observing);
3434
}

client/spaces/evented_space_primitives.ts

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import type { DataStore } from "../data/datastore.ts";
1414
* - page:deleted (string)
1515
*/
1616
export class EventedSpacePrimitives implements SpacePrimitives {
17-
// Various operations may be going on at the same time, and we don't want to trigger events unnessarily.
18-
// Therefore, we use this variable to track if any operation is in flight, and if so, we skip event triggering.
19-
// This is ok, because any event will be picked up in a following iteration.
20-
operationInProgress = false;
17+
// Various operations may be going on at the same time, and we don't want to trigger events unnecessarily.
18+
// Therefore, we use this counter to track how many operations are in flight, and if so, we skip event triggering.
19+
private operationCount = 0;
20+
21+
// When a fetchFileList is requested while operations are in flight, we defer it
22+
// so that synced changes are not missed.
23+
private deferredFetchFileList = false;
2124

2225
private enabled = false;
2326

@@ -65,6 +68,20 @@ export class EventedSpacePrimitives implements SpacePrimitives {
6568
}
6669
}
6770

71+
/**
72+
* Called when an operation completes. If a fetchFileList was deferred
73+
* because operations were in flight, trigger it now.
74+
*/
75+
private checkDeferredFetchFileList() {
76+
if (this.deferredFetchFileList && this.operationCount === 0) {
77+
this.deferredFetchFileList = false;
78+
// Schedule on next tick to avoid reentrancy
79+
setTimeout(() => {
80+
void this.fetchFileList();
81+
});
82+
}
83+
}
84+
6885
dispatchEvent(name: string, ...args: any[]): Promise<any[]> {
6986
if (!this.enabled) {
7087
return Promise.resolve([]);
@@ -74,27 +91,22 @@ export class EventedSpacePrimitives implements SpacePrimitives {
7491
}
7592

7693
async fetchFileList(): Promise<FileMeta[]> {
77-
if (this.operationInProgress) {
94+
if (this.operationCount > 0) {
7895
// Some other operation (read, write, list, meta) is already going on
7996
// this will likely trigger events, so let's not worry about any of that and avoid race condition and inconsistent data.
97+
// We mark a deferred flag so the next operation completion will trigger a fetchFileList.
8098
console.info(
81-
"operationInProgress, deferring event processing for fetchFileList.",
99+
"deferredFetchFileList: skipping event triggering for fetchFileList.",
82100
);
83-
const result = await this.wrapped.fetchFileList();
84-
// Schedule a retry after current operation completes
85-
setTimeout(() => {
86-
if (!this.operationInProgress) {
87-
void this.fetchFileList();
88-
}
89-
}, 50);
90-
return result;
101+
this.deferredFetchFileList = true;
102+
return this.wrapped.fetchFileList();
91103
}
92104
if (!this.enabled) {
93105
return this.wrapped.fetchFileList();
94106
}
95107
// console.log("Fetching file list");
96108
// Fetching mutex
97-
this.operationInProgress = true;
109+
this.operationCount++;
98110
try {
99111
// Fetch the list
100112
const newFileList = await this.wrapped.fetchFileList();
@@ -140,27 +152,25 @@ export class EventedSpacePrimitives implements SpacePrimitives {
140152
return newFileList;
141153
} finally {
142154
await this.saveSnapshot();
143-
this.operationInProgress = false;
155+
this.operationCount--;
144156
}
145157
}
146158

147159
async readFile(path: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
148160
if (!this.enabled) {
149161
return this.wrapped.readFile(path);
150162
}
163+
this.operationCount++;
151164
try {
152-
// Fetching mutex
153-
const wasFetching = this.operationInProgress;
154-
this.operationInProgress = true;
155-
156165
// Fetch file
157166
const data = await this.wrapped.readFile(path);
158-
if (!wasFetching) {
167+
if (this.operationCount === 1) {
159168
await this.triggerEventsAndCache(path, data.meta.lastModified);
160169
}
161170
return data;
162171
} finally {
163-
this.operationInProgress = false;
172+
this.operationCount--;
173+
this.checkDeferredFetchFileList();
164174
}
165175
}
166176

@@ -173,11 +183,10 @@ export class EventedSpacePrimitives implements SpacePrimitives {
173183
return this.wrapped.writeFile(path, data, meta);
174184
}
175185

176-
const wasFetching = this.operationInProgress;
177-
this.operationInProgress = true;
186+
this.operationCount++;
178187
try {
179188
const newMeta = await this.wrapped.writeFile(path, data, meta);
180-
if (!wasFetching) {
189+
if (this.operationCount === 1) {
181190
await this.triggerEventsAndCache(path, newMeta.lastModified);
182191
}
183192
if (path.endsWith(".md")) {
@@ -187,7 +196,8 @@ export class EventedSpacePrimitives implements SpacePrimitives {
187196

188197
return newMeta;
189198
} finally {
190-
this.operationInProgress = false;
199+
this.operationCount--;
200+
this.checkDeferredFetchFileList();
191201
}
192202
}
193203

@@ -212,16 +222,16 @@ export class EventedSpacePrimitives implements SpacePrimitives {
212222
return this.wrapped.getFileMeta(path, observing);
213223
}
214224

225+
this.operationCount++;
215226
try {
216-
const wasFetching = this.operationInProgress;
217-
this.operationInProgress = true;
218227
const newMeta = await this.wrapped.getFileMeta(path, observing);
219-
if (!wasFetching) {
228+
if (this.operationCount === 1) {
220229
await this.triggerEventsAndCache(path, newMeta.lastModified);
221230
}
222231
return newMeta;
223232
} finally {
224-
this.operationInProgress = false;
233+
this.operationCount--;
234+
this.checkDeferredFetchFileList();
225235
}
226236
}
227237

@@ -230,19 +240,19 @@ export class EventedSpacePrimitives implements SpacePrimitives {
230240
return this.wrapped.deleteFile(path);
231241
}
232242

243+
this.operationCount++;
233244
try {
234-
this.operationInProgress = true;
235245
if (path.endsWith(".md")) {
236246
const pageName = path.substring(0, path.length - 3);
237247
await this.dispatchEvent("page:deleted", pageName);
238248
}
239-
// await this.getPageMeta(path); // Check if page exists, if not throws Error
240249
await this.wrapped.deleteFile(path);
241250
this.deleteFromSnapshot(path);
242251
await this.dispatchEvent("file:deleted", path);
243252
} finally {
244253
await this.saveSnapshot();
245-
this.operationInProgress = false;
254+
this.operationCount--;
255+
this.checkDeferredFetchFileList();
246256
}
247257
}
248258
}

0 commit comments

Comments
 (0)