CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/backend/watcher.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Watch one SINGLE FILE for changes. Use ./path-watcher.ts for a directory.
8
9
Watch for changes to the given file, which means the ctime or mode changes (atime is ignored).
10
Returns obj, which is an event emitter with events:
11
12
- 'change', ctime, stats - when file changes or is created
13
- 'delete' - when file is deleted
14
15
and a method .close().
16
17
Only fires after the file definitely has not had its
18
ctime changed for at least debounce ms. Does NOT
19
fire when the file first has ctime changed.
20
21
NOTE: for directories we use chokidar in path-watcher. However,
22
for a single file using polling, chokidar is horribly buggy and
23
lacking in functionality (e.g., https://github.com/paulmillr/chokidar/issues/1132),
24
and declared all bugs fixed, so we steer clear. It had a lot of issues
25
with just noticing actual file changes.
26
27
I tried using node:fs's built-in watchFile and it randomly stopped working.
28
Very weird. I think this might have something to do with file paths versus inodes.
29
30
I ended up just writing a file watcher using polling from scratch.
31
32
We *always* use polling to fully support networked filesystems.
33
We use exponential backoff though which doesn't seem to be in any other
34
polling implementation, but reduces load and make sense for our use case.
35
*/
36
37
import { EventEmitter } from "node:events";
38
import { getLogger } from "./logger";
39
import { debounce as lodashDebounce } from "lodash";
40
import { stat } from "fs/promises";
41
42
const logger = getLogger("backend:watcher");
43
44
// exponential backoff to reduce load for inactive files
45
const BACKOFF = 1.2;
46
const MIN_INTERVAL_MS = 750;
47
const MAX_INTERVAL_MS = 5000;
48
49
export class Watcher extends EventEmitter {
50
private path?: string;
51
private prev: any = undefined;
52
private interval: number;
53
private minInterval: number;
54
private maxInterval: number;
55
56
constructor(
57
path: string,
58
{
59
debounce,
60
interval = MIN_INTERVAL_MS,
61
maxInterval = MAX_INTERVAL_MS,
62
}: { debounce?: number; interval?: number; maxInterval?: number } = {},
63
) {
64
super();
65
if (debounce) {
66
this.emitChange = lodashDebounce(this.emitChange, debounce);
67
}
68
logger.debug("Watcher", { path, debounce, interval, maxInterval });
69
this.path = path;
70
this.minInterval = interval;
71
this.maxInterval = maxInterval;
72
this.interval = interval;
73
this.init();
74
}
75
76
private init = async () => {
77
if (this.path == null) {
78
// closed
79
return;
80
}
81
// first time, so initialize it
82
try {
83
this.prev = await stat(this.path);
84
} catch (_) {
85
// doesn't exist
86
this.prev = null;
87
}
88
setTimeout(this.update, this.interval);
89
};
90
91
private update = async () => {
92
if (this.path == null) {
93
// closed
94
return;
95
}
96
try {
97
const prev = this.prev;
98
const curr = await stat(this.path);
99
if (
100
curr.ctimeMs != prev?.ctimeMs ||
101
curr.mtimeMs != prev?.mtimeMs ||
102
curr.mode != prev?.mode
103
) {
104
this.prev = curr;
105
this.interval = this.minInterval;
106
this.emitChange(curr);
107
}
108
} catch (_err) {
109
if (this.prev != null) {
110
this.interval = this.minInterval;
111
this.prev = null;
112
logger.debug("delete", this.path);
113
this.emit("delete");
114
}
115
} finally {
116
setTimeout(this.update, this.interval);
117
this.interval = Math.min(this.maxInterval, this.interval * BACKOFF);
118
}
119
};
120
121
private emitChange = (stats) => {
122
logger.debug("change", this.path);
123
this.emit("change", stats.ctime, stats);
124
};
125
126
close = () => {
127
logger.debug("close", this.path);
128
this.removeAllListeners();
129
delete this.path;
130
};
131
}
132
133