hyperbook 0.84.3 → 0.84.4
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/dist/assets/cloud.js +191 -154
- package/dist/assets/directive-onlineide/include/online-ide-embedded.js +3 -3
- package/dist/assets/directive-sqlide/include/includeIDE.js +22 -11
- package/dist/assets/directive-sqlide/include/sql-ide-embedded.js +30 -30
- package/dist/assets/store.js +21 -147
- package/dist/index.js +28 -13
- package/package.json +4 -4
package/dist/assets/cloud.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
window.hyperbook = window.hyperbook || {};
|
|
2
1
|
window.hyperbook.cloud = (function () {
|
|
3
2
|
// ===== Cloud Integration =====
|
|
4
3
|
const AUTH_TOKEN_KEY = "hyperbook_auth_token";
|
|
5
4
|
const AUTH_USER_KEY = "hyperbook_auth_user";
|
|
5
|
+
const LAST_EVENT_ID_KEY = "hyperbook_last_event_id";
|
|
6
|
+
const EVENT_BATCH_MAX_SIZE = 512 * 1024; // 512KB
|
|
6
7
|
let isLoadingFromCloud = false;
|
|
7
8
|
let syncManager = null;
|
|
8
9
|
|
|
@@ -41,7 +42,6 @@ window.hyperbook.cloud = (function () {
|
|
|
41
42
|
this.maxWaitTime = options.maxWaitTime || 10000;
|
|
42
43
|
this.minSaveInterval = options.minSaveInterval || 1000;
|
|
43
44
|
|
|
44
|
-
this.isDirty = false;
|
|
45
45
|
this.lastSaveTime = 0;
|
|
46
46
|
this.lastChangeTime = 0;
|
|
47
47
|
this.debounceTimer = null;
|
|
@@ -50,110 +50,25 @@ window.hyperbook.cloud = (function () {
|
|
|
50
50
|
this.saveMutex = new Mutex();
|
|
51
51
|
this.retryCount = 0;
|
|
52
52
|
|
|
53
|
-
this.
|
|
53
|
+
this.pendingEvents = [];
|
|
54
|
+
this.lastEventId = parseInt(
|
|
55
|
+
localStorage.getItem(LAST_EVENT_ID_KEY) || "0",
|
|
56
|
+
10,
|
|
57
|
+
);
|
|
54
58
|
|
|
55
59
|
this.offlineQueue = [];
|
|
56
60
|
this.isOnline = navigator.onLine;
|
|
57
61
|
|
|
58
|
-
// Snapshots for external DB polling
|
|
59
|
-
this.externalDBs = options.externalDBs || [];
|
|
60
|
-
this.externalSnapshots = {};
|
|
61
|
-
this.pollInterval = options.pollInterval || 5000;
|
|
62
|
-
this.pollTimer = null;
|
|
63
|
-
|
|
64
62
|
this.setupEventListeners();
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
_snapshotExternalDB(dbName) {
|
|
71
|
-
return new Promise((resolve) => {
|
|
72
|
-
const request = indexedDB.open(dbName);
|
|
73
|
-
request.onerror = () => resolve(null);
|
|
74
|
-
request.onsuccess = (event) => {
|
|
75
|
-
const db = event.target.result;
|
|
76
|
-
const storeNames = Array.from(db.objectStoreNames);
|
|
77
|
-
if (storeNames.length === 0) {
|
|
78
|
-
db.close();
|
|
79
|
-
resolve(null);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const parts = [];
|
|
84
|
-
const tx = db.transaction(storeNames, "readonly");
|
|
85
|
-
let pending = storeNames.length;
|
|
86
|
-
storeNames.forEach((name) => {
|
|
87
|
-
const rows = [];
|
|
88
|
-
const cursorReq = tx.objectStore(name).openCursor();
|
|
89
|
-
cursorReq.onsuccess = (e) => {
|
|
90
|
-
const cursor = e.target.result;
|
|
91
|
-
if (cursor) {
|
|
92
|
-
rows.push(JSON.stringify(cursor.value));
|
|
93
|
-
cursor.continue();
|
|
94
|
-
} else {
|
|
95
|
-
parts.push(name + ":" + rows.join(","));
|
|
96
|
-
pending--;
|
|
97
|
-
if (pending === 0) {
|
|
98
|
-
db.close();
|
|
99
|
-
resolve(parts.join("|"));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
cursorReq.onerror = () => {
|
|
104
|
-
pending--;
|
|
105
|
-
if (pending === 0) {
|
|
106
|
-
db.close();
|
|
107
|
-
resolve(parts.join("|"));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Take initial snapshots and start polling external DBs for changes.
|
|
117
|
-
*/
|
|
118
|
-
async startPolling() {
|
|
119
|
-
for (const dbName of this.externalDBs) {
|
|
120
|
-
this.externalSnapshots[dbName] = await this._snapshotExternalDB(dbName);
|
|
121
|
-
}
|
|
122
|
-
this.pollTimer = setInterval(
|
|
123
|
-
() => this._pollExternalDBs(),
|
|
124
|
-
this.pollInterval,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Stop polling external DBs.
|
|
130
|
-
*/
|
|
131
|
-
stopPolling() {
|
|
132
|
-
if (this.pollTimer) {
|
|
133
|
-
clearInterval(this.pollTimer);
|
|
134
|
-
this.pollTimer = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async _pollExternalDBs() {
|
|
139
|
-
if (isLoadingFromCloud || this.saveInProgress) return;
|
|
140
|
-
for (const dbName of this.externalDBs) {
|
|
141
|
-
const current = await this._snapshotExternalDB(dbName);
|
|
142
|
-
const prev = this.externalSnapshots[dbName];
|
|
143
|
-
|
|
144
|
-
if (current !== prev) {
|
|
145
|
-
this.externalSnapshots[dbName] = current;
|
|
146
|
-
this.markDirty(dbName);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
65
|
+
get isDirty() {
|
|
66
|
+
return this.pendingEvents.length > 0;
|
|
149
67
|
}
|
|
150
68
|
|
|
151
|
-
|
|
69
|
+
addEvent(event) {
|
|
152
70
|
if (isLoadingFromCloud || isReadOnlyMode()) return;
|
|
153
|
-
|
|
154
|
-
this.dirtyStores.add(storeName);
|
|
155
|
-
}
|
|
156
|
-
this.isDirty = true;
|
|
71
|
+
this.pendingEvents.push(event);
|
|
157
72
|
this.lastChangeTime = Date.now();
|
|
158
73
|
this.updateUI("unsaved");
|
|
159
74
|
this.scheduleSave();
|
|
@@ -196,24 +111,46 @@ window.hyperbook.cloud = (function () {
|
|
|
196
111
|
this.updateUI("saving");
|
|
197
112
|
|
|
198
113
|
try {
|
|
199
|
-
|
|
114
|
+
// Take a snapshot of pending events
|
|
115
|
+
const eventsToSend = this.pendingEvents.slice();
|
|
116
|
+
const serialized = JSON.stringify(eventsToSend);
|
|
200
117
|
|
|
201
118
|
if (!this.isOnline) {
|
|
202
119
|
this.offlineQueue.push({
|
|
203
|
-
|
|
120
|
+
events: eventsToSend,
|
|
121
|
+
afterEventId: this.lastEventId,
|
|
204
122
|
timestamp: Date.now(),
|
|
205
123
|
});
|
|
124
|
+
this.pendingEvents = [];
|
|
206
125
|
this.updateUI("offline-queued");
|
|
207
126
|
return;
|
|
208
127
|
}
|
|
209
128
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
129
|
+
let result;
|
|
130
|
+
|
|
131
|
+
if (serialized.length > EVENT_BATCH_MAX_SIZE) {
|
|
132
|
+
// Large batch — fall back to full snapshot
|
|
133
|
+
result = await this.sendSnapshot();
|
|
134
|
+
} else {
|
|
135
|
+
// Normal path — send events
|
|
136
|
+
result = await this.sendEvents(eventsToSend);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.conflict) {
|
|
140
|
+
// 409 — stale state, re-fetch
|
|
141
|
+
console.log("⚠ Stale state detected, re-fetching from cloud...");
|
|
142
|
+
await loadFromCloud();
|
|
143
|
+
this.pendingEvents = [];
|
|
144
|
+
window.location.reload();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
214
147
|
|
|
215
|
-
this.
|
|
216
|
-
this.
|
|
148
|
+
this.pendingEvents = [];
|
|
149
|
+
this.lastEventId = result.lastEventId;
|
|
150
|
+
localStorage.setItem(
|
|
151
|
+
LAST_EVENT_ID_KEY,
|
|
152
|
+
String(this.lastEventId),
|
|
153
|
+
);
|
|
217
154
|
this.lastSaveTime = Date.now();
|
|
218
155
|
this.retryCount = 0;
|
|
219
156
|
this.updateUI("saved");
|
|
@@ -228,19 +165,46 @@ window.hyperbook.cloud = (function () {
|
|
|
228
165
|
});
|
|
229
166
|
}
|
|
230
167
|
|
|
231
|
-
async
|
|
168
|
+
async sendEvents(events, afterEventId) {
|
|
169
|
+
var effectiveAfterId = afterEventId !== undefined ? afterEventId : this.lastEventId;
|
|
170
|
+
try {
|
|
171
|
+
const data = await apiRequest(
|
|
172
|
+
`/api/store/${HYPERBOOK_CLOUD.id}/events`,
|
|
173
|
+
{
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
events: events,
|
|
177
|
+
afterEventId: effectiveAfterId,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
return { lastEventId: data.lastEventId, conflict: false };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.status === 409) {
|
|
184
|
+
return { conflict: true };
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async sendSnapshot() {
|
|
232
191
|
const hyperbookExport = await store.export({ prettyJson: false });
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
192
|
+
const exportData = JSON.parse(await hyperbookExport.text());
|
|
193
|
+
|
|
194
|
+
const data = await apiRequest(
|
|
195
|
+
`/api/store/${HYPERBOOK_CLOUD.id}/snapshot`,
|
|
196
|
+
{
|
|
197
|
+
method: "POST",
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
data: {
|
|
200
|
+
version: 1,
|
|
201
|
+
origin: window.location.origin,
|
|
202
|
+
data: { hyperbook: exportData },
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
242
205
|
},
|
|
243
|
-
|
|
206
|
+
);
|
|
207
|
+
return { lastEventId: data.lastEventId, conflict: false };
|
|
244
208
|
}
|
|
245
209
|
|
|
246
210
|
scheduleRetry() {
|
|
@@ -272,7 +236,6 @@ window.hyperbook.cloud = (function () {
|
|
|
272
236
|
|
|
273
237
|
window.addEventListener("beforeunload", (e) => {
|
|
274
238
|
if (this.isDirty) {
|
|
275
|
-
this.performSave("unload");
|
|
276
239
|
e.preventDefault();
|
|
277
240
|
e.returnValue = "";
|
|
278
241
|
}
|
|
@@ -289,21 +252,38 @@ window.hyperbook.cloud = (function () {
|
|
|
289
252
|
if (this.offlineQueue.length === 0) return;
|
|
290
253
|
|
|
291
254
|
console.log(`Processing ${this.offlineQueue.length} queued saves...`);
|
|
292
|
-
this.offlineQueue.sort((a, b) => a.timestamp - b.timestamp);
|
|
293
255
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
256
|
+
// Send queued events in order
|
|
257
|
+
for (let i = 0; i < this.offlineQueue.length; i++) {
|
|
258
|
+
const queued = this.offlineQueue[i];
|
|
259
|
+
try {
|
|
260
|
+
const result = await this.sendEvents(queued.events, queued.afterEventId);
|
|
261
|
+
|
|
262
|
+
if (result.conflict) {
|
|
263
|
+
// Conflict — discard remaining queue, re-fetch
|
|
264
|
+
console.log("⚠ Offline queue conflict, re-fetching...");
|
|
265
|
+
this.offlineQueue = [];
|
|
266
|
+
await loadFromCloud();
|
|
267
|
+
window.location.reload();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.lastEventId = result.lastEventId;
|
|
272
|
+
localStorage.setItem(
|
|
273
|
+
LAST_EVENT_ID_KEY,
|
|
274
|
+
String(this.lastEventId),
|
|
275
|
+
);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error("Failed to process offline queue:", error);
|
|
278
|
+
// Keep remaining items in queue
|
|
279
|
+
this.offlineQueue = this.offlineQueue.slice(i);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
306
282
|
}
|
|
283
|
+
|
|
284
|
+
this.offlineQueue = [];
|
|
285
|
+
this.lastSaveTime = Date.now();
|
|
286
|
+
console.log("✓ Offline queue processed");
|
|
307
287
|
}
|
|
308
288
|
|
|
309
289
|
clearTimers() {
|
|
@@ -327,16 +307,31 @@ window.hyperbook.cloud = (function () {
|
|
|
327
307
|
}
|
|
328
308
|
|
|
329
309
|
async manualSave() {
|
|
330
|
-
this.
|
|
310
|
+
if (this.pendingEvents.length === 0) {
|
|
311
|
+
// No pending events — send full snapshot
|
|
312
|
+
this.clearTimers();
|
|
313
|
+
try {
|
|
314
|
+
this.updateUI("saving");
|
|
315
|
+
const result = await this.sendSnapshot();
|
|
316
|
+
this.lastEventId = result.lastEventId;
|
|
317
|
+
localStorage.setItem(
|
|
318
|
+
LAST_EVENT_ID_KEY,
|
|
319
|
+
String(this.lastEventId),
|
|
320
|
+
);
|
|
321
|
+
this.updateUI("saved");
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error("Manual save failed:", error);
|
|
324
|
+
this.updateUI("error");
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
331
328
|
this.clearTimers();
|
|
332
329
|
await this.performSave("manual");
|
|
333
330
|
}
|
|
334
331
|
|
|
335
332
|
reset() {
|
|
336
|
-
this.
|
|
337
|
-
this.dirtyStores.clear();
|
|
333
|
+
this.pendingEvents = [];
|
|
338
334
|
this.clearTimers();
|
|
339
|
-
this.stopPolling();
|
|
340
335
|
this.offlineQueue = [];
|
|
341
336
|
this.retryCount = 0;
|
|
342
337
|
}
|
|
@@ -400,6 +395,7 @@ window.hyperbook.cloud = (function () {
|
|
|
400
395
|
function clearAuthToken() {
|
|
401
396
|
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
402
397
|
localStorage.removeItem(AUTH_USER_KEY);
|
|
398
|
+
localStorage.removeItem(LAST_EVENT_ID_KEY);
|
|
403
399
|
}
|
|
404
400
|
|
|
405
401
|
/**
|
|
@@ -467,12 +463,16 @@ window.hyperbook.cloud = (function () {
|
|
|
467
463
|
const data = await response.json();
|
|
468
464
|
|
|
469
465
|
if (!response.ok) {
|
|
470
|
-
|
|
466
|
+
const err = new Error(data.error || "Request failed");
|
|
467
|
+
err.status = response.status;
|
|
468
|
+
throw err;
|
|
471
469
|
}
|
|
472
470
|
|
|
473
471
|
return data;
|
|
474
472
|
} catch (error) {
|
|
475
|
-
|
|
473
|
+
if (!error.status) {
|
|
474
|
+
console.error("Cloud API error:", error);
|
|
475
|
+
}
|
|
476
476
|
throw error;
|
|
477
477
|
}
|
|
478
478
|
}
|
|
@@ -524,10 +524,9 @@ window.hyperbook.cloud = (function () {
|
|
|
524
524
|
try {
|
|
525
525
|
const data = await apiRequest(`/api/store/${HYPERBOOK_CLOUD.id}`);
|
|
526
526
|
|
|
527
|
-
if (data && data.
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const { hyperbook, sqlIde, learnJ } = storeData;
|
|
527
|
+
if (data && data.snapshot) {
|
|
528
|
+
const storeData = data.snapshot.data || data.snapshot;
|
|
529
|
+
const { hyperbook } = storeData;
|
|
531
530
|
|
|
532
531
|
if (hyperbook) {
|
|
533
532
|
const blob = new Blob([JSON.stringify(hyperbook)], {
|
|
@@ -536,12 +535,15 @@ window.hyperbook.cloud = (function () {
|
|
|
536
535
|
await store.import(blob, { clearTablesBeforeImport: true });
|
|
537
536
|
}
|
|
538
537
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
538
|
+
// Track the server's lastEventId
|
|
539
|
+
if (data.lastEventId !== undefined) {
|
|
540
|
+
localStorage.setItem(
|
|
541
|
+
LAST_EVENT_ID_KEY,
|
|
542
|
+
String(data.lastEventId),
|
|
543
|
+
);
|
|
544
|
+
if (syncManager) {
|
|
545
|
+
syncManager.lastEventId = data.lastEventId;
|
|
546
|
+
}
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
console.log("✓ Store loaded from cloud");
|
|
@@ -584,20 +586,39 @@ window.hyperbook.cloud = (function () {
|
|
|
584
586
|
debounceDelay: 2000,
|
|
585
587
|
maxWaitTime: 10000,
|
|
586
588
|
minSaveInterval: 1000,
|
|
587
|
-
externalDBs: ["SQL-IDE", "LearnJ"],
|
|
588
|
-
pollInterval: 5000,
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
-
// Hook Dexie tables to
|
|
591
|
+
// Hook Dexie tables to capture granular events (skip currentState — ephemeral UI data)
|
|
592
592
|
store.tables.forEach((table) => {
|
|
593
593
|
if (table.name === "currentState") return;
|
|
594
|
-
table.hook("creating", () => syncManager.markDirty(table.name));
|
|
595
|
-
table.hook("updating", () => syncManager.markDirty(table.name));
|
|
596
|
-
table.hook("deleting", () => syncManager.markDirty(table.name));
|
|
597
|
-
});
|
|
598
594
|
|
|
599
|
-
|
|
600
|
-
|
|
595
|
+
table.hook("creating", function (primKey, obj) {
|
|
596
|
+
syncManager.addEvent({
|
|
597
|
+
table: table.name,
|
|
598
|
+
op: "create",
|
|
599
|
+
primKey: primKey,
|
|
600
|
+
data: obj,
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
table.hook("updating", function (modifications, primKey) {
|
|
605
|
+
syncManager.addEvent({
|
|
606
|
+
table: table.name,
|
|
607
|
+
op: "update",
|
|
608
|
+
primKey: primKey,
|
|
609
|
+
data: modifications,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
table.hook("deleting", function (primKey) {
|
|
614
|
+
syncManager.addEvent({
|
|
615
|
+
table: table.name,
|
|
616
|
+
op: "delete",
|
|
617
|
+
primKey: primKey,
|
|
618
|
+
data: null,
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
});
|
|
601
622
|
}
|
|
602
623
|
}
|
|
603
624
|
|
|
@@ -727,6 +748,14 @@ window.hyperbook.cloud = (function () {
|
|
|
727
748
|
updateUserUI(user);
|
|
728
749
|
}
|
|
729
750
|
|
|
751
|
+
// Hide local export/import/reset when logged into cloud
|
|
752
|
+
if (HYPERBOOK_CLOUD && getAuthToken()) {
|
|
753
|
+
document.querySelectorAll(".export-icon, .import-icon, .reset-icon").forEach((el) => {
|
|
754
|
+
const link = el.closest("a");
|
|
755
|
+
if (link) link.style.display = "none";
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
730
759
|
// Show impersonation banner if in readonly mode
|
|
731
760
|
if (isReadOnlyMode()) {
|
|
732
761
|
const banner = document.createElement("div");
|
|
@@ -752,6 +781,14 @@ window.hyperbook.cloud = (function () {
|
|
|
752
781
|
|
|
753
782
|
return {
|
|
754
783
|
save: () => syncManager?.manualSave(),
|
|
784
|
+
sendSnapshot: async () => {
|
|
785
|
+
if (!syncManager || !HYPERBOOK_CLOUD || !getAuthToken() || isReadOnlyMode()) return;
|
|
786
|
+
syncManager.pendingEvents = [];
|
|
787
|
+
syncManager.clearTimers();
|
|
788
|
+
const result = await syncManager.sendSnapshot();
|
|
789
|
+
syncManager.lastEventId = result.lastEventId;
|
|
790
|
+
localStorage.setItem(LAST_EVENT_ID_KEY, String(result.lastEventId));
|
|
791
|
+
},
|
|
755
792
|
userToggle,
|
|
756
793
|
login,
|
|
757
794
|
logout,
|