@brftech/filex-core 0.33.0 → 0.34.1
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 +23 -0
- package/dist/filex-core.js +2529 -2514
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +38 -38
- package/dist/filex-core.umd.cjs.map +1 -1
- package/package.json +1 -1
- package/src/composables/useRealtime.ts +12 -15
- package/src/lib/burstDebounce.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/filex-core.umd.cjs",
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { ref, type Ref } from 'vue';
|
|
10
10
|
import { RealtimeClient, type PresenceUser, type PresenceMessage } from '../lib/realtime';
|
|
11
|
+
import { burstDebounce } from '../lib/burstDebounce';
|
|
11
12
|
|
|
12
13
|
const RELOAD_DEBOUNCE_MS = 200;
|
|
14
|
+
// Ceiling on how long a run of change frames may postpone the reload it is
|
|
15
|
+
// waiting for — see burstDebounce for the starvation this exists to stop.
|
|
16
|
+
// Measured in a real browser on 2026-09-06, watching a folder while a
|
|
17
|
+
// 5 000-file zip was extracted into it: the first re-listing came 114 s into
|
|
18
|
+
// the job, and there was a 40 s gap in the middle.
|
|
19
|
+
const RELOAD_MAX_WAIT_MS = 2_000;
|
|
13
20
|
const POLL_INTERVAL_MS = 12_000;
|
|
14
21
|
|
|
15
22
|
export interface RealtimeApi {
|
|
@@ -28,19 +35,12 @@ export function useRealtime(api: RealtimeApi, opts: { reload: () => void }) {
|
|
|
28
35
|
|
|
29
36
|
let client: RealtimeClient | null = null;
|
|
30
37
|
let pendingSubscribe: string | null = null;
|
|
31
|
-
|
|
38
|
+
const debouncedReload = burstDebounce(() => opts.reload(), {
|
|
39
|
+
wait: RELOAD_DEBOUNCE_MS,
|
|
40
|
+
maxWait: RELOAD_MAX_WAIT_MS,
|
|
41
|
+
});
|
|
32
42
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
33
43
|
|
|
34
|
-
function debouncedReload(): void {
|
|
35
|
-
// A burst of change frames (e.g. a multi-file upload) collapses into one
|
|
36
|
-
// soft reload of the current folder.
|
|
37
|
-
if (reloadTimer) clearTimeout(reloadTimer);
|
|
38
|
-
reloadTimer = setTimeout(() => {
|
|
39
|
-
reloadTimer = null;
|
|
40
|
-
opts.reload();
|
|
41
|
-
}, RELOAD_DEBOUNCE_MS);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
44
|
function onPresence(msg: PresenceMessage): void {
|
|
45
45
|
// Ignore late frames for a folder we've already navigated away from.
|
|
46
46
|
if (pendingSubscribe && msg.path && msg.path !== pendingSubscribe) return;
|
|
@@ -85,10 +85,7 @@ export function useRealtime(api: RealtimeApi, opts: { reload: () => void }) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function stop(): void {
|
|
88
|
-
|
|
89
|
-
clearTimeout(reloadTimer);
|
|
90
|
-
reloadTimer = null;
|
|
91
|
-
}
|
|
88
|
+
debouncedReload.cancel();
|
|
92
89
|
if (pollTimer) {
|
|
93
90
|
clearInterval(pollTimer);
|
|
94
91
|
pollTimer = null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// burstDebounce: a trailing debounce that cannot be starved.
|
|
2
|
+
//
|
|
3
|
+
// The explorer re-lists a folder when a change frame says it changed, debounced
|
|
4
|
+
// so a burst of frames costs one listing instead of N. A PLAIN trailing
|
|
5
|
+
// debounce has a failure mode that only shows up under a sustained stream:
|
|
6
|
+
// every frame clears the pending timer, so while frames keep arriving closer
|
|
7
|
+
// together than `wait`, the reload never happens at all. That is not "one
|
|
8
|
+
// reload instead of many" — it is zero, and the person watching the folder sees
|
|
9
|
+
// nothing change until the job that is writing the files finishes.
|
|
10
|
+
//
|
|
11
|
+
// Measured in a real browser on 2026-09-06 against a 5 000-file zip extraction
|
|
12
|
+
// (frames roughly every 40 ms for 195 s): the folder's first re-listing came
|
|
13
|
+
// 114 s in, with a 40 s gap after it.
|
|
14
|
+
//
|
|
15
|
+
// `maxWait` is the ceiling: however long the stream goes on, the call happens
|
|
16
|
+
// at most `maxWait` after the FIRST frame of the run, and the run then starts
|
|
17
|
+
// again. A quiet folder is untouched — one frame still calls `wait` later.
|
|
18
|
+
export interface BurstDebounceOptions {
|
|
19
|
+
/** Quiet period after the last call before firing. */
|
|
20
|
+
wait: number;
|
|
21
|
+
/** Ceiling from the first call of a run. Must be >= wait to mean anything. */
|
|
22
|
+
maxWait: number;
|
|
23
|
+
/** Clock, injectable for tests. */
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BurstDebounced {
|
|
28
|
+
(): void;
|
|
29
|
+
/** Drop any pending call (unmount). */
|
|
30
|
+
cancel(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function burstDebounce(fn: () => void, opts: BurstDebounceOptions): BurstDebounced {
|
|
34
|
+
const now = opts.now ?? (() => Date.now());
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
let runStartedAt: number | null = null;
|
|
37
|
+
|
|
38
|
+
const call = () => {
|
|
39
|
+
const t = now();
|
|
40
|
+
if (runStartedAt === null) runStartedAt = t;
|
|
41
|
+
// Never later than the ceiling, never sooner than 0.
|
|
42
|
+
const untilCeiling = runStartedAt + opts.maxWait - t;
|
|
43
|
+
const delay = Math.max(0, Math.min(opts.wait, untilCeiling));
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
timer = setTimeout(() => {
|
|
46
|
+
timer = null;
|
|
47
|
+
runStartedAt = null;
|
|
48
|
+
fn();
|
|
49
|
+
}, delay);
|
|
50
|
+
};
|
|
51
|
+
call.cancel = () => {
|
|
52
|
+
if (timer) clearTimeout(timer);
|
|
53
|
+
timer = null;
|
|
54
|
+
runStartedAt = null;
|
|
55
|
+
};
|
|
56
|
+
return call;
|
|
57
|
+
}
|