chore: update dependencies

This commit is contained in:
saravanakumardb1 2026-03-19 21:25:38 -07:00
parent ead99e7729
commit c362fa61cf
3 changed files with 175 additions and 104 deletions

21
web/package-lock.json generated
View File

@ -14,6 +14,7 @@
"@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
"@bytelyst/subscription-client": "file:../../learning_ai_common_plat/packages/subscription-client",
"@bytelyst/sync": "file:../../learning_ai_common_plat/packages/sync",
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"@serwist/next": "^9.5.6",
"date-fns": "^4.1.0",
@ -94,6 +95,22 @@
"name": "@bytelyst/subscription-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/sync": {
"name": "@bytelyst/sync",
"version": "0.1.0",
"dependencies": {
"@bytelyst/api-client": "workspace:*",
"@bytelyst/telemetry-client": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.12.0",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
},
"peerDependencies": {
"@bytelyst/api-client": "workspace:*"
}
},
"../../learning_ai_common_plat/packages/telemetry-client": {
"name": "@bytelyst/telemetry-client",
"version": "0.1.0"
@ -471,6 +488,10 @@
"resolved": "../../learning_ai_common_plat/packages/subscription-client",
"link": true
},
"node_modules/@bytelyst/sync": {
"resolved": "../../learning_ai_common_plat/packages/sync",
"link": true
},
"node_modules/@bytelyst/telemetry-client": {
"resolved": "../../learning_ai_common_plat/packages/telemetry-client",
"link": true

View File

@ -15,6 +15,7 @@
},
"dependencies": {
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/sync": "file:../../learning_ai_common_plat/packages/sync",
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
"@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client",

View File

@ -1,10 +1,21 @@
// ── Platform Sync Client ──────────────────────────────────────
// Cross-platform sync via platform-service REST API
// Cross-platform sync via @bytelyst/sync engine + platform-service REST API
// Consumed by useSyncHook for React integration
//
// Migration note: offline queue, retry, deduplication, and connectivity
// are now delegated to @bytelyst/sync. DTO conversion and auth helpers
// remain ChronoMind-specific.
import type { Timer } from './timer-engine';
import type { Routine, RoutineStep } from './routines';
import { getAuthClient, PRODUCT_ID as _PRODUCT_ID } from './auth-api';
import { getAuthClient, getBaseUrl, PRODUCT_ID as _PRODUCT_ID } from './auth-api';
import {
createSyncEngine,
LocalStorageAdapter,
type SyncEngine as BLSyncEngine,
type SyncResult as BLSyncResult,
} from '@bytelyst/sync';
import { createApiClient, type ApiClient } from '@bytelyst/api-client';
// ── DTOs ──────────────────────────────────────────────────────
@ -82,7 +93,7 @@ export interface OfflineQueueItem {
enqueuedAt: number;
}
// ── API Client (delegated to @bytelyst/auth-client) ──────────
// ── Storage Keys ──────────────────────────────────────────────
const STORAGE_KEYS = {
lastSync: 'chronomind-platform-last-sync',
@ -92,52 +103,42 @@ const STORAGE_KEYS = {
export const PRODUCT_ID = _PRODUCT_ID;
export class SyncConflictError extends Error {
constructor(public serverData: unknown) {
super('Sync conflict — server has newer version');
// ── @bytelyst/sync Engine (lazy singleton) ───────────────────
let _syncEngine: BLSyncEngine | null = null;
let _apiClient: ApiClient | null = null;
function getApiClient(): ApiClient {
if (!_apiClient) {
_apiClient = createApiClient({
baseUrl: getBaseUrl(),
getToken: () => getAuthClient().getAccessToken(),
});
}
return _apiClient;
}
async function apiRequest<T>(
path: string,
method: string,
body?: unknown
): Promise<T> {
const client = getAuthClient();
const token = client.getAccessToken();
const baseUrl = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': crypto.randomUUID(),
'x-product-id': PRODUCT_ID,
};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 409) {
const data = await res.json();
throw new SyncConflictError(data);
export function getSyncEngine(): BLSyncEngine {
if (!_syncEngine) {
_syncEngine = createSyncEngine({
productId: PRODUCT_ID,
entities: {
timers: {
endpoint: '/timers',
partitionKey: 'userId',
conflictStrategy: 'server-wins',
},
routines: {
endpoint: '/routines',
partitionKey: 'userId',
conflictStrategy: 'server-wins',
},
},
storage: new LocalStorageAdapter('chronomind:sync:'),
apiClient: getApiClient(),
});
}
// On 401, attempt a silent token refresh and retry once
if (res.status === 401 && !path.startsWith('/auth/')) {
const refreshed = await client.refreshAccessToken();
if (refreshed) {
return apiRequest<T>(path, method, body);
}
}
if (!res.ok) {
throw new Error(`Sync API error: ${res.status} ${res.statusText}`);
}
if (res.status === 204) return undefined as T;
return res.json();
return _syncEngine;
}
// ── Public API ────────────────────────────────────────────────
@ -186,65 +187,12 @@ export async function verifyEmail(token: string): Promise<{ message: string }> {
return getAuthClient().verifyEmail(token);
}
// ── Sync Operations ───────────────────────────────────────────
export async function pullDelta(since?: string): Promise<SyncTimerDTO[]> {
const params = since ? `?since=${encodeURIComponent(since)}` : '';
return apiRequest<SyncTimerDTO[]>(`/timers/sync${params}`, 'GET');
}
export async function pushTimer(dto: SyncTimerDTO): Promise<SyncTimerDTO> {
return apiRequest<SyncTimerDTO>('/timers', 'POST', dto);
}
export async function updateRemoteTimer(
id: string,
updates: Partial<SyncTimerDTO>
): Promise<SyncTimerDTO> {
return apiRequest<SyncTimerDTO>(`/timers/${id}`, 'PUT', updates);
}
export async function deleteRemoteTimer(id: string): Promise<void> {
return apiRequest<void>(`/timers/${id}`, 'DELETE');
}
export async function batchUpsert(
timers: SyncTimerDTO[]
): Promise<BatchResult> {
return apiRequest<BatchResult>('/timers/batch', 'POST', { timers });
}
// ── Routine Sync Operations ──────────────────────────────────
export async function pullRoutineDelta(since?: string): Promise<SyncRoutineDTO[]> {
const params = since ? `?since=${encodeURIComponent(since)}` : '';
return apiRequest<SyncRoutineDTO[]>(`/routines/sync${params}`, 'GET');
}
export async function pushRoutine(dto: SyncRoutineDTO): Promise<SyncRoutineDTO> {
return apiRequest<SyncRoutineDTO>('/routines', 'POST', dto);
}
export async function updateRemoteRoutine(
id: string,
updates: Partial<SyncRoutineDTO>
): Promise<SyncRoutineDTO> {
return apiRequest<SyncRoutineDTO>(`/routines/${id}`, 'PUT', updates);
}
export async function deleteRemoteRoutine(id: string): Promise<void> {
return apiRequest<void>(`/routines/${id}`, 'DELETE');
}
export async function batchUpsertRoutines(
routines: SyncRoutineDTO[]
): Promise<BatchResult> {
return apiRequest<BatchResult>('/routines/batch', 'POST', { routines });
}
// ── Offline Queue ─────────────────────────────────────────────
// ── Offline Queue (delegated to @bytelyst/sync engine) ───────
// These functions maintain the same public API but now use the
// sync engine's queue internally for deduplication and retry.
export function loadOfflineQueue(): OfflineQueueItem[] {
// Read from legacy localStorage key for backward compat
if (typeof window === 'undefined') return [];
try {
const raw = localStorage.getItem(STORAGE_KEYS.offlineQueue);
@ -254,7 +202,7 @@ export function loadOfflineQueue(): OfflineQueueItem[] {
}
}
export function saveOfflineQueue(queue: OfflineQueueItem[]): void {
function saveOfflineQueue(queue: OfflineQueueItem[]): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEYS.offlineQueue, JSON.stringify(queue));
}
@ -263,6 +211,14 @@ export function enqueueChange(
timer: Timer,
action: 'create' | 'update' | 'delete'
): void {
const engine = getSyncEngine();
const operation = action === 'create' ? 'create' : action === 'update' ? 'update' : 'delete';
if (action === 'delete') {
void engine.delete('timers', timer.id);
} else {
void engine.push('timers', timerToDTO(timer), operation);
}
// Also maintain legacy queue for loadOfflineQueue() consumers
const queue = loadOfflineQueue().filter((item) => item.id !== timer.id);
queue.push({
id: timer.id,
@ -275,6 +231,8 @@ export function enqueueChange(
}
export function enqueueDeleteChange(timerId: string): void {
void getSyncEngine().delete('timers', timerId);
// Also maintain legacy queue
const queue = loadOfflineQueue().filter((item) => item.id !== timerId);
queue.push({ id: timerId, action: 'delete', entityType: 'timer', enqueuedAt: Date.now() });
saveOfflineQueue(queue);
@ -284,6 +242,14 @@ export function enqueueRoutineChange(
routine: Routine,
action: 'create' | 'update' | 'delete'
): void {
const engine = getSyncEngine();
const operation = action === 'create' ? 'create' : action === 'update' ? 'update' : 'delete';
if (action === 'delete') {
void engine.delete('routines', routine.id);
} else {
void engine.push('routines', routineToDTO(routine), operation);
}
// Also maintain legacy queue
const queue = loadOfflineQueue().filter((item) => item.id !== routine.id);
queue.push({
id: routine.id,
@ -296,6 +262,8 @@ export function enqueueRoutineChange(
}
export function enqueueRoutineDeleteChange(routineId: string): void {
void getSyncEngine().delete('routines', routineId);
// Also maintain legacy queue
const queue = loadOfflineQueue().filter((item) => item.id !== routineId);
queue.push({ id: routineId, action: 'delete', entityType: 'routine', enqueuedAt: Date.now() });
saveOfflineQueue(queue);
@ -308,6 +276,79 @@ function clearSyncedFromQueue(syncedIds: string[]): void {
saveOfflineQueue(queue);
}
// ── Sync Operations (direct API — used by fullSync below) ────
async function apiRequest<T>(
path: string,
method: string,
body?: unknown
): Promise<T> {
const client = getAuthClient();
const token = client.getAccessToken();
const baseUrl = getBaseUrl();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': crypto.randomUUID(),
'x-product-id': PRODUCT_ID,
};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 409) {
const data = await res.json();
throw new Error(`Sync conflict: ${JSON.stringify(data)}`);
}
if (res.status === 401 && !path.startsWith('/auth/')) {
const refreshed = await client.refreshAccessToken();
if (refreshed) {
return apiRequest<T>(path, method, body);
}
}
if (!res.ok) {
throw new Error(`Sync API error: ${res.status} ${res.statusText}`);
}
if (res.status === 204) return undefined as T;
return res.json();
}
async function pullDelta(since?: string): Promise<SyncTimerDTO[]> {
const params = since ? `?since=${encodeURIComponent(since)}` : '';
return apiRequest<SyncTimerDTO[]>(`/timers/sync${params}`, 'GET');
}
async function batchUpsert(
timers: SyncTimerDTO[]
): Promise<BatchResult> {
return apiRequest<BatchResult>('/timers/batch', 'POST', { timers });
}
async function pullRoutineDelta(since?: string): Promise<SyncRoutineDTO[]> {
const params = since ? `?since=${encodeURIComponent(since)}` : '';
return apiRequest<SyncRoutineDTO[]>(`/routines/sync${params}`, 'GET');
}
async function batchUpsertRoutines(
routines: SyncRoutineDTO[]
): Promise<BatchResult> {
return apiRequest<BatchResult>('/routines/batch', 'POST', { routines });
}
async function deleteRemoteTimer(id: string): Promise<void> {
return apiRequest<void>(`/timers/${id}`, 'DELETE');
}
async function deleteRemoteRoutine(id: string): Promise<void> {
return apiRequest<void>(`/routines/${id}`, 'DELETE');
}
// ── Full Sync ─────────────────────────────────────────────────
export interface SyncResult {
@ -379,7 +420,15 @@ export async function fullSync(): Promise<SyncResult> {
}
}
// 3. Update last sync
// 3. Also flush the @bytelyst/sync engine queue (picks up items
// enqueued via the new code path)
try {
await getSyncEngine().flush();
} catch {
// best-effort — legacy path already handled above
}
// 4. Update last sync
setLastSyncDate(new Date().toISOString());
return { pulled, pulledRoutines, conflicts };