feat(platform): wire shared platform packages into mobile + refactor web blob-client (DRY)

Phase 1 — Platform SDK integration:
- Mobile: added @bytelyst/telemetry-client, feature-flag-client, kill-switch-client, blob-client deps
- Mobile: created src/lib/platform.ts — centralized platform init (telemetry, flags, kill-switch, blob)
- Mobile: wired initPlatform() into root layout startup
- Web: refactored blob-client.ts to use shared @bytelyst/blob-client (eliminates hand-rolled SAS fetch code)
- Web: added @bytelyst/blob-client dep

DRY: Both web and mobile now use the same @bytelyst/blob-client package for blob operations.

Verification: web typecheck + mobile typecheck pass.
This commit is contained in:
saravanakumardb1 2026-03-10 19:29:00 -07:00
parent 9d392eab39
commit 878c644dd8
7 changed files with 140 additions and 33 deletions

View File

@ -1,19 +1,23 @@
{
"name": "bytelyst-agentic-notes-mobile",
"name": "@notelett/mobile",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bytelyst-agentic-notes-mobile",
"name": "@notelett/mobile",
"version": "0.1.0",
"dependencies": {
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client",
"@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
"@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
"@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client",
"@bytelyst/offline-queue": "file:../../learning_ai_common_plat/packages/offline-queue",
"@bytelyst/platform-client": "file:../../learning_ai_common_plat/packages/platform-client",
"@bytelyst/react-native-platform-sdk": "file:../../learning_ai_common_plat/packages/react-native-platform-sdk",
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"expo": "~55.0.4",
"expo-router": "~6.0.4",
"expo-status-bar": "~3.0.9",
@ -45,6 +49,10 @@
"name": "@bytelyst/auth-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/blob-client": {
"name": "@bytelyst/blob-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/design-tokens": {
"name": "@bytelyst/design-tokens",
"version": "0.1.0",
@ -52,6 +60,14 @@
"tsx": "^4.0.0"
}
},
"../../learning_ai_common_plat/packages/feature-flag-client": {
"name": "@bytelyst/feature-flag-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/kill-switch-client": {
"name": "@bytelyst/kill-switch-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/offline-queue": {
"name": "@bytelyst/offline-queue",
"version": "0.1.0"
@ -81,6 +97,10 @@
"react-native": ">=0.72.0"
}
},
"../../learning_ai_common_plat/packages/telemetry-client": {
"name": "@bytelyst/telemetry-client",
"version": "0.1.0"
},
"../learning_ai_common_plat/packages/api-client": {
"extraneous": true
},
@ -1586,10 +1606,22 @@
"resolved": "../../learning_ai_common_plat/packages/auth-client",
"link": true
},
"node_modules/@bytelyst/blob-client": {
"resolved": "../../learning_ai_common_plat/packages/blob-client",
"link": true
},
"node_modules/@bytelyst/design-tokens": {
"resolved": "../../learning_ai_common_plat/packages/design-tokens",
"link": true
},
"node_modules/@bytelyst/feature-flag-client": {
"resolved": "../../learning_ai_common_plat/packages/feature-flag-client",
"link": true
},
"node_modules/@bytelyst/kill-switch-client": {
"resolved": "../../learning_ai_common_plat/packages/kill-switch-client",
"link": true
},
"node_modules/@bytelyst/offline-queue": {
"resolved": "../../learning_ai_common_plat/packages/offline-queue",
"link": true
@ -1602,6 +1634,10 @@
"resolved": "../../learning_ai_common_plat/packages/react-native-platform-sdk",
"link": true
},
"node_modules/@bytelyst/telemetry-client": {
"resolved": "../../learning_ai_common_plat/packages/telemetry-client",
"link": true
},
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",

View File

@ -16,9 +16,13 @@
"dependencies": {
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client",
"@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
"@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
"@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client",
"@bytelyst/offline-queue": "file:../../learning_ai_common_plat/packages/offline-queue",
"@bytelyst/platform-client": "file:../../learning_ai_common_plat/packages/platform-client",
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"@bytelyst/react-native-platform-sdk": "file:../../learning_ai_common_plat/packages/react-native-platform-sdk",
"expo": "~55.0.4",
"expo-router": "~6.0.4",

View File

@ -5,6 +5,7 @@ import { useAuthStore, type AuthState } from '../store/auth-store';
import { useInboxStore, type InboxState } from '../store/inbox-store';
import { useNotesStore, type NotesState } from '../store/notes-store';
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
import { initPlatform } from '../lib/platform';
export default function RootLayout() {
const bootstrapAuth = useAuthStore((state: AuthState) => state.bootstrap);
@ -14,6 +15,7 @@ export default function RootLayout() {
useEffect(() => {
void bootstrapAuth();
void initPlatform();
void hydrateNotes();
void hydrateWorkspaces();
void hydrateInbox();

View File

@ -0,0 +1,64 @@
import { createTelemetryClient } from '@bytelyst/telemetry-client';
import { createFeatureFlagClient } from '@bytelyst/feature-flag-client';
import { createKillSwitchClient } from '@bytelyst/kill-switch-client';
import { createBlobClient } from '@bytelyst/blob-client';
import { API_CONFIG, PRODUCT_ID } from '../api/config';
import { mmkvStorage } from '../store/mmkv-storage';
function getAccessToken(): string | null {
return mmkvStorage.getItem(`${PRODUCT_ID}_access_token`);
}
export const telemetryClient = createTelemetryClient({
productId: PRODUCT_ID,
baseUrl: API_CONFIG.platformBaseUrl,
endpoint: '/telemetry/events',
platform: 'mobile',
channel: 'notelett_mobile',
transport: 'fetch',
appVersion: '0.1.0',
buildNumber: '1',
releaseChannel: 'dev',
osFamily: 'other',
});
export const featureFlagClient = createFeatureFlagClient({
baseUrl: API_CONFIG.platformBaseUrl,
productId: PRODUCT_ID,
platform: 'mobile',
pollIntervalMs: 5 * 60 * 1000,
});
export const killSwitchClient = createKillSwitchClient({
baseUrl: API_CONFIG.platformBaseUrl,
productId: PRODUCT_ID,
platform: 'mobile',
});
export const blobClient = createBlobClient({
baseUrl: API_CONFIG.platformBaseUrl,
productId: PRODUCT_ID,
getAccessToken,
});
let platformInitialized = false;
export async function initPlatform(): Promise<void> {
if (platformInitialized) return;
platformInitialized = true;
telemetryClient.init();
telemetryClient.trackEvent('info', 'app_shell', 'mobile_app_initialized');
await featureFlagClient.init().catch(() => {
// Feature flags are best-effort
});
}
export function isFeatureEnabled(key: string): boolean {
return featureFlagClient.isEnabled(key);
}
export async function checkKillSwitch(): Promise<{ disabled: boolean; message: string | null }> {
return killSwitchClient.check();
}

9
web/package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client",
"@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
"@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client",
"@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
@ -47,6 +48,10 @@
"name": "@bytelyst/auth-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/blob-client": {
"name": "@bytelyst/blob-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/design-tokens": {
"name": "@bytelyst/design-tokens",
"version": "0.1.0",
@ -460,6 +465,10 @@
"resolved": "../../learning_ai_common_plat/packages/auth-client",
"link": true
},
"node_modules/@bytelyst/blob-client": {
"resolved": "../../learning_ai_common_plat/packages/blob-client",
"link": true
},
"node_modules/@bytelyst/design-tokens": {
"resolved": "../../learning_ai_common_plat/packages/design-tokens",
"link": true

View File

@ -15,6 +15,7 @@
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
"@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client",
"@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client",
"@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client",
"@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client",

View File

@ -1,46 +1,36 @@
"use client";
import { platformClient } from "@/lib/platform";
import { createBlobClient } from "@bytelyst/blob-client";
import { PLATFORM_SERVICE_URL, PRODUCT_ID } from "@/lib/product-config";
type BlobSasResponse = {
sasUrl: string;
};
function getAccessToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(`${PRODUCT_ID}_access_token`);
}
const blobClient = createBlobClient({
baseUrl: PLATFORM_SERVICE_URL,
productId: PRODUCT_ID,
getAccessToken,
});
export async function getArtifactReadUrl(blobPath: string): Promise<string> {
const response = await platformClient.post<BlobSasResponse>("/blob/sas", {
container: "attachments",
blobName: blobPath,
permissions: "r",
expiresInMinutes: 15,
});
return response.sasUrl;
const sas = await blobClient.getSasUrl("attachments", blobPath, "r", 15);
return sas.sasUrl;
}
export async function getArtifactUploadUrl(blobPath: string): Promise<string> {
const response = await platformClient.post<BlobSasResponse>("/blob/sas", {
container: "attachments",
blobName: blobPath,
permissions: "cw",
expiresInMinutes: 30,
});
return response.sasUrl;
const sas = await blobClient.getSasUrl("attachments", blobPath, "rw", 30);
return sas.sasUrl;
}
export async function uploadArtifact(
file: File,
blobPath: string,
): Promise<{ blobPath: string; contentType: string; sizeBytes: number }> {
const sasUrl = await getArtifactUploadUrl(blobPath);
await fetch(sasUrl, {
method: "PUT",
headers: {
"x-ms-blob-type": "BlockBlob",
"Content-Type": file.type || "application/octet-stream",
},
body: file,
await blobClient.upload("attachments", file, {
contentType: file.type || "application/octet-stream",
blobName: blobPath,
});
return {
@ -51,7 +41,8 @@ export async function uploadArtifact(
}
export async function downloadArtifact(blobPath: string): Promise<Blob> {
const sasUrl = await getArtifactReadUrl(blobPath);
const response = await fetch(sasUrl);
const response = await blobClient.download("attachments", blobPath);
return response.blob();
}
export { blobClient };