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/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
|
||||||
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
|
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
|
||||||
"@bytelyst/subscription-client": "file:../../learning_ai_common_plat/packages/subscription-client",
|
"@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",
|
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
|
||||||
"@serwist/next": "^9.5.6",
|
"@serwist/next": "^9.5.6",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@ -94,6 +95,22 @@
|
|||||||
"name": "@bytelyst/subscription-client",
|
"name": "@bytelyst/subscription-client",
|
||||||
"version": "0.1.0"
|
"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": {
|
"../../learning_ai_common_plat/packages/telemetry-client": {
|
||||||
"name": "@bytelyst/telemetry-client",
|
"name": "@bytelyst/telemetry-client",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
@ -471,6 +488,10 @@
|
|||||||
"resolved": "../../learning_ai_common_plat/packages/subscription-client",
|
"resolved": "../../learning_ai_common_plat/packages/subscription-client",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@bytelyst/sync": {
|
||||||
|
"resolved": "../../learning_ai_common_plat/packages/sync",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@bytelyst/telemetry-client": {
|
"node_modules/@bytelyst/telemetry-client": {
|
||||||
"resolved": "../../learning_ai_common_plat/packages/telemetry-client",
|
"resolved": "../../learning_ai_common_plat/packages/telemetry-client",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
|
"@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/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
|
||||||
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
|
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
|
||||||
"@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client",
|
"@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client",
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
// ── Platform Sync Client ──────────────────────────────────────
|
// ── 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
|
// 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 { Timer } from './timer-engine';
|
||||||
import type { Routine, RoutineStep } from './routines';
|
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 ──────────────────────────────────────────────────────
|
// ── DTOs ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -82,7 +93,7 @@ export interface OfflineQueueItem {
|
|||||||
enqueuedAt: number;
|
enqueuedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API Client (delegated to @bytelyst/auth-client) ──────────
|
// ── Storage Keys ──────────────────────────────────────────────
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
lastSync: 'chronomind-platform-last-sync',
|
lastSync: 'chronomind-platform-last-sync',
|
||||||
@ -92,52 +103,42 @@ const STORAGE_KEYS = {
|
|||||||
|
|
||||||
export const PRODUCT_ID = _PRODUCT_ID;
|
export const PRODUCT_ID = _PRODUCT_ID;
|
||||||
|
|
||||||
export class SyncConflictError extends Error {
|
// ── @bytelyst/sync Engine (lazy singleton) ───────────────────
|
||||||
constructor(public serverData: unknown) {
|
|
||||||
super('Sync conflict — server has newer version');
|
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>(
|
export function getSyncEngine(): BLSyncEngine {
|
||||||
path: string,
|
if (!_syncEngine) {
|
||||||
method: string,
|
_syncEngine = createSyncEngine({
|
||||||
body?: unknown
|
productId: PRODUCT_ID,
|
||||||
): Promise<T> {
|
entities: {
|
||||||
const client = getAuthClient();
|
timers: {
|
||||||
const token = client.getAccessToken();
|
endpoint: '/timers',
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
|
partitionKey: 'userId',
|
||||||
const headers: Record<string, string> = {
|
conflictStrategy: 'server-wins',
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
'x-request-id': crypto.randomUUID(),
|
routines: {
|
||||||
'x-product-id': PRODUCT_ID,
|
endpoint: '/routines',
|
||||||
};
|
partitionKey: 'userId',
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
conflictStrategy: 'server-wins',
|
||||||
|
},
|
||||||
const res = await fetch(`${baseUrl}${path}`, {
|
},
|
||||||
method,
|
storage: new LocalStorageAdapter('chronomind:sync:'),
|
||||||
headers,
|
apiClient: getApiClient(),
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 409) {
|
|
||||||
const data = await res.json();
|
|
||||||
throw new SyncConflictError(data);
|
|
||||||
}
|
}
|
||||||
|
return _syncEngine;
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────
|
||||||
@ -186,65 +187,12 @@ export async function verifyEmail(token: string): Promise<{ message: string }> {
|
|||||||
return getAuthClient().verifyEmail(token);
|
return getAuthClient().verifyEmail(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync Operations ───────────────────────────────────────────
|
// ── Offline Queue (delegated to @bytelyst/sync engine) ───────
|
||||||
|
// These functions maintain the same public API but now use the
|
||||||
export async function pullDelta(since?: string): Promise<SyncTimerDTO[]> {
|
// sync engine's queue internally for deduplication and retry.
|
||||||
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 ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function loadOfflineQueue(): OfflineQueueItem[] {
|
export function loadOfflineQueue(): OfflineQueueItem[] {
|
||||||
|
// Read from legacy localStorage key for backward compat
|
||||||
if (typeof window === 'undefined') return [];
|
if (typeof window === 'undefined') return [];
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEYS.offlineQueue);
|
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;
|
if (typeof window === 'undefined') return;
|
||||||
localStorage.setItem(STORAGE_KEYS.offlineQueue, JSON.stringify(queue));
|
localStorage.setItem(STORAGE_KEYS.offlineQueue, JSON.stringify(queue));
|
||||||
}
|
}
|
||||||
@ -263,6 +211,14 @@ export function enqueueChange(
|
|||||||
timer: Timer,
|
timer: Timer,
|
||||||
action: 'create' | 'update' | 'delete'
|
action: 'create' | 'update' | 'delete'
|
||||||
): void {
|
): 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);
|
const queue = loadOfflineQueue().filter((item) => item.id !== timer.id);
|
||||||
queue.push({
|
queue.push({
|
||||||
id: timer.id,
|
id: timer.id,
|
||||||
@ -275,6 +231,8 @@ export function enqueueChange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function enqueueDeleteChange(timerId: string): void {
|
export function enqueueDeleteChange(timerId: string): void {
|
||||||
|
void getSyncEngine().delete('timers', timerId);
|
||||||
|
// Also maintain legacy queue
|
||||||
const queue = loadOfflineQueue().filter((item) => item.id !== timerId);
|
const queue = loadOfflineQueue().filter((item) => item.id !== timerId);
|
||||||
queue.push({ id: timerId, action: 'delete', entityType: 'timer', enqueuedAt: Date.now() });
|
queue.push({ id: timerId, action: 'delete', entityType: 'timer', enqueuedAt: Date.now() });
|
||||||
saveOfflineQueue(queue);
|
saveOfflineQueue(queue);
|
||||||
@ -284,6 +242,14 @@ export function enqueueRoutineChange(
|
|||||||
routine: Routine,
|
routine: Routine,
|
||||||
action: 'create' | 'update' | 'delete'
|
action: 'create' | 'update' | 'delete'
|
||||||
): void {
|
): 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);
|
const queue = loadOfflineQueue().filter((item) => item.id !== routine.id);
|
||||||
queue.push({
|
queue.push({
|
||||||
id: routine.id,
|
id: routine.id,
|
||||||
@ -296,6 +262,8 @@ export function enqueueRoutineChange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function enqueueRoutineDeleteChange(routineId: string): void {
|
export function enqueueRoutineDeleteChange(routineId: string): void {
|
||||||
|
void getSyncEngine().delete('routines', routineId);
|
||||||
|
// Also maintain legacy queue
|
||||||
const queue = loadOfflineQueue().filter((item) => item.id !== routineId);
|
const queue = loadOfflineQueue().filter((item) => item.id !== routineId);
|
||||||
queue.push({ id: routineId, action: 'delete', entityType: 'routine', enqueuedAt: Date.now() });
|
queue.push({ id: routineId, action: 'delete', entityType: 'routine', enqueuedAt: Date.now() });
|
||||||
saveOfflineQueue(queue);
|
saveOfflineQueue(queue);
|
||||||
@ -308,6 +276,79 @@ function clearSyncedFromQueue(syncedIds: string[]): void {
|
|||||||
saveOfflineQueue(queue);
|
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 ─────────────────────────────────────────────────
|
// ── Full Sync ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface SyncResult {
|
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());
|
setLastSyncDate(new Date().toISOString());
|
||||||
|
|
||||||
return { pulled, pulledRoutines, conflicts };
|
return { pulled, pulledRoutines, conflicts };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user