@uekichinos/sentinel 0.1.0 → 0.2.0
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/CHANGELOG.md +7 -0
- package/README.md +39 -4
- package/dist/index.cjs +17 -5
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.global.js +17 -5
- package/dist/index.js +17 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@uekichinos/sentinel` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-04-12
|
|
6
|
+
### Added
|
|
7
|
+
- `sentinel.getRemainingMs()` — returns milliseconds until idle; useful for countdown indicators and progress bars
|
|
8
|
+
- `notify.body` factory function — `body` can now be `() => unknown`, evaluated at idle time rather than at init; useful for capturing dynamic state (e.g. current user ID or session ID)
|
|
9
|
+
- Async `notify.headers` support — `headers` function can now return `Promise<Record<string, string>>`, enabling async token refresh before the idle request fires
|
|
10
|
+
- 17 new tests covering the above additions (45 total)
|
|
11
|
+
|
|
5
12
|
## [0.1.0] - 2026-04-12
|
|
6
13
|
### Added
|
|
7
14
|
- Initial release
|
package/README.md
CHANGED
|
@@ -66,6 +66,18 @@ Restarts the idle countdown from zero. If currently idle, transitions back to ac
|
|
|
66
66
|
|
|
67
67
|
Returns `true` if the user is currently idle.
|
|
68
68
|
|
|
69
|
+
### `sentinel.getRemainingMs()`
|
|
70
|
+
|
|
71
|
+
Returns the number of milliseconds remaining until the user is considered idle. Returns `0` when already idle or when the sentinel has not been started.
|
|
72
|
+
|
|
73
|
+
Useful for building countdown indicators or progress bars:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
setInterval(() => {
|
|
77
|
+
progressBar.style.width = `${(sentinel.getRemainingMs() / timeoutMs) * 100}%`
|
|
78
|
+
}, 100)
|
|
79
|
+
```
|
|
80
|
+
|
|
69
81
|
---
|
|
70
82
|
|
|
71
83
|
## Options
|
|
@@ -119,17 +131,26 @@ const sentinel = createSentinel({
|
|
|
119
131
|
|--------|------|---------|-------------|
|
|
120
132
|
| `url` | `string` | — | Endpoint to call |
|
|
121
133
|
| `method` | `string` | `'POST'` | HTTP method |
|
|
122
|
-
| `headers` | `Record<string, string> \| () => Record<string, string
|
|
123
|
-
| `body` | `unknown` | — | Request body — serialised to JSON |
|
|
134
|
+
| `headers` | `Record<string, string> \| () => Record<string, string> \| Promise<Record<string, string>>` | — | Static or dynamic headers (sync or async) |
|
|
135
|
+
| `body` | `unknown \| () => unknown` | — | Request body — serialised to JSON, or a factory evaluated at idle time |
|
|
124
136
|
|
|
125
|
-
**Headers as a function** — the function is called at the moment idle fires, not at init. This ensures you always send a fresh token rather than one captured when the page loaded.
|
|
137
|
+
**Headers as a function** — the function is called at the moment idle fires, not at init. Supports both sync and async functions. This ensures you always send a fresh token rather than one captured when the page loaded.
|
|
126
138
|
|
|
127
139
|
```js
|
|
128
140
|
// Token captured at init — may be stale after a refresh
|
|
129
141
|
headers: { Authorization: `Bearer ${getToken()}` }
|
|
130
142
|
|
|
131
|
-
// Token evaluated at idle time — always fresh
|
|
143
|
+
// Token evaluated at idle time — always fresh (sync)
|
|
132
144
|
headers: () => ({ Authorization: `Bearer ${getToken()}` })
|
|
145
|
+
|
|
146
|
+
// Async token refresh — awaited before the request fires
|
|
147
|
+
headers: async () => ({ Authorization: `Bearer ${await refreshToken()}` })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Body as a function** — like headers, a body factory is evaluated at idle time rather than at init. Useful for capturing dynamic state:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
body: () => ({ userId: store.user.id, sessionId: store.session.id })
|
|
133
154
|
```
|
|
134
155
|
|
|
135
156
|
The notify request fails silently on network error — `onIdle` always fires regardless.
|
|
@@ -181,6 +202,20 @@ pollInterval = setInterval(fetchData, 5000)
|
|
|
181
202
|
sentinel.start()
|
|
182
203
|
```
|
|
183
204
|
|
|
205
|
+
### Countdown indicator
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const TIMEOUT_MS = 15 * 60 * 1000 // 15m in ms
|
|
209
|
+
|
|
210
|
+
const sentinel = createSentinel({ timeout: TIMEOUT_MS })
|
|
211
|
+
sentinel.start()
|
|
212
|
+
|
|
213
|
+
setInterval(() => {
|
|
214
|
+
const pct = (sentinel.getRemainingMs() / TIMEOUT_MS) * 100
|
|
215
|
+
progressBar.style.width = `${pct}%`
|
|
216
|
+
}, 200)
|
|
217
|
+
```
|
|
218
|
+
|
|
184
219
|
### Stop on page unload
|
|
185
220
|
|
|
186
221
|
```js
|
package/dist/index.cjs
CHANGED
|
@@ -45,13 +45,18 @@ function parseTtl(ttl) {
|
|
|
45
45
|
// src/index.ts
|
|
46
46
|
var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
|
|
47
47
|
var DEFAULT_THROTTLE = 500;
|
|
48
|
-
function resolveHeaders(headers) {
|
|
48
|
+
async function resolveHeaders(headers) {
|
|
49
49
|
if (!headers) return {};
|
|
50
|
-
|
|
50
|
+
const result = typeof headers === "function" ? headers() : headers;
|
|
51
|
+
return result instanceof Promise ? await result : result;
|
|
52
|
+
}
|
|
53
|
+
function resolveBody(body) {
|
|
54
|
+
return typeof body === "function" ? body() : body;
|
|
51
55
|
}
|
|
52
56
|
async function fireNotify(notify) {
|
|
53
|
-
const headers = resolveHeaders(notify.headers);
|
|
54
|
-
const
|
|
57
|
+
const headers = await resolveHeaders(notify.headers);
|
|
58
|
+
const body = resolveBody(notify.body);
|
|
59
|
+
const hasBody = body !== void 0;
|
|
55
60
|
try {
|
|
56
61
|
await fetch(notify.url, {
|
|
57
62
|
method: notify.method ?? "POST",
|
|
@@ -59,7 +64,7 @@ async function fireNotify(notify) {
|
|
|
59
64
|
...hasBody ? { "Content-Type": "application/json" } : {},
|
|
60
65
|
...headers
|
|
61
66
|
},
|
|
62
|
-
...hasBody ? { body: JSON.stringify(
|
|
67
|
+
...hasBody ? { body: JSON.stringify(body) } : {}
|
|
63
68
|
});
|
|
64
69
|
} catch {
|
|
65
70
|
}
|
|
@@ -72,13 +77,16 @@ function createSentinel(options) {
|
|
|
72
77
|
const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
|
|
73
78
|
const watchVisibility = options.watchVisibility ?? true;
|
|
74
79
|
let timer = null;
|
|
80
|
+
let timerTarget = null;
|
|
75
81
|
let idle = false;
|
|
76
82
|
let started = false;
|
|
77
83
|
let lastActivity = 0;
|
|
78
84
|
function scheduleIdle() {
|
|
79
85
|
if (timer !== null) clearTimeout(timer);
|
|
86
|
+
timerTarget = Date.now() + timeoutMs;
|
|
80
87
|
timer = setTimeout(() => {
|
|
81
88
|
timer = null;
|
|
89
|
+
timerTarget = null;
|
|
82
90
|
idle = true;
|
|
83
91
|
options.onIdle?.();
|
|
84
92
|
if (options.notify) fireNotify(options.notify);
|
|
@@ -143,6 +151,10 @@ function createSentinel(options) {
|
|
|
143
151
|
},
|
|
144
152
|
isIdle() {
|
|
145
153
|
return idle;
|
|
154
|
+
},
|
|
155
|
+
getRemainingMs() {
|
|
156
|
+
if (idle || !started || timerTarget === null) return 0;
|
|
157
|
+
return Math.max(0, timerTarget - Date.now());
|
|
146
158
|
}
|
|
147
159
|
};
|
|
148
160
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -6,11 +6,14 @@ type NotifyOptions = {
|
|
|
6
6
|
method?: string;
|
|
7
7
|
/**
|
|
8
8
|
* Headers to include in the request.
|
|
9
|
-
* Pass a function to evaluate headers at call time — useful for dynamic tokens.
|
|
9
|
+
* Pass a function (sync or async) to evaluate headers at call time — useful for dynamic or refreshed tokens.
|
|
10
10
|
*/
|
|
11
|
-
headers?: Record<string, string> | (() => Record<string, string>);
|
|
12
|
-
/**
|
|
13
|
-
|
|
11
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
12
|
+
/**
|
|
13
|
+
* Optional request body — serialised to JSON.
|
|
14
|
+
* Pass a function to evaluate the body at idle time — useful for capturing dynamic state.
|
|
15
|
+
*/
|
|
16
|
+
body?: unknown | (() => unknown);
|
|
14
17
|
};
|
|
15
18
|
type SentinelOptions = {
|
|
16
19
|
/** How long before the user is considered idle */
|
|
@@ -37,6 +40,8 @@ type SentinelInstance = {
|
|
|
37
40
|
reset(): void;
|
|
38
41
|
/** Returns true if the user is currently idle */
|
|
39
42
|
isIdle(): boolean;
|
|
43
|
+
/** Returns milliseconds remaining until idle. Returns 0 when already idle or not started. */
|
|
44
|
+
getRemainingMs(): number;
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,14 @@ type NotifyOptions = {
|
|
|
6
6
|
method?: string;
|
|
7
7
|
/**
|
|
8
8
|
* Headers to include in the request.
|
|
9
|
-
* Pass a function to evaluate headers at call time — useful for dynamic tokens.
|
|
9
|
+
* Pass a function (sync or async) to evaluate headers at call time — useful for dynamic or refreshed tokens.
|
|
10
10
|
*/
|
|
11
|
-
headers?: Record<string, string> | (() => Record<string, string>);
|
|
12
|
-
/**
|
|
13
|
-
|
|
11
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
12
|
+
/**
|
|
13
|
+
* Optional request body — serialised to JSON.
|
|
14
|
+
* Pass a function to evaluate the body at idle time — useful for capturing dynamic state.
|
|
15
|
+
*/
|
|
16
|
+
body?: unknown | (() => unknown);
|
|
14
17
|
};
|
|
15
18
|
type SentinelOptions = {
|
|
16
19
|
/** How long before the user is considered idle */
|
|
@@ -37,6 +40,8 @@ type SentinelInstance = {
|
|
|
37
40
|
reset(): void;
|
|
38
41
|
/** Returns true if the user is currently idle */
|
|
39
42
|
isIdle(): boolean;
|
|
43
|
+
/** Returns milliseconds remaining until idle. Returns 0 when already idle or not started. */
|
|
44
|
+
getRemainingMs(): number;
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
/**
|
package/dist/index.global.js
CHANGED
|
@@ -45,13 +45,18 @@ var Sentinel = (() => {
|
|
|
45
45
|
// src/index.ts
|
|
46
46
|
var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
|
|
47
47
|
var DEFAULT_THROTTLE = 500;
|
|
48
|
-
function resolveHeaders(headers) {
|
|
48
|
+
async function resolveHeaders(headers) {
|
|
49
49
|
if (!headers) return {};
|
|
50
|
-
|
|
50
|
+
const result = typeof headers === "function" ? headers() : headers;
|
|
51
|
+
return result instanceof Promise ? await result : result;
|
|
52
|
+
}
|
|
53
|
+
function resolveBody(body) {
|
|
54
|
+
return typeof body === "function" ? body() : body;
|
|
51
55
|
}
|
|
52
56
|
async function fireNotify(notify) {
|
|
53
|
-
const headers = resolveHeaders(notify.headers);
|
|
54
|
-
const
|
|
57
|
+
const headers = await resolveHeaders(notify.headers);
|
|
58
|
+
const body = resolveBody(notify.body);
|
|
59
|
+
const hasBody = body !== void 0;
|
|
55
60
|
try {
|
|
56
61
|
await fetch(notify.url, {
|
|
57
62
|
method: notify.method ?? "POST",
|
|
@@ -59,7 +64,7 @@ var Sentinel = (() => {
|
|
|
59
64
|
...hasBody ? { "Content-Type": "application/json" } : {},
|
|
60
65
|
...headers
|
|
61
66
|
},
|
|
62
|
-
...hasBody ? { body: JSON.stringify(
|
|
67
|
+
...hasBody ? { body: JSON.stringify(body) } : {}
|
|
63
68
|
});
|
|
64
69
|
} catch {
|
|
65
70
|
}
|
|
@@ -72,13 +77,16 @@ var Sentinel = (() => {
|
|
|
72
77
|
const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
|
|
73
78
|
const watchVisibility = options.watchVisibility ?? true;
|
|
74
79
|
let timer = null;
|
|
80
|
+
let timerTarget = null;
|
|
75
81
|
let idle = false;
|
|
76
82
|
let started = false;
|
|
77
83
|
let lastActivity = 0;
|
|
78
84
|
function scheduleIdle() {
|
|
79
85
|
if (timer !== null) clearTimeout(timer);
|
|
86
|
+
timerTarget = Date.now() + timeoutMs;
|
|
80
87
|
timer = setTimeout(() => {
|
|
81
88
|
timer = null;
|
|
89
|
+
timerTarget = null;
|
|
82
90
|
idle = true;
|
|
83
91
|
options.onIdle?.();
|
|
84
92
|
if (options.notify) fireNotify(options.notify);
|
|
@@ -143,6 +151,10 @@ var Sentinel = (() => {
|
|
|
143
151
|
},
|
|
144
152
|
isIdle() {
|
|
145
153
|
return idle;
|
|
154
|
+
},
|
|
155
|
+
getRemainingMs() {
|
|
156
|
+
if (idle || !started || timerTarget === null) return 0;
|
|
157
|
+
return Math.max(0, timerTarget - Date.now());
|
|
146
158
|
}
|
|
147
159
|
};
|
|
148
160
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,13 +19,18 @@ function parseTtl(ttl) {
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
|
|
21
21
|
var DEFAULT_THROTTLE = 500;
|
|
22
|
-
function resolveHeaders(headers) {
|
|
22
|
+
async function resolveHeaders(headers) {
|
|
23
23
|
if (!headers) return {};
|
|
24
|
-
|
|
24
|
+
const result = typeof headers === "function" ? headers() : headers;
|
|
25
|
+
return result instanceof Promise ? await result : result;
|
|
26
|
+
}
|
|
27
|
+
function resolveBody(body) {
|
|
28
|
+
return typeof body === "function" ? body() : body;
|
|
25
29
|
}
|
|
26
30
|
async function fireNotify(notify) {
|
|
27
|
-
const headers = resolveHeaders(notify.headers);
|
|
28
|
-
const
|
|
31
|
+
const headers = await resolveHeaders(notify.headers);
|
|
32
|
+
const body = resolveBody(notify.body);
|
|
33
|
+
const hasBody = body !== void 0;
|
|
29
34
|
try {
|
|
30
35
|
await fetch(notify.url, {
|
|
31
36
|
method: notify.method ?? "POST",
|
|
@@ -33,7 +38,7 @@ async function fireNotify(notify) {
|
|
|
33
38
|
...hasBody ? { "Content-Type": "application/json" } : {},
|
|
34
39
|
...headers
|
|
35
40
|
},
|
|
36
|
-
...hasBody ? { body: JSON.stringify(
|
|
41
|
+
...hasBody ? { body: JSON.stringify(body) } : {}
|
|
37
42
|
});
|
|
38
43
|
} catch {
|
|
39
44
|
}
|
|
@@ -46,13 +51,16 @@ function createSentinel(options) {
|
|
|
46
51
|
const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
|
|
47
52
|
const watchVisibility = options.watchVisibility ?? true;
|
|
48
53
|
let timer = null;
|
|
54
|
+
let timerTarget = null;
|
|
49
55
|
let idle = false;
|
|
50
56
|
let started = false;
|
|
51
57
|
let lastActivity = 0;
|
|
52
58
|
function scheduleIdle() {
|
|
53
59
|
if (timer !== null) clearTimeout(timer);
|
|
60
|
+
timerTarget = Date.now() + timeoutMs;
|
|
54
61
|
timer = setTimeout(() => {
|
|
55
62
|
timer = null;
|
|
63
|
+
timerTarget = null;
|
|
56
64
|
idle = true;
|
|
57
65
|
options.onIdle?.();
|
|
58
66
|
if (options.notify) fireNotify(options.notify);
|
|
@@ -117,6 +125,10 @@ function createSentinel(options) {
|
|
|
117
125
|
},
|
|
118
126
|
isIdle() {
|
|
119
127
|
return idle;
|
|
128
|
+
},
|
|
129
|
+
getRemainingMs() {
|
|
130
|
+
if (idle || !started || timerTarget === null) return 0;
|
|
131
|
+
return Math.max(0, timerTarget - Date.now());
|
|
120
132
|
}
|
|
121
133
|
};
|
|
122
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uekichinos/sentinel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight idle detection for the browser. Fires callbacks and optionally notifies a backend when the user goes inactive. Supports TTL timeouts, tab visibility pausing, activity throttling, and dynamic auth headers. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"uekichinos",
|