chore: update dependencies
This commit is contained in:
parent
ead99e7729
commit
c362fa61cf
21
web/package-lock.json
generated
21
web/package-lock.json
generated
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
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(),
|
||||
});
|
||||
|
||||
if (res.status === 409) {
|
||||
const data = await res.json();
|
||||
throw new SyncConflictError(data);
|
||||
}
|
||||
|
||||
// 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 };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user