stellar-drive 1.2.14 → 1.2.15
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/bin/install-pwa.d.ts.map +1 -1
- package/dist/bin/install-pwa.js +16 -1
- package/dist/bin/install-pwa.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +2 -0
- package/dist/data.js.map +1 -1
- package/dist/diagnostics.d.ts +9 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +374 -281
- package/dist/engine.js.map +1 -1
- package/dist/entries/types.d.ts +1 -1
- package/dist/entries/types.d.ts.map +1 -1
- package/dist/realtime.d.ts +1 -0
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +2 -1
- package/dist/realtime.js.map +1 -1
- package/dist/stores/sync.d.ts +44 -0
- package/dist/stores/sync.d.ts.map +1 -1
- package/dist/stores/sync.js +38 -2
- package/dist/stores/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SyncStatus.svelte +210 -10
package/dist/stores/sync.js
CHANGED
|
@@ -79,7 +79,8 @@ function createSyncStatusStore() {
|
|
|
79
79
|
lastSyncTime: null,
|
|
80
80
|
syncMessage: null,
|
|
81
81
|
isTabVisible: true,
|
|
82
|
-
realtimeState: 'disconnected'
|
|
82
|
+
realtimeState: 'disconnected',
|
|
83
|
+
progress: null
|
|
83
84
|
});
|
|
84
85
|
// ---------------------------------------------------------------------------
|
|
85
86
|
// Anti-Flicker Timing State
|
|
@@ -234,6 +235,40 @@ function createSyncStatusStore() {
|
|
|
234
235
|
* @see {@link RealtimeState} for possible values
|
|
235
236
|
*/
|
|
236
237
|
setRealtimeState: (realtimeState) => update((state) => ({ ...state, realtimeState })),
|
|
238
|
+
/**
|
|
239
|
+
* Begin tracking a high-volume batch push. Populates the `progress` field
|
|
240
|
+
* with an initial snapshot so the UI can render a progress bar instead of
|
|
241
|
+
* just an indeterminate spinner.
|
|
242
|
+
*
|
|
243
|
+
* @param total - Queue size at the start of the push (the denominator)
|
|
244
|
+
*/
|
|
245
|
+
startProgress: (total) => update((state) => ({
|
|
246
|
+
...state,
|
|
247
|
+
progress: { total, completed: 0, failed: 0, currentTable: null }
|
|
248
|
+
})),
|
|
249
|
+
/**
|
|
250
|
+
* Advance the batch-push progress counters. Called after each batch or
|
|
251
|
+
* individual item finishes so the UI can reflect real-time throughput.
|
|
252
|
+
*
|
|
253
|
+
* @param delta - How many items finished since the last update
|
|
254
|
+
* @param failed - How many of those items failed (defaults to 0)
|
|
255
|
+
* @param currentTable - Optional table name currently being processed
|
|
256
|
+
*/
|
|
257
|
+
advanceProgress: (delta, failed = 0, currentTable) => update((state) => {
|
|
258
|
+
if (!state.progress)
|
|
259
|
+
return state;
|
|
260
|
+
return {
|
|
261
|
+
...state,
|
|
262
|
+
progress: {
|
|
263
|
+
total: state.progress.total,
|
|
264
|
+
completed: state.progress.completed + delta,
|
|
265
|
+
failed: state.progress.failed + failed,
|
|
266
|
+
currentTable: currentTable !== undefined ? currentTable : state.progress.currentTable
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}),
|
|
270
|
+
/** Clear progress tracking — call when a large push completes (or aborts). */
|
|
271
|
+
clearProgress: () => update((state) => ({ ...state, progress: null })),
|
|
237
272
|
/**
|
|
238
273
|
* Reset the entire store to its initial default state.
|
|
239
274
|
*
|
|
@@ -258,7 +293,8 @@ function createSyncStatusStore() {
|
|
|
258
293
|
lastSyncTime: null,
|
|
259
294
|
syncMessage: null,
|
|
260
295
|
isTabVisible: true,
|
|
261
|
-
realtimeState: 'disconnected'
|
|
296
|
+
realtimeState: 'disconnected',
|
|
297
|
+
progress: null
|
|
262
298
|
});
|
|
263
299
|
}
|
|
264
300
|
};
|
package/dist/stores/sync.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/stores/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/stores/sync.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { syncStatusStore, isOnline } from 'stellar-drive/stores';
|
|
24
|
-
import type { SyncError, RealtimeState } from 'stellar-drive/types';
|
|
24
|
+
import type { SyncError, RealtimeState, SyncProgress } from 'stellar-drive/types';
|
|
25
25
|
import { runFullSync } from 'stellar-drive';
|
|
26
26
|
import type { SyncStatus } from 'stellar-drive/types';
|
|
27
27
|
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
let syncMessage = $state<string | null>(null);
|
|
50
50
|
/** Supabase Realtime connection state */
|
|
51
51
|
let realtimeState = $state<RealtimeState>('disconnected');
|
|
52
|
+
/** Live progress for a high-volume batch push (null outside of one). */
|
|
53
|
+
let progress = $state<SyncProgress | null>(null);
|
|
52
54
|
/** Whether the tooltip is visible */
|
|
53
55
|
let showTooltip = $state(false);
|
|
54
56
|
/** Whether the error-details panel inside the tooltip is expanded */
|
|
@@ -89,6 +91,7 @@
|
|
|
89
91
|
syncErrors = value.syncErrors;
|
|
90
92
|
lastSyncTime = value.lastSyncTime;
|
|
91
93
|
syncMessage = value.syncMessage;
|
|
94
|
+
progress = value.progress;
|
|
92
95
|
|
|
93
96
|
// Track realtime state changes for smooth animation
|
|
94
97
|
if (value.realtimeState !== prevRealtimeState) {
|
|
@@ -125,13 +128,20 @@
|
|
|
125
128
|
// =============================================================================
|
|
126
129
|
|
|
127
130
|
/**
|
|
128
|
-
* Trigger a full manual sync when the indicator is clicked
|
|
129
|
-
*
|
|
131
|
+
* Trigger a full manual sync when the indicator is clicked.
|
|
132
|
+
*
|
|
133
|
+
* Clickable whenever online — during `catching-up` the user can still
|
|
134
|
+
* click to queue another sync pass (it will coalesce harmlessly), which
|
|
135
|
+
* gives the user a sense of agency during long batch pushes instead of
|
|
136
|
+
* staring at a disabled button. During `syncing` we still block to avoid
|
|
137
|
+
* stacking concurrent short-cycle syncs.
|
|
130
138
|
*/
|
|
131
139
|
function handleSyncClick() {
|
|
132
|
-
if (online
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
if (!online) return;
|
|
141
|
+
// Allow clicks during catching-up so the user has an escape hatch;
|
|
142
|
+
// runFullSync is idempotent via the internal sync lock.
|
|
143
|
+
if (status === 'syncing' && !progress) return;
|
|
144
|
+
runFullSync(false);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
/**
|
|
@@ -180,17 +190,30 @@
|
|
|
180
190
|
// =============================================================================
|
|
181
191
|
|
|
182
192
|
/**
|
|
183
|
-
* Map the raw sync status + online flag into one of
|
|
184
|
-
* `offline` | `syncing` | `error` | `pending` | `synced`.
|
|
193
|
+
* Map the raw sync status + online flag into one of six display states:
|
|
194
|
+
* `offline` | `catching-up` | `syncing` | `error` | `pending` | `synced`.
|
|
195
|
+
*
|
|
196
|
+
* `catching-up` is distinct from `syncing` — it surfaces *during* a
|
|
197
|
+
* high-volume batch push where the progress monitor is populating
|
|
198
|
+
* `progress`. The UI shows a determinate ring (with percentage) instead
|
|
199
|
+
* of an opaque spinner, and the button is clickable so the user knows
|
|
200
|
+
* work is in flight AND can still force-resync if they get impatient.
|
|
185
201
|
*/
|
|
186
202
|
const displayState = $derived(() => {
|
|
187
203
|
if (!online) return 'offline';
|
|
204
|
+
if (progress && progress.total > 0) return 'catching-up';
|
|
188
205
|
if (status === 'syncing') return 'syncing';
|
|
189
206
|
if (status === 'error') return 'error';
|
|
190
207
|
if (pendingCount > 0) return 'pending';
|
|
191
208
|
return 'synced';
|
|
192
209
|
});
|
|
193
210
|
|
|
211
|
+
/** Percentage completed for the catching-up progress ring (0–100). */
|
|
212
|
+
const progressPct = $derived(() => {
|
|
213
|
+
if (!progress || progress.total === 0) return 0;
|
|
214
|
+
return Math.min(100, Math.round((progress.completed / progress.total) * 100));
|
|
215
|
+
});
|
|
216
|
+
|
|
194
217
|
// =============================================================================
|
|
195
218
|
// Effect — Transition Animation Trigger
|
|
196
219
|
// =============================================================================
|
|
@@ -243,6 +266,8 @@
|
|
|
243
266
|
switch (state) {
|
|
244
267
|
case 'offline':
|
|
245
268
|
return 'Offline';
|
|
269
|
+
case 'catching-up':
|
|
270
|
+
return 'Catching Up';
|
|
246
271
|
case 'syncing':
|
|
247
272
|
return 'Syncing';
|
|
248
273
|
case 'error':
|
|
@@ -277,6 +302,13 @@
|
|
|
277
302
|
switch (state) {
|
|
278
303
|
case 'offline':
|
|
279
304
|
return "Changes will sync when you're back online.";
|
|
305
|
+
case 'catching-up': {
|
|
306
|
+
const prog = progress;
|
|
307
|
+
if (!prog) return 'Syncing a large batch…';
|
|
308
|
+
const pct = progressPct();
|
|
309
|
+
const remaining = Math.max(0, prog.total - prog.completed);
|
|
310
|
+
return `${prog.completed.toLocaleString()} of ${prog.total.toLocaleString()} synced · ${remaining.toLocaleString()} to go (${pct}%)`;
|
|
311
|
+
}
|
|
280
312
|
case 'syncing':
|
|
281
313
|
return 'Syncing your data...';
|
|
282
314
|
case 'error':
|
|
@@ -384,17 +416,51 @@
|
|
|
384
416
|
<button
|
|
385
417
|
class="sync-indicator"
|
|
386
418
|
class:offline={displayState() === 'offline'}
|
|
419
|
+
class:catching-up={displayState() === 'catching-up'}
|
|
387
420
|
class:syncing={displayState() === 'syncing'}
|
|
388
421
|
class:error={displayState() === 'error'}
|
|
389
422
|
class:pending={displayState() === 'pending'}
|
|
390
423
|
class:synced={displayState() === 'synced'}
|
|
391
424
|
onclick={handleSyncClick}
|
|
392
|
-
disabled={!online || status === 'syncing'}
|
|
425
|
+
disabled={!online || (status === 'syncing' && !progress)}
|
|
393
426
|
aria-label={statusLabel()}
|
|
394
427
|
>
|
|
395
428
|
<!-- Animated ring around the button -->
|
|
396
429
|
<span class="indicator-ring"></span>
|
|
397
430
|
|
|
431
|
+
<!-- ═══ Catching-Up Progress Ring (determinate SVG circle) ═══ -->
|
|
432
|
+
{#if displayState() === 'catching-up'}
|
|
433
|
+
<svg
|
|
434
|
+
class="progress-ring"
|
|
435
|
+
width="36"
|
|
436
|
+
height="36"
|
|
437
|
+
viewBox="0 0 36 36"
|
|
438
|
+
aria-hidden="true"
|
|
439
|
+
>
|
|
440
|
+
<!-- Background track -->
|
|
441
|
+
<circle
|
|
442
|
+
class="progress-ring-track"
|
|
443
|
+
cx="18"
|
|
444
|
+
cy="18"
|
|
445
|
+
r="15.5"
|
|
446
|
+
fill="none"
|
|
447
|
+
stroke-width="2.25"
|
|
448
|
+
/>
|
|
449
|
+
<!-- Foreground arc — stroke-dasharray driven by progressPct -->
|
|
450
|
+
<circle
|
|
451
|
+
class="progress-ring-arc"
|
|
452
|
+
cx="18"
|
|
453
|
+
cy="18"
|
|
454
|
+
r="15.5"
|
|
455
|
+
fill="none"
|
|
456
|
+
stroke-width="2.25"
|
|
457
|
+
stroke-linecap="round"
|
|
458
|
+
style:stroke-dasharray={`${(progressPct() / 100) * 97.39} 97.39`}
|
|
459
|
+
transform="rotate(-90 18 18)"
|
|
460
|
+
/>
|
|
461
|
+
</svg>
|
|
462
|
+
{/if}
|
|
463
|
+
|
|
398
464
|
<!-- ═══ Morphing Icon Container ═══ -->
|
|
399
465
|
<span class="indicator-core" class:transitioning={isTransitioning}>
|
|
400
466
|
<!-- Offline Icon (wifi-off) -->
|
|
@@ -487,6 +553,11 @@
|
|
|
487
553
|
d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
|
|
488
554
|
/>
|
|
489
555
|
</svg>
|
|
556
|
+
|
|
557
|
+
<!-- Catching-Up Percentage (sits inside the progress ring) -->
|
|
558
|
+
{#if displayState() === 'catching-up'}
|
|
559
|
+
<span class="progress-pct">{progressPct()}</span>
|
|
560
|
+
{/if}
|
|
490
561
|
</span>
|
|
491
562
|
|
|
492
563
|
<!-- Pending count badge -->
|
|
@@ -520,12 +591,18 @@
|
|
|
520
591
|
<div
|
|
521
592
|
class="status-dot"
|
|
522
593
|
class:offline={displayState() === 'offline'}
|
|
594
|
+
class:catching-up={displayState() === 'catching-up'}
|
|
523
595
|
class:syncing={displayState() === 'syncing'}
|
|
524
596
|
class:error={displayState() === 'error'}
|
|
525
597
|
class:pending={displayState() === 'pending'}
|
|
526
598
|
class:synced={displayState() === 'synced'}
|
|
527
599
|
></div>
|
|
528
600
|
<span class="status-label">{statusLabel()}</span>
|
|
601
|
+
{#if displayState() === 'catching-up' && progress}
|
|
602
|
+
<span class="progress-count">
|
|
603
|
+
{progress.completed.toLocaleString()} / {progress.total.toLocaleString()}
|
|
604
|
+
</span>
|
|
605
|
+
{/if}
|
|
529
606
|
{#if realtimeLabel() && displayState() !== 'offline'}
|
|
530
607
|
<span
|
|
531
608
|
class="realtime-badge"
|
|
@@ -538,9 +615,17 @@
|
|
|
538
615
|
{realtimeLabel()}
|
|
539
616
|
</span>
|
|
540
617
|
{/if}
|
|
541
|
-
{#if formattedLastSync() && displayState() !== 'syncing'}
|
|
618
|
+
{#if formattedLastSync() && displayState() !== 'syncing' && displayState() !== 'catching-up'}
|
|
542
619
|
<span class="last-sync">{formattedLastSync()}</span>
|
|
543
620
|
{/if}
|
|
621
|
+
|
|
622
|
+
<!-- ── In-Tooltip Progress Bar (shown while catching up) ── -->
|
|
623
|
+
{#if displayState() === 'catching-up'}
|
|
624
|
+
<div class="catching-up-bar" role="progressbar" aria-valuenow={progressPct()} aria-valuemin="0" aria-valuemax="100">
|
|
625
|
+
<span class="catching-up-bar-fill" style:width={`${progressPct()}%`}></span>
|
|
626
|
+
<span class="catching-up-bar-shimmer"></span>
|
|
627
|
+
</div>
|
|
628
|
+
{/if}
|
|
544
629
|
</div>
|
|
545
630
|
|
|
546
631
|
<!-- ── Status Description ── -->
|
|
@@ -1034,6 +1119,115 @@
|
|
|
1034
1119
|
}
|
|
1035
1120
|
}
|
|
1036
1121
|
|
|
1122
|
+
/* ═══ Catching-Up State (high-volume batch push in flight) ═══ */
|
|
1123
|
+
|
|
1124
|
+
.sync-indicator.catching-up {
|
|
1125
|
+
/* Sapphire → primary gradient border communicates "work flowing in" */
|
|
1126
|
+
border-color: rgba(108, 92, 231, 0.5);
|
|
1127
|
+
background: linear-gradient(
|
|
1128
|
+
135deg,
|
|
1129
|
+
color-mix(in srgb, var(--color-primary) 10%, transparent) 0%,
|
|
1130
|
+
color-mix(in srgb, var(--color-primary) 3%, transparent) 100%
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.sync-indicator.catching-up:not(:disabled):hover {
|
|
1135
|
+
box-shadow: 0 0 28px var(--color-primary-glow);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/* Determinate progress ring — scaled to sit over the button's border */
|
|
1139
|
+
.progress-ring {
|
|
1140
|
+
position: absolute;
|
|
1141
|
+
inset: -4px;
|
|
1142
|
+
width: calc(100% + 8px);
|
|
1143
|
+
height: calc(100% + 8px);
|
|
1144
|
+
pointer-events: none;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.progress-ring-track {
|
|
1148
|
+
stroke: rgba(108, 92, 231, 0.15);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.progress-ring-arc {
|
|
1152
|
+
stroke: var(--color-primary-light, #a29bfe);
|
|
1153
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-primary) 55%, transparent));
|
|
1154
|
+
transition: stroke-dasharray 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* Percentage text sitting inside the ring */
|
|
1158
|
+
.progress-pct {
|
|
1159
|
+
position: absolute;
|
|
1160
|
+
inset: 0;
|
|
1161
|
+
display: flex;
|
|
1162
|
+
align-items: center;
|
|
1163
|
+
justify-content: center;
|
|
1164
|
+
font-size: 10px;
|
|
1165
|
+
font-weight: 700;
|
|
1166
|
+
font-variant-numeric: tabular-nums;
|
|
1167
|
+
color: var(--color-primary-light, #a29bfe);
|
|
1168
|
+
letter-spacing: -0.02em;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.progress-pct::after {
|
|
1172
|
+
content: '%';
|
|
1173
|
+
font-size: 7px;
|
|
1174
|
+
font-weight: 600;
|
|
1175
|
+
margin-left: 1px;
|
|
1176
|
+
opacity: 0.7;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/* Running count chip shown in tooltip header */
|
|
1180
|
+
.progress-count {
|
|
1181
|
+
margin-left: auto;
|
|
1182
|
+
font-size: 11px;
|
|
1183
|
+
font-weight: 600;
|
|
1184
|
+
font-variant-numeric: tabular-nums;
|
|
1185
|
+
color: var(--color-primary-light, #a29bfe);
|
|
1186
|
+
letter-spacing: -0.01em;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/* In-tooltip progress bar with shimmer */
|
|
1190
|
+
.catching-up-bar {
|
|
1191
|
+
position: relative;
|
|
1192
|
+
width: 100%;
|
|
1193
|
+
height: 4px;
|
|
1194
|
+
margin-top: 4px;
|
|
1195
|
+
background: rgba(108, 92, 231, 0.12);
|
|
1196
|
+
border-radius: 2px;
|
|
1197
|
+
overflow: hidden;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.catching-up-bar-fill {
|
|
1201
|
+
position: absolute;
|
|
1202
|
+
inset: 0 auto 0 0;
|
|
1203
|
+
background: linear-gradient(
|
|
1204
|
+
90deg,
|
|
1205
|
+
var(--color-primary, #6c5ce7) 0%,
|
|
1206
|
+
var(--color-primary-light, #a29bfe) 100%
|
|
1207
|
+
);
|
|
1208
|
+
border-radius: 2px;
|
|
1209
|
+
transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.catching-up-bar-shimmer {
|
|
1213
|
+
position: absolute;
|
|
1214
|
+
inset: 0;
|
|
1215
|
+
background: linear-gradient(
|
|
1216
|
+
90deg,
|
|
1217
|
+
transparent 0%,
|
|
1218
|
+
rgba(255, 255, 255, 0.22) 50%,
|
|
1219
|
+
transparent 100%
|
|
1220
|
+
);
|
|
1221
|
+
transform: translateX(-100%);
|
|
1222
|
+
animation: catching-up-shimmer 1.6s ease-in-out infinite;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
@keyframes catching-up-shimmer {
|
|
1226
|
+
to {
|
|
1227
|
+
transform: translateX(100%);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1037
1231
|
/* ═══ Pending State ═══ */
|
|
1038
1232
|
|
|
1039
1233
|
.sync-indicator.pending {
|
|
@@ -1329,6 +1523,12 @@
|
|
|
1329
1523
|
animation: dotPulse 1s ease-in-out infinite;
|
|
1330
1524
|
}
|
|
1331
1525
|
|
|
1526
|
+
.status-dot.catching-up {
|
|
1527
|
+
background: var(--color-primary-light, #a29bfe);
|
|
1528
|
+
box-shadow: 0 0 10px var(--color-primary-glow);
|
|
1529
|
+
animation: dotPulse 1.4s ease-in-out infinite;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1332
1532
|
@keyframes dotPulse {
|
|
1333
1533
|
0%,
|
|
1334
1534
|
100% {
|