@craftthingy-digital-innovation/cty-smart-merge-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 +111 -0
- package/dist/cty-smart-merge-sync.es.js +56 -0
- package/dist/cty-smart-merge-sync.umd.js +1 -0
- package/package.json +35 -0
- package/src/index.js +129 -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,111 @@
|
|
|
1
|
+
# @craftthingy-digital-innovation/cty-smart-merge-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 kolaborasi sinkronisasi data real-time multi-user. Menggunakan algoritma **Smart Merge** untuk menyinkronkan data perubahan server tanpa merusak kursor ketik aktif pengguna (`document.activeElement`) dan tanpa menimpa perubahan lokal yang sedang mengantre di outbox.
|
|
10
|
+
|
|
11
|
+
Sangat cocok untuk merancang UI kolaboratif seperti Google Sheets, dashboard multi-user, atau sistem input data bersama.
|
|
12
|
+
|
|
13
|
+
### Instalasi
|
|
14
|
+
```bash
|
|
15
|
+
npm install @craftthingy-digital-innovation/cty-smart-merge-sync-web
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Cara Penggunaan
|
|
19
|
+
```javascript
|
|
20
|
+
import { SmartSyncEngine } from '@craftthingy-digital-innovation/cty-smart-merge-sync-web';
|
|
21
|
+
|
|
22
|
+
const syncEngine = new SmartSyncEngine({
|
|
23
|
+
interval: 4000, // Ambil data dari server setiap 4 detik
|
|
24
|
+
fetchData: async () => {
|
|
25
|
+
const res = await fetch('/api/entries');
|
|
26
|
+
const result = await res.json();
|
|
27
|
+
return result.data; // Mengembalikan array data terbaru
|
|
28
|
+
},
|
|
29
|
+
isPendingLocal: (uuid, field) => {
|
|
30
|
+
// Return true jika baris/kolom ini sedang memiliki antrean simpan offline
|
|
31
|
+
return myOfflineQueue.hasPending(uuid, field);
|
|
32
|
+
},
|
|
33
|
+
fields: ['no_paspor', 'nama_pemohon', 'tempat_lahir'] // Kolom yang disinkronkan
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Jalankan polling sinkronisasi
|
|
37
|
+
syncEngine.start();
|
|
38
|
+
|
|
39
|
+
syncEngine.on('sync', (remoteList) => {
|
|
40
|
+
// Gabungkan data remote ke state lokal & perbarui DOM secara aman
|
|
41
|
+
syncEngine.merge(localEntries, remoteList, {
|
|
42
|
+
onAdd: (newItem) => {
|
|
43
|
+
// Tambahkan baris baru ke tabel DOM Anda
|
|
44
|
+
appendRowToTableDOM(newItem);
|
|
45
|
+
},
|
|
46
|
+
onDelete: (uuid) => {
|
|
47
|
+
// Hapus baris dari tabel DOM Anda
|
|
48
|
+
removeRowFromTableDOM(uuid);
|
|
49
|
+
},
|
|
50
|
+
onUpdateField: (uuid, field, val) => {
|
|
51
|
+
// Perbarui nilai input kolom spesifik di DOM
|
|
52
|
+
const input = document.querySelector(`tr[data-uuid="${uuid}"] .field-${field}`);
|
|
53
|
+
if (input) input.value = val;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## English
|
|
62
|
+
|
|
63
|
+
A client-side JavaScript library to coordinate real-time multi-user data synchronization. Employs a **Smart Merge** algorithm to merge database updates without causing cursor jumps, losing user's focus (`document.activeElement`), or overwriting pending local outbox edits.
|
|
64
|
+
|
|
65
|
+
Perfect for collaborative UIs such as shared spreadsheets, multi-user dashboards, or shared forms.
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
```bash
|
|
69
|
+
npm install @craftthingy-digital-innovation/cty-smart-merge-sync-web
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Usage
|
|
73
|
+
```javascript
|
|
74
|
+
import { SmartSyncEngine } from '@craftthingy-digital-innovation/cty-smart-merge-sync-web';
|
|
75
|
+
|
|
76
|
+
const syncEngine = new SmartSyncEngine({
|
|
77
|
+
interval: 4000, // Poll server every 4 seconds
|
|
78
|
+
fetchData: async () => {
|
|
79
|
+
const res = await fetch('/api/entries');
|
|
80
|
+
const result = await res.json();
|
|
81
|
+
return result.data; // Return latest remote array
|
|
82
|
+
},
|
|
83
|
+
isPendingLocal: (uuid, field) => {
|
|
84
|
+
// Return true if this row/field has pending local saves in your outbox
|
|
85
|
+
return myOfflineQueue.hasPending(uuid, field);
|
|
86
|
+
},
|
|
87
|
+
fields: ['no_paspor', 'nama_pemohon', 'tempat_lahir'] // Fields to merge
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Start collaboration polling
|
|
91
|
+
syncEngine.start();
|
|
92
|
+
|
|
93
|
+
syncEngine.on('sync', (remoteList) => {
|
|
94
|
+
// Merge remote data into local state & DOM safely
|
|
95
|
+
syncEngine.merge(localEntries, remoteList, {
|
|
96
|
+
onAdd: (newItem) => {
|
|
97
|
+
// Add new row to your table DOM
|
|
98
|
+
appendRowToTableDOM(newItem);
|
|
99
|
+
},
|
|
100
|
+
onDelete: (uuid) => {
|
|
101
|
+
// Remove row from your table DOM
|
|
102
|
+
removeRowFromTableDOM(uuid);
|
|
103
|
+
},
|
|
104
|
+
onUpdateField: (uuid, field, val) => {
|
|
105
|
+
// Update specific input element value in the DOM
|
|
106
|
+
const input = document.querySelector(`tr[data-uuid="${uuid}"] .field-${field}`);
|
|
107
|
+
if (input) input.value = val;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class p extends EventTarget {
|
|
2
|
+
constructor(n = {}) {
|
|
3
|
+
super(), this.fetchData = n.fetchData || null, this.interval = n.interval || 4e3, this.isPendingLocal = n.isPendingLocal || (() => !1), this.fields = n.fields || [], this.timer = null;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Starts the collaboration sync polling
|
|
7
|
+
*/
|
|
8
|
+
start() {
|
|
9
|
+
this.timer || (this.timer = setInterval(async () => {
|
|
10
|
+
if (typeof this.fetchData == "function")
|
|
11
|
+
try {
|
|
12
|
+
const n = await this.fetchData();
|
|
13
|
+
this.dispatchEvent(new CustomEvent("sync", { detail: n }));
|
|
14
|
+
} catch (n) {
|
|
15
|
+
this.dispatchEvent(new CustomEvent("error", { detail: n }));
|
|
16
|
+
}
|
|
17
|
+
}, this.interval));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Stops the polling
|
|
21
|
+
*/
|
|
22
|
+
stop() {
|
|
23
|
+
this.timer && (clearInterval(this.timer), this.timer = null);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Merges remote data changes into a local data list while protecting user input focus
|
|
27
|
+
* @param {Array} localList The current local state array
|
|
28
|
+
* @param {Array} remoteList The remote state array fetched from server
|
|
29
|
+
* @param {object} callbacks Event handlers: { onAdd(item), onDelete(uuid), onUpdateField(uuid, field, val) }
|
|
30
|
+
*/
|
|
31
|
+
merge(n, o, s = {}) {
|
|
32
|
+
const a = typeof document < "u" ? document.activeElement : null, c = o.map((t) => t.uuid);
|
|
33
|
+
for (let t = n.length - 1; t >= 0; t--) {
|
|
34
|
+
const d = n[t], e = d.id !== null && d.id !== void 0, h = c.includes(d.uuid), i = this.isPendingLocal(d.uuid);
|
|
35
|
+
e && !h && !i && (n.splice(t, 1), typeof s.onDelete == "function" && s.onDelete(d.uuid));
|
|
36
|
+
}
|
|
37
|
+
return o.forEach((t) => {
|
|
38
|
+
const d = n.findIndex((e) => e.uuid === t.uuid);
|
|
39
|
+
if (d === -1)
|
|
40
|
+
n.push(t), typeof s.onAdd == "function" && s.onAdd(t, n.length);
|
|
41
|
+
else {
|
|
42
|
+
const e = n[d];
|
|
43
|
+
this.fields.forEach((i) => {
|
|
44
|
+
const u = t[i], f = a && a.classList.contains(`field-${i}`) && a.closest(`[data-uuid="${t.uuid}"]`), r = this.isPendingLocal(t.uuid, i);
|
|
45
|
+
!f && !r ? e[i] !== u && (e[i] = u, typeof s.onUpdateField == "function" && s.onUpdateField(t.uuid, i, u, t)) : e[i] = u;
|
|
46
|
+
}), ["id", "tanggal_input", "foto_ttd"].forEach((i) => {
|
|
47
|
+
t.hasOwnProperty(i) && e[i] !== t[i] && (e[i] = t[i], typeof s.onUpdateMeta == "function" && s.onUpdateMeta(t.uuid, i, t[i], t));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}), this.dispatchEvent(new CustomEvent("merged", { detail: { localList: n } })), n;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
typeof window < "u" && (window.SmartSyncEngine = p);
|
|
54
|
+
export {
|
|
55
|
+
p as SmartSyncEngine
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(u,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(u=typeof globalThis<"u"?globalThis:u||self,o(u.SmartMerge={}))})(this,function(u){"use strict";class o extends EventTarget{constructor(n={}){super(),this.fetchData=n.fetchData||null,this.interval=n.interval||4e3,this.isPendingLocal=n.isPendingLocal||(()=>!1),this.fields=n.fields||[],this.timer=null}start(){this.timer||(this.timer=setInterval(async()=>{if(typeof this.fetchData=="function")try{const n=await this.fetchData();this.dispatchEvent(new CustomEvent("sync",{detail:n}))}catch(n){this.dispatchEvent(new CustomEvent("error",{detail:n}))}},this.interval))}stop(){this.timer&&(clearInterval(this.timer),this.timer=null)}merge(n,c,s={}){const f=typeof document<"u"?document.activeElement:null,r=c.map(t=>t.uuid);for(let t=n.length-1;t>=0;t--){const d=n[t],i=d.id!==null&&d.id!==void 0,h=r.includes(d.uuid),e=this.isPendingLocal(d.uuid);i&&!h&&!e&&(n.splice(t,1),typeof s.onDelete=="function"&&s.onDelete(d.uuid))}return c.forEach(t=>{const d=n.findIndex(i=>i.uuid===t.uuid);if(d===-1)n.push(t),typeof s.onAdd=="function"&&s.onAdd(t,n.length);else{const i=n[d];this.fields.forEach(e=>{const a=t[e],p=f&&f.classList.contains(`field-${e}`)&&f.closest(`[data-uuid="${t.uuid}"]`),l=this.isPendingLocal(t.uuid,e);!p&&!l?i[e]!==a&&(i[e]=a,typeof s.onUpdateField=="function"&&s.onUpdateField(t.uuid,e,a,t)):i[e]=a}),["id","tanggal_input","foto_ttd"].forEach(e=>{t.hasOwnProperty(e)&&i[e]!==t[e]&&(i[e]=t[e],typeof s.onUpdateMeta=="function"&&s.onUpdateMeta(t.uuid,e,t[e],t))})}}),this.dispatchEvent(new CustomEvent("merged",{detail:{localList:n}})),n}}typeof window<"u"&&(window.SmartSyncEngine=o),u.SmartSyncEngine=o,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@craftthingy-digital-innovation/cty-smart-merge-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-smart-merge-sync-web.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/CraftThingy-Digital-Innovation/cty-smart-merge-sync-web/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/CraftThingy-Digital-Innovation/cty-smart-merge-sync-web#readme",
|
|
13
|
+
"main": "./dist/cty-smart-merge-sync.umd.js",
|
|
14
|
+
"module": "./dist/cty-smart-merge-sync.es.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/cty-smart-merge-sync.es.js",
|
|
18
|
+
"require": "./dist/cty-smart-merge-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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cty-smart-merge-sync-web
|
|
3
|
+
* Smart Merge Collaboration Engine
|
|
4
|
+
* Public-Source Corporate Royalty License (PSCRL)
|
|
5
|
+
* Copyright (c) 2026 CraftThingy Digital Innovation & Alif Nurhidayat
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SmartSyncEngine extends EventTarget {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.fetchData = options.fetchData || null; // Async function returning remote list
|
|
12
|
+
this.interval = options.interval || 4000;
|
|
13
|
+
this.isPendingLocal = options.isPendingLocal || (() => false); // Check if field has pending outbox updates
|
|
14
|
+
this.fields = options.fields || []; // Array of field names to merge
|
|
15
|
+
|
|
16
|
+
this.timer = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Starts the collaboration sync polling
|
|
21
|
+
*/
|
|
22
|
+
start() {
|
|
23
|
+
if (this.timer) return;
|
|
24
|
+
|
|
25
|
+
this.timer = setInterval(async () => {
|
|
26
|
+
if (typeof this.fetchData !== 'function') return;
|
|
27
|
+
try {
|
|
28
|
+
const remoteList = await this.fetchData();
|
|
29
|
+
this.dispatchEvent(new CustomEvent('sync', { detail: remoteList }));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
this.dispatchEvent(new CustomEvent('error', { detail: err }));
|
|
32
|
+
}
|
|
33
|
+
}, this.interval);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stops the polling
|
|
38
|
+
*/
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.timer) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merges remote data changes into a local data list while protecting user input focus
|
|
48
|
+
* @param {Array} localList The current local state array
|
|
49
|
+
* @param {Array} remoteList The remote state array fetched from server
|
|
50
|
+
* @param {object} callbacks Event handlers: { onAdd(item), onDelete(uuid), onUpdateField(uuid, field, val) }
|
|
51
|
+
*/
|
|
52
|
+
merge(localList, remoteList, callbacks = {}) {
|
|
53
|
+
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
|
54
|
+
const remoteUuids = remoteList.map(item => item.uuid);
|
|
55
|
+
|
|
56
|
+
// 1. Process Deletions
|
|
57
|
+
for (let i = localList.length - 1; i >= 0; i--) {
|
|
58
|
+
const local = localList[i];
|
|
59
|
+
// If it is saved on server, but not in remote list, and not pending local save, it was deleted
|
|
60
|
+
const isSaved = local.id !== null && local.id !== undefined;
|
|
61
|
+
const inRemote = remoteUuids.includes(local.uuid);
|
|
62
|
+
const hasPending = this.isPendingLocal(local.uuid);
|
|
63
|
+
|
|
64
|
+
if (isSaved && !inRemote && !hasPending) {
|
|
65
|
+
localList.splice(i, 1);
|
|
66
|
+
if (typeof callbacks.onDelete === 'function') {
|
|
67
|
+
callbacks.onDelete(local.uuid);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Process Additions and Updates
|
|
73
|
+
remoteList.forEach((remote) => {
|
|
74
|
+
const localIdx = localList.findIndex(e => e.uuid === remote.uuid);
|
|
75
|
+
|
|
76
|
+
if (localIdx === -1) {
|
|
77
|
+
// New record added by another client
|
|
78
|
+
localList.push(remote);
|
|
79
|
+
if (typeof callbacks.onAdd === 'function') {
|
|
80
|
+
callbacks.onAdd(remote, localList.length);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const local = localList[localIdx];
|
|
84
|
+
|
|
85
|
+
// Merge fields
|
|
86
|
+
this.fields.forEach((field) => {
|
|
87
|
+
const remoteVal = remote[field];
|
|
88
|
+
|
|
89
|
+
// Focus and outbox pending check
|
|
90
|
+
const isFocused = activeEl &&
|
|
91
|
+
activeEl.classList.contains(`field-${field}`) &&
|
|
92
|
+
activeEl.closest(`[data-uuid="${remote.uuid}"]`);
|
|
93
|
+
const isPending = this.isPendingLocal(remote.uuid, field);
|
|
94
|
+
|
|
95
|
+
if (!isFocused && !isPending) {
|
|
96
|
+
const oldVal = local[field];
|
|
97
|
+
if (oldVal !== remoteVal) {
|
|
98
|
+
local[field] = remoteVal;
|
|
99
|
+
if (typeof callbacks.onUpdateField === 'function') {
|
|
100
|
+
callbacks.onUpdateField(remote.uuid, field, remoteVal, remote);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// Keep remote state updated in background cache but don't overwrite user's typing
|
|
105
|
+
local[field] = remoteVal;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Merge non-input attributes (e.g. ID, created time, signature files)
|
|
110
|
+
const metaFields = ['id', 'tanggal_input', 'foto_ttd'];
|
|
111
|
+
metaFields.forEach(field => {
|
|
112
|
+
if (remote.hasOwnProperty(field) && local[field] !== remote[field]) {
|
|
113
|
+
local[field] = remote[field];
|
|
114
|
+
if (typeof callbacks.onUpdateMeta === 'function') {
|
|
115
|
+
callbacks.onUpdateMeta(remote.uuid, field, remote[field], remote);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.dispatchEvent(new CustomEvent('merged', { detail: { localList } }));
|
|
123
|
+
return localList;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof window !== 'undefined') {
|
|
128
|
+
window.SmartSyncEngine = SmartSyncEngine;
|
|
129
|
+
}
|