@craftthingy-digital-innovation/cty-offline-outbox-sync-web 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 +38 -0
- package/README.md +95 -0
- package/dist/cty-offline-outbox-sync.es.js +85 -0
- package/dist/cty-offline-outbox-sync.umd.js +1 -0
- package/package.json +35 -0
- package/src/index.js +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Public-Source Corporate Royalty License (PSCRL)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alif Nurhidayat (alifnurhidayatwork@gmail.com)
|
|
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 for non-commercial, personal, educational,
|
|
8
|
+
research, and open-source public use, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
1. NON-COMMERCIAL & PUBLIC USE
|
|
11
|
+
The Software is completely free to use, modify, and distribute for individuals,
|
|
12
|
+
educational institutions, personal research, testing, and non-commercial open-source
|
|
13
|
+
projects.
|
|
14
|
+
|
|
15
|
+
2. CORPORATE & COMMERCIAL BUSINESS USE
|
|
16
|
+
Any use of the Software by for-profit companies, corporations, commercial applications,
|
|
17
|
+
SaaS (Software as a Service) platforms, or for any revenue-generating activity
|
|
18
|
+
requires a commercial licensing agreement.
|
|
19
|
+
|
|
20
|
+
By using this Software for commercial or corporate purposes, you agree to:
|
|
21
|
+
a. Pay a royalty fee of 1% (one percent) of the gross monthly revenue generated by
|
|
22
|
+
any product, service, or platform that incorporates or uses this Software.
|
|
23
|
+
b. Startups or businesses with gross annual revenues of less than $10,000 USD (or equivalent)
|
|
24
|
+
are exempt from royalties until they exceed this threshold.
|
|
25
|
+
c. Provide quarterly reporting of gross revenues generated to the copyright holder.
|
|
26
|
+
|
|
27
|
+
3. CONTACT & CUSTOM LICENSING
|
|
28
|
+
For royalty payments, reporting, custom enterprise flat-rate licenses, or general inquiries,
|
|
29
|
+
contact the copyright holder directly at:
|
|
30
|
+
alifnurhidayatwork@gmail.com
|
|
31
|
+
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
35
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
36
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
37
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
38
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @craftthingy-digital-innovation/cty-offline-outbox-sync-web
|
|
2
|
+
|
|
3
|
+
Bilingual documentation: [Bahasa Indonesia](#bahasa-indonesia) | [English](#english)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Bahasa Indonesia
|
|
8
|
+
|
|
9
|
+
Library client-side Javascript untuk menangani pengiriman data otomatis (auto-save) dengan kemampuan penyimpanan lokal sementara (`localStorage` outbox) dan pencobaan ulang otomatis (`auto-retry`) berkala jika koneksi terputus.
|
|
10
|
+
|
|
11
|
+
Sangat berguna untuk aplikasi web yang membutuhkan persistensi tinggi (seperti spreadsheet, form panjang, CRM) agar terhindar dari kehilangan data saat jaringan buruk.
|
|
12
|
+
|
|
13
|
+
### Instalasi
|
|
14
|
+
```bash
|
|
15
|
+
npm install @craftthingy-digital-innovation/cty-offline-outbox-sync-web
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Cara Penggunaan
|
|
19
|
+
```javascript
|
|
20
|
+
import { OfflineOutbox } from '@craftthingy-digital-innovation/cty-offline-outbox-sync-web';
|
|
21
|
+
|
|
22
|
+
const outbox = new OfflineOutbox({
|
|
23
|
+
storageKey: 'paspor_offline_queue', // Kunci localStorage
|
|
24
|
+
retryInterval: 6000, // Waktu tunggu coba kembali (6 detik)
|
|
25
|
+
onSync: async (item) => {
|
|
26
|
+
// Jalankan pengiriman data asinkron Anda ke server
|
|
27
|
+
const response = await fetch('/api/save', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(item.postData)
|
|
31
|
+
});
|
|
32
|
+
return await response.json();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Masukkan perubahan data ke outbox (akan di-merge otomatis jika UUID sama)
|
|
37
|
+
outbox.enqueue('row-uuid-123', { nama_pemohon: 'Alif Nurhidayat' });
|
|
38
|
+
|
|
39
|
+
// Dengarkan perubahan status sinkronisasi untuk update UI badge
|
|
40
|
+
outbox.on('status-change', ({ pendingCount, online }) => {
|
|
41
|
+
if (!online) {
|
|
42
|
+
updateStatusUI(`Koneksi terputus. Menyimpan lokal... (${pendingCount} tertunda)`);
|
|
43
|
+
} else if (pendingCount > 0) {
|
|
44
|
+
updateStatusUI(`Menyinkronkan... (${pendingCount} perubahan)`);
|
|
45
|
+
} else {
|
|
46
|
+
updateStatusUI('Semua data tersimpan otomatis');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## English
|
|
54
|
+
|
|
55
|
+
A client-side JavaScript library to handle automatic form submissions (auto-save) with local cache fallback (`localStorage` outbox) and periodic auto-retry capabilities when connections are disrupted.
|
|
56
|
+
|
|
57
|
+
Extremely useful for sheets, databases, long forms, or CRMs to prevent data loss on unstable networks.
|
|
58
|
+
|
|
59
|
+
### Installation
|
|
60
|
+
```bash
|
|
61
|
+
npm install @craftthingy-digital-innovation/cty-offline-outbox-sync-web
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Usage
|
|
65
|
+
```javascript
|
|
66
|
+
import { OfflineOutbox } from '@craftthingy-digital-innovation/cty-offline-outbox-sync-web';
|
|
67
|
+
|
|
68
|
+
const outbox = new OfflineOutbox({
|
|
69
|
+
storageKey: 'paspor_offline_queue', // Key in localStorage
|
|
70
|
+
retryInterval: 6000, // Retry interval (6 seconds)
|
|
71
|
+
onSync: async (item) => {
|
|
72
|
+
// Execute your async API save request
|
|
73
|
+
const response = await fetch('/api/save', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify(item.postData)
|
|
77
|
+
});
|
|
78
|
+
return await response.json();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Enqueue changes (automatically merged if same UUID is modified)
|
|
83
|
+
outbox.enqueue('row-uuid-123', { nama_pemohon: 'Alif Nurhidayat' });
|
|
84
|
+
|
|
85
|
+
// Listen for status changes to update your UI indicators
|
|
86
|
+
outbox.on('status-change', ({ pendingCount, online }) => {
|
|
87
|
+
if (!online) {
|
|
88
|
+
updateStatusUI(`Connection offline. Saved locally (${pendingCount} pending)`);
|
|
89
|
+
} else if (pendingCount > 0) {
|
|
90
|
+
updateStatusUI(`Syncing... (${pendingCount} changes)`);
|
|
91
|
+
} else {
|
|
92
|
+
updateStatusUI('All data saved automatically');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
class i extends EventTarget {
|
|
2
|
+
constructor(t = {}) {
|
|
3
|
+
super(), this.storageKey = t.storageKey || "cty_offline_outbox_queue", this.retryInterval = t.retryInterval || 6e3, this.onSync = t.onSync || null, this.queue = JSON.parse(localStorage.getItem(this.storageKey) || "[]"), this.isProcessing = !1, typeof window < "u" && (window.addEventListener("online", () => {
|
|
4
|
+
this.dispatchEvent(new CustomEvent("network-status-change", { detail: { online: !0 } })), this.processQueue();
|
|
5
|
+
}), window.addEventListener("offline", () => {
|
|
6
|
+
this.dispatchEvent(new CustomEvent("network-status-change", { detail: { online: !1 } }));
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Enqueues data for submission. Deduplicates by uuid.
|
|
11
|
+
* @param {string} uuid Unique identifier for the row/entity
|
|
12
|
+
* @param {object} postData Fields and values to update
|
|
13
|
+
*/
|
|
14
|
+
enqueue(t, e) {
|
|
15
|
+
const s = this.queue.findIndex((n) => n.uuid === t);
|
|
16
|
+
s !== -1 ? (Object.assign(this.queue[s].postData, e), this.queue[s].timestamp = Date.now()) : this.queue.push({
|
|
17
|
+
uuid: t,
|
|
18
|
+
postData: e,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
attempts: 0
|
|
21
|
+
}), this.save(), this.notifyStatus(), this.processQueue();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Saves the queue to local storage
|
|
25
|
+
*/
|
|
26
|
+
save() {
|
|
27
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Dispatches status update events
|
|
31
|
+
*/
|
|
32
|
+
notifyStatus(t = !1) {
|
|
33
|
+
const e = typeof navigator < "u" ? navigator.onLine : !0;
|
|
34
|
+
this.dispatchEvent(new CustomEvent("status-change", {
|
|
35
|
+
detail: {
|
|
36
|
+
pendingCount: this.queue.length,
|
|
37
|
+
online: e && !t,
|
|
38
|
+
queue: [...this.queue]
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Processes the first item in the queue recursively
|
|
44
|
+
*/
|
|
45
|
+
async processQueue() {
|
|
46
|
+
if (this.isProcessing || this.queue.length === 0) return;
|
|
47
|
+
if (!(typeof navigator < "u" ? navigator.onLine : !0)) {
|
|
48
|
+
this.notifyStatus(!0);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.isProcessing = !0;
|
|
52
|
+
const e = this.queue[0];
|
|
53
|
+
this.dispatchEvent(new CustomEvent("sync-start", { detail: { item: e } }));
|
|
54
|
+
try {
|
|
55
|
+
if (typeof this.onSync != "function")
|
|
56
|
+
throw new Error("onSync callback is not registered or not a function.");
|
|
57
|
+
const s = await this.onSync(e);
|
|
58
|
+
this.queue.shift(), this.save(), this.dispatchEvent(new CustomEvent("sync-success", { detail: { item: e, result: s } })), this.isProcessing = !1, this.notifyStatus(), this.processQueue();
|
|
59
|
+
} catch (s) {
|
|
60
|
+
console.warn("Outbox sync failed, will retry:", s), e.attempts++, this.save(), this.dispatchEvent(new CustomEvent("sync-failure", { detail: { item: e, error: s } })), this.isProcessing = !1, this.notifyStatus(!0), setTimeout(() => this.processQueue(), this.retryInterval);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clear all pending items in the queue
|
|
65
|
+
*/
|
|
66
|
+
clear() {
|
|
67
|
+
this.queue = [], this.save(), this.notifyStatus();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get all queued items
|
|
71
|
+
*/
|
|
72
|
+
getQueue() {
|
|
73
|
+
return [...this.queue];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Callback binder helper
|
|
77
|
+
*/
|
|
78
|
+
on(t, e) {
|
|
79
|
+
this.addEventListener(t, (s) => e(s.detail));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
typeof window < "u" && (window.OfflineOutbox = i);
|
|
83
|
+
export {
|
|
84
|
+
i as OfflineOutbox
|
|
85
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(n,i){typeof exports=="object"&&typeof module<"u"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(n=typeof globalThis<"u"?globalThis:n||self,i(n.OfflineOutbox={}))})(this,function(n){"use strict";class i extends EventTarget{constructor(t={}){super(),this.storageKey=t.storageKey||"cty_offline_outbox_queue",this.retryInterval=t.retryInterval||6e3,this.onSync=t.onSync||null,this.queue=JSON.parse(localStorage.getItem(this.storageKey)||"[]"),this.isProcessing=!1,typeof window<"u"&&(window.addEventListener("online",()=>{this.dispatchEvent(new CustomEvent("network-status-change",{detail:{online:!0}})),this.processQueue()}),window.addEventListener("offline",()=>{this.dispatchEvent(new CustomEvent("network-status-change",{detail:{online:!1}}))}))}enqueue(t,e){const s=this.queue.findIndex(o=>o.uuid===t);s!==-1?(Object.assign(this.queue[s].postData,e),this.queue[s].timestamp=Date.now()):this.queue.push({uuid:t,postData:e,timestamp:Date.now(),attempts:0}),this.save(),this.notifyStatus(),this.processQueue()}save(){localStorage.setItem(this.storageKey,JSON.stringify(this.queue))}notifyStatus(t=!1){const e=typeof navigator<"u"?navigator.onLine:!0;this.dispatchEvent(new CustomEvent("status-change",{detail:{pendingCount:this.queue.length,online:e&&!t,queue:[...this.queue]}}))}async processQueue(){if(this.isProcessing||this.queue.length===0)return;if(!(typeof navigator<"u"?navigator.onLine:!0)){this.notifyStatus(!0);return}this.isProcessing=!0;const e=this.queue[0];this.dispatchEvent(new CustomEvent("sync-start",{detail:{item:e}}));try{if(typeof this.onSync!="function")throw new Error("onSync callback is not registered or not a function.");const s=await this.onSync(e);this.queue.shift(),this.save(),this.dispatchEvent(new CustomEvent("sync-success",{detail:{item:e,result:s}})),this.isProcessing=!1,this.notifyStatus(),this.processQueue()}catch(s){console.warn("Outbox sync failed, will retry:",s),e.attempts++,this.save(),this.dispatchEvent(new CustomEvent("sync-failure",{detail:{item:e,error:s}})),this.isProcessing=!1,this.notifyStatus(!0),setTimeout(()=>this.processQueue(),this.retryInterval)}}clear(){this.queue=[],this.save(),this.notifyStatus()}getQueue(){return[...this.queue]}on(t,e){this.addEventListener(t,s=>e(s.detail))}}typeof window<"u"&&(window.OfflineOutbox=i),n.OfflineOutbox=i,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@craftthingy-digital-innovation/cty-offline-outbox-sync-web",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/CraftThingy-Digital-Innovation/cty-offline-outbox-sync-web.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/CraftThingy-Digital-Innovation/cty-offline-outbox-sync-web/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/CraftThingy-Digital-Innovation/cty-offline-outbox-sync-web#readme",
|
|
13
|
+
"main": "./dist/cty-offline-outbox-sync.umd.js",
|
|
14
|
+
"module": "./dist/cty-offline-outbox-sync.es.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/cty-offline-outbox-sync.es.js",
|
|
18
|
+
"require": "./dist/cty-offline-outbox-sync.umd.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "vite build"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"vite": "^5.2.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cty-offline-outbox-sync-web
|
|
3
|
+
* Offline Outbox Auto-Retry Manager
|
|
4
|
+
* Public-Source Corporate Royalty License (PSCRL)
|
|
5
|
+
* Copyright (c) 2026 CraftThingy Digital Innovation & Alif Nurhidayat
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class OfflineOutbox extends EventTarget {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.storageKey = options.storageKey || 'cty_offline_outbox_queue';
|
|
12
|
+
this.retryInterval = options.retryInterval || 6000;
|
|
13
|
+
this.onSync = options.onSync || null; // Async function (item) => Promise<any>
|
|
14
|
+
|
|
15
|
+
this.queue = JSON.parse(localStorage.getItem(this.storageKey) || '[]');
|
|
16
|
+
this.isProcessing = false;
|
|
17
|
+
|
|
18
|
+
// Listen to network status changes automatically
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
window.addEventListener('online', () => {
|
|
21
|
+
this.dispatchEvent(new CustomEvent('network-status-change', { detail: { online: true } }));
|
|
22
|
+
this.processQueue();
|
|
23
|
+
});
|
|
24
|
+
window.addEventListener('offline', () => {
|
|
25
|
+
this.dispatchEvent(new CustomEvent('network-status-change', { detail: { online: false } }));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enqueues data for submission. Deduplicates by uuid.
|
|
32
|
+
* @param {string} uuid Unique identifier for the row/entity
|
|
33
|
+
* @param {object} postData Fields and values to update
|
|
34
|
+
*/
|
|
35
|
+
enqueue(uuid, postData) {
|
|
36
|
+
const existingIdx = this.queue.findIndex(item => item.uuid === uuid);
|
|
37
|
+
|
|
38
|
+
if (existingIdx !== -1) {
|
|
39
|
+
// Merge updates
|
|
40
|
+
Object.assign(this.queue[existingIdx].postData, postData);
|
|
41
|
+
this.queue[existingIdx].timestamp = Date.now();
|
|
42
|
+
} else {
|
|
43
|
+
this.queue.push({
|
|
44
|
+
uuid: uuid,
|
|
45
|
+
postData: postData,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
attempts: 0
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.save();
|
|
52
|
+
this.notifyStatus();
|
|
53
|
+
this.processQueue();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Saves the queue to local storage
|
|
58
|
+
*/
|
|
59
|
+
save() {
|
|
60
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Dispatches status update events
|
|
65
|
+
*/
|
|
66
|
+
notifyStatus(isOfflineError = false) {
|
|
67
|
+
const online = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
68
|
+
this.dispatchEvent(new CustomEvent('status-change', {
|
|
69
|
+
detail: {
|
|
70
|
+
pendingCount: this.queue.length,
|
|
71
|
+
online: online && !isOfflineError,
|
|
72
|
+
queue: [...this.queue]
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Processes the first item in the queue recursively
|
|
79
|
+
*/
|
|
80
|
+
async processQueue() {
|
|
81
|
+
if (this.isProcessing || this.queue.length === 0) return;
|
|
82
|
+
|
|
83
|
+
const online = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
84
|
+
if (!online) {
|
|
85
|
+
this.notifyStatus(true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.isProcessing = true;
|
|
90
|
+
const item = this.queue[0];
|
|
91
|
+
|
|
92
|
+
this.dispatchEvent(new CustomEvent('sync-start', { detail: { item } }));
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (typeof this.onSync !== 'function') {
|
|
96
|
+
throw new Error("onSync callback is not registered or not a function.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await this.onSync(item);
|
|
100
|
+
|
|
101
|
+
// Success! Remove from outbox
|
|
102
|
+
this.queue.shift();
|
|
103
|
+
this.save();
|
|
104
|
+
|
|
105
|
+
this.dispatchEvent(new CustomEvent('sync-success', { detail: { item, result } }));
|
|
106
|
+
|
|
107
|
+
this.isProcessing = false;
|
|
108
|
+
this.notifyStatus();
|
|
109
|
+
|
|
110
|
+
// Recursively process next item
|
|
111
|
+
this.processQueue();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn("Outbox sync failed, will retry:", err);
|
|
114
|
+
item.attempts++;
|
|
115
|
+
this.save();
|
|
116
|
+
|
|
117
|
+
this.dispatchEvent(new CustomEvent('sync-failure', { detail: { item, error: err } }));
|
|
118
|
+
|
|
119
|
+
this.isProcessing = false;
|
|
120
|
+
this.notifyStatus(true);
|
|
121
|
+
|
|
122
|
+
// Retry after configured delay
|
|
123
|
+
setTimeout(() => this.processQueue(), this.retryInterval);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear all pending items in the queue
|
|
129
|
+
*/
|
|
130
|
+
clear() {
|
|
131
|
+
this.queue = [];
|
|
132
|
+
this.save();
|
|
133
|
+
this.notifyStatus();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get all queued items
|
|
138
|
+
*/
|
|
139
|
+
getQueue() {
|
|
140
|
+
return [...this.queue];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Callback binder helper
|
|
145
|
+
*/
|
|
146
|
+
on(event, callback) {
|
|
147
|
+
this.addEventListener(event, (e) => callback(e.detail));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof window !== 'undefined') {
|
|
152
|
+
window.OfflineOutbox = OfflineOutbox;
|
|
153
|
+
}
|