@soham20/smart-offline-sdk 0.1.0 → 0.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/PROJECT_ANALYSIS.md +94 -0
- package/README.md +53 -4
- package/package.json +1 -1
- package/smart-offline-sw.js +162 -24
- package/src/config.js +16 -0
- package/src/index.js +21 -6
- package/src/priority.js +44 -0
- package/src/usageTracker.js +37 -0
- package/.github/workflows/ci.yml +0 -17
- package/CHANGELOG.md +0 -6
- package/demo/api/profile.json +0 -5
- package/demo/index.html +0 -32
- package/examples/example.js +0 -7
- package/tests/basic.test.js +0 -7
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SmartOffline SDK — Project Analysis
|
|
2
|
+
|
|
3
|
+
Date: 2026-01-22
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Project Summary
|
|
8
|
+
|
|
9
|
+
SmartOffline is a JavaScript SDK that enables offline-first behavior by registering a Service Worker which caches configured pages and API responses. The repository includes a demo, basic tests, and a Node-style test SDK.
|
|
10
|
+
|
|
11
|
+
Key locations:
|
|
12
|
+
|
|
13
|
+
- `smart-offline-sw.js` — Service Worker implementing caching, usage tracking, and priority logic.
|
|
14
|
+
- `src/index.js` — Browser SDK entry (`SmartOffline.init`) that registers the SW and sends configuration.
|
|
15
|
+
- `src/usageTracker.js` — IndexedDB-based usage tracker (client-side helper).
|
|
16
|
+
- `src/sdk/index.js` — Small Node test client (`HackvisionClient`) used by tests/examples.
|
|
17
|
+
- `demo/` — Demo page and sample API (`demo/index.html`, `demo/api/profile.json`).
|
|
18
|
+
- `tests/` — `tests/basic.test.js` (Jest) covering the test client.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Features That Work (Implemented)
|
|
23
|
+
|
|
24
|
+
- Service Worker registration via `SmartOffline.init()` (browser).
|
|
25
|
+
- SW intercepts `GET` requests for configured pages and APIs and caches successful network responses.
|
|
26
|
+
- Offline fallback: SW serves cached responses when fetch fails.
|
|
27
|
+
- Usage tracking in IndexedDB (stores `count` and `lastAccessed` per URL) implemented both in `smart-offline-sw.js` and `src/usageTracker.js`.
|
|
28
|
+
- Priority determination (high/normal) based on frequency and recency thresholds.
|
|
29
|
+
- Demo implementation showing SDK usage and an API example.
|
|
30
|
+
- Node test client (`HackvisionClient.echo`) and unit test (`jest`) pass.
|
|
31
|
+
- CI workflow exists to run tests (.github/workflows/ci.yml).
|
|
32
|
+
- Package published as `@soham20/smart-offline-sdk` (v0.1.1).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Issues & Gaps (Needs Attention)
|
|
37
|
+
|
|
38
|
+
- README contains unresolved merge markers. Clean and add clear quickstarts for browser and Node.
|
|
39
|
+
- `package.json` declares `"type": "commonjs"` but `src/index.js` uses ESM-style `export` (module mismatch). This may confuse Node consumers and bundlers.
|
|
40
|
+
- No build/bundling step: source files are published directly. This reduces compatibility across consumers (CJS/ESM/browser UMD). No `dist/` artifacts.
|
|
41
|
+
- Service Worker behavior is not covered by automated integration tests (typical but recommended to add Playwright/Puppeteer tests for offline behavior).
|
|
42
|
+
- Inconsistent public API surface: demo uses `SmartOffline.init()` while package `main` points to `src/sdk/index.js` (Node test client). Clarify and add a top-level entry that documents both browser and Node exports.
|
|
43
|
+
- Privacy/consent: usage tracking stores per-URL usage in IndexedDB — document privacy implications and provide opt-out.
|
|
44
|
+
- No cache eviction / quota management — cache can grow without limits.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Short-Term Recommendations (Actionable)
|
|
49
|
+
|
|
50
|
+
1. Fix `README.md` (remove merge markers) and add clear Quickstart sections for browser and Node usage.
|
|
51
|
+
2. Standardize module format:
|
|
52
|
+
- Option A: Build ESM + CJS bundles (Rollup) and set `main`/`module`/`exports` in `package.json`.
|
|
53
|
+
- Option B: Convert `src/index.js` to CommonJS `module.exports` if targeting Node-only.
|
|
54
|
+
3. Add a build step that outputs `dist/` bundles (UMD for browsers, ESM and CJS for Node). Update `package.json` scripts (`build`, `prepublishOnly`).
|
|
55
|
+
4. Add integration tests (Playwright) that:
|
|
56
|
+
- Serve demo, register SW, fetch a resource online, go offline, and verify cached response is served.
|
|
57
|
+
5. Add cache size management (LRU or simple max-entries) using usage metrics in IndexedDB.
|
|
58
|
+
6. Document the usage tracking behavior and add opt-out configuration (e.g., `trackUsage: false`).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Mid/Large-Term Enhancements (Future)
|
|
63
|
+
|
|
64
|
+
- Prefetching & background sync for high-priority resources.
|
|
65
|
+
- Configurable stale-while-revalidate and TTL per resource.
|
|
66
|
+
- TypeScript conversion + publishing type definitions.
|
|
67
|
+
- Expose a small diagnostics API so apps can query cache status and usage stats.
|
|
68
|
+
- Publish a UMD browser bundle and host on a CDN for direct `<script>` usage.
|
|
69
|
+
- Telemetry (opt-in) and analytics for adoption + cache effectiveness metrics.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Suggested Next Steps (I can implement)
|
|
74
|
+
|
|
75
|
+
- Patch `README.md` to a clean quickstart (browser + Node).
|
|
76
|
+
- Add a small build setup (Rollup) to produce `dist/` artifacts and update `package.json`.
|
|
77
|
+
- Add a Playwright integration test for offline cache validation.
|
|
78
|
+
|
|
79
|
+
If you want, I can: (pick one or more)
|
|
80
|
+
|
|
81
|
+
- Fix the `README.md` now.
|
|
82
|
+
- Add a minimal Rollup build and update `package.json`.
|
|
83
|
+
- Add a Playwright test and a small local server script to run it.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Notes & Observations
|
|
88
|
+
|
|
89
|
+
- The published package name changed from `hackvision2026-sdk` (spam-detected) to scoped `@soham20/smart-offline-sdk`, which is preferable.
|
|
90
|
+
- Tests pass (`npm test`). The test target is small (echo), so expand tests to include SDK behavior where feasible.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
_Generated by repository scan on 2026-01-22._
|
package/README.md
CHANGED
|
@@ -2,13 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
This repository contains the JavaScript SDK for Hackvision2026.
|
|
4
4
|
|
|
5
|
-
Quickstart
|
|
5
|
+
## Quickstart
|
|
6
6
|
|
|
7
7
|
```javascript
|
|
8
|
-
|
|
8
|
+
import { SmartOffline } from "./src/index.js";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
SmartOffline.init({
|
|
11
|
+
pages: ["/dashboard", "/profile"],
|
|
12
|
+
apis: ["/api/user", "/api/data"],
|
|
13
|
+
debug: true,
|
|
14
|
+
});
|
|
12
15
|
```
|
|
13
16
|
|
|
14
17
|
See `examples/` for a runnable example.
|
|
18
|
+
|
|
19
|
+
## Priority Tuning Options
|
|
20
|
+
|
|
21
|
+
You can fine-tune how the SDK decides caching priority:
|
|
22
|
+
|
|
23
|
+
| Option | Type | Default | Description |
|
|
24
|
+
|---------------------|-------------------------------------------|---------------|-----------------------------------------------------------------------------|
|
|
25
|
+
| `frequencyThreshold`| `number` | `3` | Number of accesses before a resource is considered "frequent" |
|
|
26
|
+
| `recencyThreshold` | `number` (ms) | `86400000` (24h) | Milliseconds within which a resource is considered "recent" |
|
|
27
|
+
| `maxResourceSize` | `number` (bytes) | `Infinity` | Max bytes to cache per resource; larger resources are skipped |
|
|
28
|
+
| `networkQuality` | `'auto'` \| `'fast'` \| `'slow'` | `'auto'` | Affects caching aggressiveness; `'auto'` uses Network Information API |
|
|
29
|
+
| `significance` | `{ [urlPattern: string]: 'high' \| 'normal' \| 'low' }` | `{}` | Manual priority overrides per URL pattern |
|
|
30
|
+
|
|
31
|
+
### Example with all options
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
SmartOffline.init({
|
|
35
|
+
pages: ["/dashboard"],
|
|
36
|
+
apis: ["/api/"],
|
|
37
|
+
debug: true,
|
|
38
|
+
|
|
39
|
+
// Priority tuning
|
|
40
|
+
frequencyThreshold: 5, // require 5 accesses to be "frequent"
|
|
41
|
+
recencyThreshold: 12 * 60 * 60 * 1000, // 12 hours
|
|
42
|
+
maxResourceSize: 500 * 1024, // skip caching resources > 500 KB
|
|
43
|
+
networkQuality: "auto", // or 'fast' / 'slow'
|
|
44
|
+
significance: {
|
|
45
|
+
"/api/critical": "high", // always high priority
|
|
46
|
+
"/api/analytics": "low", // always low priority
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### From npm (after publishing)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @soham20/smart-offline-sdk
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Directly from GitHub
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install git+https://github.com/OwaisShaikh1/Hackvision2026.git
|
|
63
|
+
```
|
package/package.json
CHANGED
package/smart-offline-sw.js
CHANGED
|
@@ -2,11 +2,19 @@ const CACHE_NAME = "smart-offline-cache-v1";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SDK configuration received from SmartOffline.init()
|
|
5
|
+
* Includes priority tuning knobs.
|
|
5
6
|
*/
|
|
6
7
|
let SDK_CONFIG = {
|
|
7
8
|
pages: [],
|
|
8
9
|
apis: [],
|
|
9
10
|
debug: false,
|
|
11
|
+
|
|
12
|
+
// Priority tuning defaults
|
|
13
|
+
frequencyThreshold: 3,
|
|
14
|
+
recencyThreshold: 24 * 60 * 60 * 1000, // 24h
|
|
15
|
+
maxResourceSize: Infinity,
|
|
16
|
+
networkQuality: "auto", // 'auto' | 'fast' | 'slow'
|
|
17
|
+
significance: {}, // { urlPattern: 'high' | 'normal' | 'low' }
|
|
10
18
|
};
|
|
11
19
|
|
|
12
20
|
/**
|
|
@@ -22,11 +30,104 @@ self.addEventListener("message", (event) => {
|
|
|
22
30
|
}
|
|
23
31
|
});
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* -------- Usage Tracking (IndexedDB) --------
|
|
35
|
+
* Tracks frequency + recency per URL
|
|
36
|
+
*/
|
|
37
|
+
function trackUsage(url) {
|
|
38
|
+
const request = indexedDB.open("smart-offline-usage", 1);
|
|
39
|
+
|
|
40
|
+
request.onupgradeneeded = () => {
|
|
41
|
+
const db = request.result;
|
|
42
|
+
if (!db.objectStoreNames.contains("usage")) {
|
|
43
|
+
db.createObjectStore("usage", { keyPath: "url" });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
request.onsuccess = () => {
|
|
48
|
+
const db = request.result;
|
|
49
|
+
const tx = db.transaction("usage", "readwrite");
|
|
50
|
+
const store = tx.objectStore("usage");
|
|
51
|
+
|
|
52
|
+
const getReq = store.get(url);
|
|
53
|
+
getReq.onsuccess = () => {
|
|
54
|
+
const data = getReq.result || {
|
|
55
|
+
url,
|
|
56
|
+
count: 0,
|
|
57
|
+
lastAccessed: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
data.count += 1;
|
|
61
|
+
data.lastAccessed = Date.now();
|
|
62
|
+
store.put(data);
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read usage info
|
|
69
|
+
*/
|
|
70
|
+
function getUsage(url) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const request = indexedDB.open("smart-offline-usage", 1);
|
|
73
|
+
|
|
74
|
+
request.onsuccess = () => {
|
|
75
|
+
const db = request.result;
|
|
76
|
+
const tx = db.transaction("usage", "readonly");
|
|
77
|
+
const store = tx.objectStore("usage");
|
|
78
|
+
|
|
79
|
+
const getReq = store.get(url);
|
|
80
|
+
getReq.onsuccess = () => resolve(getReq.result);
|
|
81
|
+
getReq.onerror = () => resolve(null);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
request.onerror = () => resolve(null);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decide priority based on real usage and developer-tuned config
|
|
90
|
+
*/
|
|
91
|
+
function isHighPriority(usage, url) {
|
|
92
|
+
// Manual significance override
|
|
93
|
+
for (const pattern in SDK_CONFIG.significance) {
|
|
94
|
+
if (url.includes(pattern)) {
|
|
95
|
+
const sig = SDK_CONFIG.significance[pattern];
|
|
96
|
+
if (sig === "high") return true;
|
|
97
|
+
if (sig === "low") return false;
|
|
98
|
+
// 'normal' falls through to dynamic logic
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!usage) return false;
|
|
103
|
+
|
|
104
|
+
const frequent = usage.count >= SDK_CONFIG.frequencyThreshold;
|
|
105
|
+
const recent = Date.now() - usage.lastAccessed <= SDK_CONFIG.recencyThreshold;
|
|
106
|
+
|
|
107
|
+
return frequent || recent;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect effective network quality (uses Navigator.connection if available)
|
|
112
|
+
*/
|
|
113
|
+
function getEffectiveNetworkQuality() {
|
|
114
|
+
if (SDK_CONFIG.networkQuality !== "auto") {
|
|
115
|
+
return SDK_CONFIG.networkQuality; // developer override
|
|
116
|
+
}
|
|
117
|
+
// Use Network Information API if available
|
|
118
|
+
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
119
|
+
if (conn) {
|
|
120
|
+
const dominated = ["slow-2g", "2g", "3g"];
|
|
121
|
+
if (dominated.includes(conn.effectiveType)) return "slow";
|
|
122
|
+
}
|
|
123
|
+
return "fast";
|
|
124
|
+
}
|
|
125
|
+
|
|
25
126
|
/**
|
|
26
127
|
* Install event
|
|
27
128
|
*/
|
|
28
129
|
self.addEventListener("install", (event) => {
|
|
29
|
-
self.skipWaiting();
|
|
130
|
+
self.skipWaiting();
|
|
30
131
|
event.waitUntil(caches.open(CACHE_NAME));
|
|
31
132
|
});
|
|
32
133
|
|
|
@@ -36,69 +137,106 @@ self.addEventListener("install", (event) => {
|
|
|
36
137
|
self.addEventListener("activate", (event) => {
|
|
37
138
|
event.waitUntil(
|
|
38
139
|
Promise.all([
|
|
39
|
-
self.clients.claim(),
|
|
140
|
+
self.clients.claim(),
|
|
40
141
|
caches.keys().then((cacheNames) =>
|
|
41
142
|
Promise.all(
|
|
42
143
|
cacheNames.map((name) => {
|
|
43
144
|
if (name !== CACHE_NAME) {
|
|
44
145
|
return caches.delete(name);
|
|
45
146
|
}
|
|
46
|
-
})
|
|
47
|
-
)
|
|
147
|
+
})
|
|
148
|
+
)
|
|
48
149
|
),
|
|
49
|
-
])
|
|
150
|
+
])
|
|
50
151
|
);
|
|
51
152
|
});
|
|
52
153
|
|
|
53
|
-
|
|
54
154
|
/**
|
|
55
|
-
* Fetch event
|
|
155
|
+
* Fetch event — SMART + PRIORITY logic
|
|
56
156
|
*/
|
|
57
157
|
self.addEventListener("fetch", (event) => {
|
|
58
158
|
const request = event.request;
|
|
59
159
|
|
|
60
160
|
if (request.method !== "GET") return;
|
|
61
161
|
|
|
62
|
-
console.log("[SW] Intercepted:", request.url);
|
|
63
|
-
|
|
64
162
|
const isPage = SDK_CONFIG.pages.some((p) =>
|
|
65
163
|
request.url.includes(p)
|
|
66
164
|
);
|
|
67
|
-
|
|
68
165
|
const isAPI = SDK_CONFIG.apis.some((a) =>
|
|
69
166
|
request.url.includes(a)
|
|
70
167
|
);
|
|
71
168
|
|
|
72
169
|
if (!isPage && !isAPI) return;
|
|
73
170
|
|
|
171
|
+
if (SDK_CONFIG.debug) {
|
|
172
|
+
console.log("[SW] Intercepted:", request.url);
|
|
173
|
+
}
|
|
174
|
+
|
|
74
175
|
event.respondWith(
|
|
75
176
|
fetch(request)
|
|
76
|
-
.then((response) => {
|
|
77
|
-
|
|
78
|
-
|
|
177
|
+
.then(async (response) => {
|
|
178
|
+
// Network success
|
|
179
|
+
trackUsage(request.url);
|
|
180
|
+
|
|
181
|
+
// Check resource size limit
|
|
182
|
+
const contentLength = response.headers.get("content-length");
|
|
183
|
+
const size = contentLength ? parseInt(contentLength, 10) : 0;
|
|
184
|
+
if (size > SDK_CONFIG.maxResourceSize) {
|
|
185
|
+
if (SDK_CONFIG.debug) {
|
|
186
|
+
console.log(
|
|
187
|
+
`[SmartOffline] Skipped caching (size ${size} > ${SDK_CONFIG.maxResourceSize}):`,
|
|
188
|
+
request.url
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Network quality aware caching
|
|
195
|
+
const netQuality = getEffectiveNetworkQuality();
|
|
196
|
+
if (netQuality === "slow" && !isHighPriority(null, request.url)) {
|
|
197
|
+
// On slow network, skip caching low priority resources proactively
|
|
198
|
+
if (SDK_CONFIG.debug) {
|
|
199
|
+
console.log(
|
|
200
|
+
`[SmartOffline] Skipped caching (slow network, not high priority):`,
|
|
201
|
+
request.url
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
79
206
|
|
|
207
|
+
const clone = response.clone();
|
|
80
208
|
caches.open(CACHE_NAME).then((cache) => {
|
|
81
|
-
cache.put(
|
|
209
|
+
cache.put(request.url, clone);
|
|
82
210
|
});
|
|
83
211
|
|
|
84
212
|
if (SDK_CONFIG.debug) {
|
|
85
213
|
console.log(
|
|
86
214
|
`[SmartOffline] Cached ${isAPI ? "API" : "PAGE"}:`,
|
|
87
|
-
|
|
215
|
+
request.url
|
|
88
216
|
);
|
|
89
217
|
}
|
|
90
218
|
|
|
91
219
|
return response;
|
|
92
220
|
})
|
|
93
221
|
.catch(() => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
222
|
+
// Offline / network failure
|
|
223
|
+
trackUsage(request.url);
|
|
224
|
+
|
|
225
|
+
return getUsage(request.url).then((usage) => {
|
|
226
|
+
const highPriority = isHighPriority(usage, request.url);
|
|
227
|
+
|
|
228
|
+
if (SDK_CONFIG.debug) {
|
|
229
|
+
console.log(
|
|
230
|
+
`[SmartOffline] ${
|
|
231
|
+
highPriority ? "HIGH" : "NORMAL"
|
|
232
|
+
} priority:`,
|
|
233
|
+
request.url
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// v1 behavior: both return cache, but priority is decided & logged
|
|
238
|
+
return caches.match(request.url);
|
|
239
|
+
});
|
|
240
|
+
})
|
|
102
241
|
);
|
|
103
242
|
});
|
|
104
|
-
|
package/src/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Build sdk config object with defaults. Used by tests to validate defaults.
|
|
2
|
+
function buildConfig(input = {}) {
|
|
3
|
+
return {
|
|
4
|
+
pages: input.pages || [],
|
|
5
|
+
apis: input.apis || [],
|
|
6
|
+
debug: input.debug || false,
|
|
7
|
+
|
|
8
|
+
frequencyThreshold: input.frequencyThreshold ?? 3,
|
|
9
|
+
recencyThreshold: input.recencyThreshold ?? 24 * 60 * 60 * 1000,
|
|
10
|
+
maxResourceSize: input.maxResourceSize ?? Infinity,
|
|
11
|
+
networkQuality: input.networkQuality ?? 'auto',
|
|
12
|
+
significance: input.significance || {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { buildConfig };
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartOffline SDK
|
|
3
|
+
*
|
|
4
|
+
* Priority config options:
|
|
5
|
+
* - frequencyThreshold: number of accesses before resource is considered "frequent" (default 3)
|
|
6
|
+
* - recencyThreshold: milliseconds within which resource is considered "recent" (default 24h)
|
|
7
|
+
* - maxResourceSize: max bytes to cache per resource; larger resources skipped (default Infinity)
|
|
8
|
+
* - networkQuality: 'auto' | 'fast' | 'slow' — affects caching aggressiveness (default 'auto')
|
|
9
|
+
* - significance: { [urlPattern]: 'high' | 'normal' | 'low' } — manual priority overrides
|
|
10
|
+
*/
|
|
1
11
|
function init(config = {}) {
|
|
2
12
|
if (!("serviceWorker" in navigator)) {
|
|
3
13
|
console.warn("Service Workers not supported");
|
|
@@ -8,14 +18,14 @@ function init(config = {}) {
|
|
|
8
18
|
pages: config.pages || [],
|
|
9
19
|
apis: config.apis || [],
|
|
10
20
|
debug: config.debug || false,
|
|
21
|
+
|
|
22
|
+
|
|
11
23
|
};
|
|
12
24
|
|
|
13
|
-
navigator.serviceWorker.register("/smart-offline-sw.js").then((
|
|
25
|
+
navigator.serviceWorker.register("/smart-offline-sw.js").then(() => {
|
|
14
26
|
console.log("Smart Offline Service Worker registered");
|
|
15
27
|
|
|
16
|
-
// Wait for SW to be ready before sending config
|
|
17
28
|
navigator.serviceWorker.ready.then(() => {
|
|
18
|
-
// Send config to active service worker
|
|
19
29
|
if (navigator.serviceWorker.controller) {
|
|
20
30
|
navigator.serviceWorker.controller.postMessage({
|
|
21
31
|
type: "INIT_CONFIG",
|
|
@@ -28,7 +38,6 @@ function init(config = {}) {
|
|
|
28
38
|
}
|
|
29
39
|
});
|
|
30
40
|
|
|
31
|
-
// Handle new SW taking control (on first install)
|
|
32
41
|
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
33
42
|
if (navigator.serviceWorker.controller) {
|
|
34
43
|
navigator.serviceWorker.controller.postMessage({
|
|
@@ -37,11 +46,17 @@ function init(config = {}) {
|
|
|
37
46
|
});
|
|
38
47
|
|
|
39
48
|
if (sdkConfig.debug) {
|
|
40
|
-
console.log(
|
|
49
|
+
console.log(
|
|
50
|
+
"[SmartOffline] Config sent after controllerchange:",
|
|
51
|
+
sdkConfig
|
|
52
|
+
);
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
});
|
|
44
56
|
});
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
const SmartOffline = { init };
|
|
60
|
+
|
|
61
|
+
export { SmartOffline }; // ✅ named export
|
|
62
|
+
export default SmartOffline; // ✅ default export
|
package/src/priority.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Utility functions for priority decisions used by the service worker
|
|
2
|
+
|
|
3
|
+
function matchesSignificance(url, significance) {
|
|
4
|
+
for (const pattern in significance) {
|
|
5
|
+
if (Object.prototype.hasOwnProperty.call(significance, pattern)) {
|
|
6
|
+
if (url.includes(pattern)) return significance[pattern];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isHighPriority(usage, url, config = {}) {
|
|
13
|
+
// Manual significance override
|
|
14
|
+
if (config.significance) {
|
|
15
|
+
const sig = matchesSignificance(url, config.significance);
|
|
16
|
+
if (sig === 'high') return true;
|
|
17
|
+
if (sig === 'low') return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!usage) return false;
|
|
21
|
+
|
|
22
|
+
const freqThreshold = config.frequencyThreshold ?? 3;
|
|
23
|
+
const recencyThreshold = config.recencyThreshold ?? 24 * 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const frequent = usage.count >= freqThreshold;
|
|
26
|
+
const recent = Date.now() - usage.lastAccessed <= recencyThreshold;
|
|
27
|
+
|
|
28
|
+
return frequent || recent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getEffectiveNetworkQuality(effectiveType, configNetwork = 'auto') {
|
|
32
|
+
if (configNetwork !== 'auto') return configNetwork;
|
|
33
|
+
if (!effectiveType) return 'fast';
|
|
34
|
+
const slowTypes = ['slow-2g', '2g', '3g'];
|
|
35
|
+
return slowTypes.includes(effectiveType) ? 'slow' : 'fast';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shouldSkipBySize(size, config = {}) {
|
|
39
|
+
const max = config.maxResourceSize ?? Infinity;
|
|
40
|
+
if (!size) return false; // unknown size -> don't skip
|
|
41
|
+
return size > max;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { isHighPriority, getEffectiveNetworkQuality, shouldSkipBySize };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const DB_NAME = "smart-offline-usage";
|
|
2
|
+
const STORE_NAME = "usage";
|
|
3
|
+
const DB_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
function openDB() {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
8
|
+
|
|
9
|
+
request.onupgradeneeded = () => {
|
|
10
|
+
const db = request.result;
|
|
11
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
12
|
+
db.createObjectStore(STORE_NAME, { keyPath: "url" });
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
request.onsuccess = () => resolve(request.result);
|
|
17
|
+
request.onerror = () => reject(request.error);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function trackUsage(url) {
|
|
22
|
+
const db = await openDB();
|
|
23
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
24
|
+
const store = tx.objectStore(STORE_NAME);
|
|
25
|
+
|
|
26
|
+
const existing = await new Promise(resolve => {
|
|
27
|
+
const req = store.get(url);
|
|
28
|
+
req.onsuccess = () => resolve(req.result);
|
|
29
|
+
req.onerror = () => resolve(null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const data = existing || { url, count: 0, lastAccessed: 0 };
|
|
33
|
+
data.count += 1;
|
|
34
|
+
data.lastAccessed = Date.now();
|
|
35
|
+
|
|
36
|
+
store.put(data);
|
|
37
|
+
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on: [push, pull_request]
|
|
4
|
-
|
|
5
|
-
jobs:
|
|
6
|
-
test:
|
|
7
|
-
runs-on: windows-latest
|
|
8
|
-
strategy:
|
|
9
|
-
matrix:
|
|
10
|
-
node-version: [18.x, 20.x]
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v4
|
|
13
|
-
- uses: actions/setup-node@v4
|
|
14
|
-
with:
|
|
15
|
-
node-version: ${{ matrix.node-version }}
|
|
16
|
-
- run: npm ci
|
|
17
|
-
- run: npm test
|
package/CHANGELOG.md
DELETED
package/demo/api/profile.json
DELETED
package/demo/index.html
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>Smart Offline SDK Demo</title>
|
|
5
|
-
</head>
|
|
6
|
-
<body>
|
|
7
|
-
<h1>Smart Offline SDK Demo</h1>
|
|
8
|
-
<p>If this page reloads offline, the SDK works.</p>
|
|
9
|
-
|
|
10
|
-
<script type="module">
|
|
11
|
-
import { SmartOffline } from "../src/index.js";
|
|
12
|
-
|
|
13
|
-
SmartOffline.init({
|
|
14
|
-
pages: ["/demo/index.html","/demo/api/profile.json"],
|
|
15
|
-
apis: ["https://jsonplaceholder.typicode.com/posts"],
|
|
16
|
-
debug: true
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
fetch("https://jsonplaceholder.typicode.com/posts")
|
|
20
|
-
.then(res => res.json())
|
|
21
|
-
.then(data => {
|
|
22
|
-
document.body.innerHTML += `<pre>${data}</pre>`;
|
|
23
|
-
})
|
|
24
|
-
.catch(err => {
|
|
25
|
-
document.body.innerHTML += "<p>API failed</p>";
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
</body>
|
|
32
|
-
</html>
|
package/examples/example.js
DELETED