cachel 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geet Trivedi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # cachel
2
+
3
+ Offline-first asset caching for the browser, powered by IndexedDB.
4
+
5
+ Fetch remote assets once, serve them forever from local cache. Works with any framework or none at all.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install cachel
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Usage
18
+
19
+ ```javascript
20
+ import Cachel from 'cachel';
21
+
22
+ const cache = new Cachel('my-app');
23
+
24
+ // fetch and cache a remote asset
25
+ await cache.load('https://example.com/logo.png');
26
+
27
+ // retrieve cached asset as an object URL
28
+ const url = await cache.get('https://example.com/logo.png');
29
+ img.src = url; // works offline
30
+ ```
31
+
32
+ ---
33
+
34
+ ## API
35
+
36
+ ### `new Cachel(name?)`
37
+
38
+ Creates a new cachel instance. `name` is used as the IndexedDB database name, prefixed internally as `cachel:<name>`.
39
+
40
+ ```javascript
41
+ const cache = new Cachel('my-app'); // opens "cachel:my-app" in IndexedDB
42
+ ```
43
+
44
+ Defaults to `'idb'` if no name is provided.
45
+
46
+ ---
47
+
48
+ ### `cache.load(url)`
49
+
50
+ Fetches a remote asset and stores it in IndexedDB as a blob. If the asset is already cached, the network request is skipped.
51
+
52
+ ```javascript
53
+ await cache.load('https://example.com/hero.jpg');
54
+ ```
55
+
56
+ Supported content types: `image/*`, `video/*`, `audio/*`, `font/*`
57
+
58
+ Throws if the fetch fails or the content type is not supported.
59
+
60
+ ---
61
+
62
+ ### `cache.get(url)`
63
+
64
+ Retrieves a cached asset and returns it as an object URL. Returns `null` if not found.
65
+
66
+ ```javascript
67
+ const url = await cache.get('https://example.com/hero.jpg');
68
+ if (url) img.src = url;
69
+ ```
70
+
71
+ ---
72
+
73
+ ### `cache.remove(url)`
74
+
75
+ Removes a single cached asset.
76
+
77
+ ```javascript
78
+ await cache.remove('https://example.com/hero.jpg');
79
+ ```
80
+
81
+ ---
82
+
83
+ ### `cache.keys()`
84
+
85
+ Returns an array of all cached URLs.
86
+
87
+ ```javascript
88
+ const cached = await cache.keys();
89
+ console.log(cached); // ['https://example.com/logo.png', ...]
90
+ ```
91
+
92
+ ---
93
+
94
+ ### `cache.clear()`
95
+
96
+ Removes all cached assets from the store but keeps the database intact.
97
+
98
+ ```javascript
99
+ await cache.clear();
100
+ ```
101
+
102
+ ---
103
+
104
+ ### `cache.delete()`
105
+
106
+ Drops the entire IndexedDB database.
107
+
108
+ ```javascript
109
+ await cache.delete();
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Framework Usage
115
+
116
+ ### Angular
117
+
118
+ ```typescript
119
+ @Directive({ selector: '[cachel]' })
120
+ export class CachelDirective implements OnInit, OnDestroy {
121
+ @Input() cachel: string;
122
+ private objectUrl: string;
123
+ private cache = new Cachel('my-app');
124
+
125
+ async ngOnInit() {
126
+ await this.cache.load(this.cachel);
127
+ this.objectUrl = await this.cache.get(this.cachel);
128
+ this.el.nativeElement.src = this.objectUrl;
129
+ }
130
+
131
+ ngOnDestroy() {
132
+ if (this.objectUrl) URL.revokeObjectURL(this.objectUrl);
133
+ }
134
+
135
+ constructor(private el: ElementRef) {}
136
+ }
137
+ ```
138
+
139
+ ```html
140
+ <img cachel="https://example.com/logo.png" />
141
+ ```
142
+
143
+ ### React
144
+
145
+ ```javascript
146
+ const cache = new Cachel('my-app');
147
+
148
+ export function useCachedAsset(url) {
149
+ const [src, setSrc] = useState(null);
150
+
151
+ useEffect(() => {
152
+ cache.load(url).then(() => cache.get(url)).then(setSrc);
153
+ return () => { if (src) URL.revokeObjectURL(src); };
154
+ }, [url]);
155
+
156
+ return src;
157
+ }
158
+
159
+ // usage
160
+ const src = useCachedAsset('https://example.com/logo.png');
161
+ <img src={src} />
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Browser Compatibility
167
+
168
+ cachel uses IndexedDB which is supported in all modern browsers since 2015.
169
+
170
+ | Browser | Support |
171
+ |---|---|
172
+ | Chrome | 23+ |
173
+ | Firefox | 10+ |
174
+ | Safari | 10+ |
175
+ | Edge | 79+ |
176
+ | iOS Safari | 10+ |
177
+
178
+ No service worker required. Works in any browser context.
179
+
180
+ ---
181
+
182
+ ## License
183
+
184
+ MIT
package/cachel.js ADDED
@@ -0,0 +1,61 @@
1
+ import Idb from "./lib/idb.js";
2
+ import convertToBlob from './utils/blob.js';
3
+
4
+ class Cachel {
5
+
6
+ #idb;
7
+
8
+ constructor(name = 'idb'){
9
+ this.#idb = new Idb(name);
10
+ }
11
+
12
+ async load(url){
13
+ if(typeof url !== 'string') return;
14
+ url = url.trim();
15
+ if(!url) return;
16
+ if(await this.#idb.has(url)) return;
17
+ const blob = await convertToBlob(url);
18
+ if(!blob){
19
+ throw new Error(`cachel: caching failed for the requested resource - ${url}`);
20
+ }
21
+ try{
22
+ const result = await this.#idb.set(url, blob);
23
+ return result;
24
+ }
25
+ catch(err){
26
+ console.error(`cachel: error occurred while saving ${url} - ${err.message}`);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async get(url){
32
+ if(typeof url !== 'string') return;
33
+ url = url.trim();
34
+ if(!url) return;
35
+ const blob = await this.#idb.get(url);
36
+ if(!blob) return null;
37
+ return URL.createObjectURL(blob);
38
+ }
39
+
40
+ async keys() {
41
+ return this.#idb.keys();
42
+ }
43
+
44
+ async delete() {
45
+ return this.#idb.delete();
46
+ }
47
+
48
+ async remove(url){
49
+ if(typeof url !== 'string') return;
50
+ url = url.trim();
51
+ if(!url) return;
52
+ return this.#idb.remove(url);
53
+ }
54
+
55
+ async clear(){
56
+ return this.#idb.clear();
57
+ }
58
+
59
+ }
60
+
61
+ export default Cachel;
package/lib/idb.js ADDED
@@ -0,0 +1,121 @@
1
+ import promisify from "../utils/promisify.js";
2
+
3
+ class Idb {
4
+ #name = null;
5
+ #db = null;
6
+ #storeName = 'assets';
7
+ #ready = null;
8
+
9
+ constructor(idbName){
10
+ this.#name = `cachel:${idbName}`;
11
+ this.#ready = this.#init();
12
+ }
13
+
14
+ async #init(){
15
+ try {
16
+ const request = indexedDB.open(this.#name, 1);
17
+ request.onupgradeneeded = e => {
18
+ const db = e.target.result;
19
+ if(!db.objectStoreNames.contains(this.#storeName)){
20
+ db.createObjectStore(this.#storeName, { keyPath: 'url' });
21
+ }
22
+ };
23
+ this.#db = await promisify(request);
24
+ }
25
+ catch(err) {
26
+ console.warn(`cachel: failed to initialise IndexedDB - ${err.message}`);
27
+ }
28
+ }
29
+
30
+ async #isReady(){
31
+ await this.#ready;
32
+ return !!this.#db;
33
+ }
34
+
35
+ #getStoreRef(permission = 'readonly'){
36
+ return this.#db.transaction(this.#storeName, permission).objectStore(this.#storeName);
37
+ }
38
+
39
+ async set(url, blob){
40
+ if(!await this.#isReady()) return;
41
+ const store = this.#getStoreRef('readwrite');
42
+ return promisify(store.put({ url, blob }));
43
+ }
44
+
45
+ async get(url){
46
+ if(!await this.#isReady()) return null;
47
+ try{
48
+ const store = this.#getStoreRef();
49
+ const record = await promisify(store.get(url));
50
+ return record ? record.blob : null;
51
+ }
52
+ catch(err){
53
+ console.error(err.message);
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async has(url) {
59
+ if(!await this.#isReady()) return false;
60
+ try{
61
+ const store = this.#getStoreRef();
62
+ const record = await promisify(store.get(url));
63
+ return !!record;
64
+ }
65
+ catch(err){
66
+ console.error(`cachel: failed to access ${url} - ${err.message}`);
67
+ return false;
68
+ }
69
+ }
70
+
71
+ async remove(url){
72
+ if(!await this.#isReady()) return;
73
+ try{
74
+ const store = this.#getStoreRef('readwrite');
75
+ return await promisify(store.delete(url));
76
+ }
77
+ catch(err){
78
+ console.error(`cachel: failed to remove ${url} - ${err.message}`);
79
+ return null;
80
+ }
81
+ }
82
+
83
+ async clear() {
84
+ if(!await this.#isReady()) return;
85
+ try{
86
+ const store = this.#getStoreRef('readwrite');
87
+ return await promisify(store.clear());
88
+ }
89
+ catch(err){
90
+ console.error(`cachel: failed to clear - ${err.message}`);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ async delete(){
96
+ if(!await this.#isReady()) return;
97
+ try{
98
+ this.#db.close();
99
+ return await promisify(indexedDB.deleteDatabase(this.#name));
100
+ }
101
+ catch(err){
102
+ console.error(`cachel: failed to delete ${this.#name} - ${err.message}`);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ async keys(){
108
+ if(!await this.#isReady()) return [];
109
+ try{
110
+ const store = this.#getStoreRef();
111
+ return await promisify(store.getAllKeys());
112
+ }
113
+ catch(err){
114
+ console.error(`cachel: failed to get keys - ${err.message}`);
115
+ return [];
116
+ }
117
+ }
118
+
119
+ }
120
+
121
+ export default Idb;
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "cachel",
3
+ "version": "1.0.0",
4
+ "main": "cachel.js",
5
+ "files": [ "cachel.js", "lib/", "utils/", "README.md", "LICENSE" ],
6
+ "scripts": {
7
+ "dev": "live-server"
8
+ },
9
+ "keywords": ["cache", "indexeddb", "offline", "pwa", "blob", "assets"],
10
+ "author": {
11
+ "name": "Geet Trivedi",
12
+ "url": "https://www.geettrivedi.com"
13
+ },
14
+ "type": "module",
15
+ "license": "MIT",
16
+ "description": "Offline-first asset caching via IndexedDB",
17
+ "devDependencies": {
18
+ "live-server": "^1.2.2"
19
+ }
20
+ }
package/utils/blob.js ADDED
@@ -0,0 +1,25 @@
1
+ const convertToBlob = async url => {
2
+ try{
3
+ const response = await fetch(url);
4
+
5
+ if(!response.ok){
6
+ throw new Error(`cachel: failed to fetch [${url}] - ${response.status} | ${response.statusText}`);
7
+ }
8
+
9
+ const contentType = response.headers.get('content-type');
10
+ const allowedContentTypes = ['image/', 'video/', 'audio/', 'font/'];
11
+ const isContentTypeAllowed = allowedContentTypes.some(type => contentType.startsWith(type));
12
+
13
+ if(!isContentTypeAllowed){
14
+ throw new Error(`cachel: content type ${contentType} is not supported for ${url}`);
15
+ }
16
+
17
+ return await response.blob();
18
+ }
19
+ catch(err){
20
+ console.error(err.message);
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export default convertToBlob;
@@ -0,0 +1,5 @@
1
+ const promisify = (request) => new Promise((resolve, reject) => {
2
+ request.onsuccess = () => resolve(request.result);
3
+ request.onerror = () => reject(request.error);
4
+ });
5
+ export default promisify;