@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 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
+ }