mobile v0.1 with deployment for ios (#191)

* stage 1: project init

* stage 2: skeleton with navigation

* step 2.5: streams list

* step 4: stream playback experience

* step 5-6: compose experience

* fix: broken record

* transcode media particles to mp4

* build: reproducible go generate

* build: rename skaffold module for particle processor worker

* infra: increase particle processor worker resources

Was dealing with OOM errors

* tweaks to mobile

* log transcode work

* view on desktop placeholder

* tweak padding

* cap video resolution to save on memory

* infra: bump memory limits as insurance

* ux improvements

* update bundle id for mobile

* config for mobile
This commit was merged in pull request #191.
This commit is contained in:
Arjun Patel
2026-04-29 17:39:11 -07:00
committed by GitHub
parent 3a11a82cd3
commit e3461dd5cd
110 changed files with 14682 additions and 22 deletions
+90
View File
@@ -0,0 +1,90 @@
import { z } from "zod";
import { appEnv } from "@/config/env";
export class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = "ApiError";
}
}
/**
* Thrown when a free-plan network attempts to create a non-container particle
* after hitting its daily message limit. Compose UI also disables triggers
* proactively via `useNetworkUsage` — this throw is a last-line defense.
*/
export class QuotaExceededError extends Error {
constructor(public readonly networkId: string) {
super("Daily message limit reached");
this.name = "QuotaExceededError";
}
}
function normalizeMessage(message: string): string {
return message.replace(/^Error:\s*/, "").trim();
}
export function toUserMessage(err: unknown): string {
if (err instanceof ApiError) {
if (err.status === 401) return "Please sign in again.";
if (err.status === 403) return "You don't have permission to do that.";
if (err.status === 404) return "Not found.";
if (err.status === 408 || err.status === 429) {
return "Please try again in a moment.";
}
if (err.status >= 500) {
return "Something went wrong on our end. Please try again.";
}
return normalizeMessage(err.message) || "Request failed.";
}
if (err instanceof z.ZodError) {
return "Received unexpected data from the server.";
}
if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
return "Network error. Check your connection.";
}
if (err instanceof Error) {
return normalizeMessage(err.message) || "Something went wrong.";
}
return "Something went wrong.";
}
type ErrorContext = Record<string, unknown>;
type ErrorSink = (err: unknown, context?: ErrorContext) => void;
// Sentry (or any observability backend) installs itself via `installErrorSinks`
// from App.tsx. Until then, logError is a dev-only console call and reportError
// always prints — no call site needs to know.
let captureSink: ErrorSink | null = null;
let breadcrumbSink: ErrorSink | null = null;
export function installErrorSinks(sinks: {
capture: ErrorSink;
breadcrumb: ErrorSink;
}): void {
captureSink = sinks.capture;
breadcrumbSink = sinks.breadcrumb;
}
/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */
export function logError(err: unknown, context?: ErrorContext): void {
if (appEnv === "dev") {
// eslint-disable-next-line no-console
console.error("[error]", err, context ?? {});
}
breadcrumbSink?.(err, context);
}
/** Unexpected failures the user may not see. Always captured. */
export function reportError(err: unknown, context?: ErrorContext): void {
// eslint-disable-next-line no-console
console.error("[error]", err, context ?? {});
captureSink?.(err, context);
}