Migration Approach: Parallel Development (Swift app continues in production) Goal: Modern TypeScript web application inspired by proven Swift architecture Timeline: Foundation β Core β Features β Polish Last Updated: 2025-10-25
This is NOT a line-by-line translation from Swift to TypeScript. Instead, weβre creating a modern web application inspired by the proven Swift architecture, taking the best patterns and adapting them to web technologies.
Why Rewrite Instead of Port?
β Type Safety: Swiftβs strong typing β TypeScript strict mode + Zod β Clean Architecture: MVVM + Services β Similar pattern in TS β Real-Time Sync: Firestore listeners β WebSocket/SSE implementation β Offline-First: SQLite + sync β Consider similar approach β Domain Modeling: Rich enums & entities β Zod schemas + TS types
π Observable Pattern: @Published β Zustand/Jotai state management
π Dependency Injection: AppDependencies β Context providers/DI container
π Service Layer: Protocol-based β Interface-based TypeScript
π Repository Pattern: FirestoreDataStoreProtocol β Repository interfaces
π Actor Concurrency: Swift actors β Web Workers or async patterns
β Over-Complex Layout: Simplify table layout visualization β Apple-Specific: Drop Pencil drawing, native iOS features β Tight Coupling: Decouple from Koenji-specific assumptions β Manual Caching: Use modern caching strategies (TanStack Query)
Status: Architectural decisions made, documented
Completed:
Outcome: Solid foundation for development
Goal: Make architectural decisions that enable everything else
Database Strategy:
API Architecture:
Frontend Framework:
Goal: Port core business entities and validation
Tasks:
Success Criteria:
Goal: Implement data persistence and business services
Tasks:
Migration Strategy:
// Swift Pattern
class ReservationService: ObservableObject {
@Published var reservations: [Reservation] = []
private let store: FirestoreDataStoreProtocol
}
// TypeScript Equivalent
class ReservationService {
private reservationRepo: ReservationRepository;
async getReservations(date: string): Promise<Reservation[]> {
return await this.reservationRepo.findByDate(date);
}
}
Goal: Create responsive web interface matching Swift app functionality
Timeline/List Views (Priority 1):
Table Layout View (Priority 2):
Sales Interface (Priority 3):
Goal: Multi-user editing with conflict resolution
Tasks:
Goal: Make system configurable for different restaurants
Tasks:
Goal: Production-ready application
Tasks:
Swift Model (simplified):
struct Reservation: Codable, Identifiable {
let id: String
let name: String
let phone: String
let numberOfPersons: Int
let dateString: String
let startTime: String
var endTime: String?
let category: ReservationCategory
let type: ReservationType
var status: ReservationStatus
var acceptance: Acceptance
// Enums
enum ReservationCategory: String, CaseIterable {
case lunch, dinner, noBookingZone
}
enum ReservationStatus: String, CaseIterable {
case pending, confirmed, canceled, noShow, showedUp, late, toHandle, deleted, na
}
// ... more properties
}
TypeScript Migration:
// @seatkit/types/src/reservation.ts
import { z } from 'zod';
export const ReservationCategorySchema = z.enum([
'lunch',
'dinner',
'noBookingZone',
]);
export const ReservationStatusSchema = z.enum([
'pending',
'confirmed',
'canceled',
'noShow',
'showedUp',
'late',
'toHandle',
'deleted',
]);
export const ReservationTypeSchema = z.enum([
'walkIn',
'inAdvance',
'waitingList',
]);
export const AcceptanceSchema = z.enum(['confirmed', 'toConfirm', 'na']);
export const ReservationSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
phone: z.string().min(1),
numberOfPersons: z.number().int().min(1).max(20),
dateString: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
startTime: z.string().regex(/^\d{2}:\d{2}$/),
endTime: z
.string()
.regex(/^\d{2}:\d{2}$/)
.optional(),
category: ReservationCategorySchema,
type: ReservationTypeSchema,
status: ReservationStatusSchema,
acceptance: AcceptanceSchema,
// Optional fields
specialRequests: z.string().optional(),
dietaryRestrictions: z.string().optional(),
language: z.enum(['italian', 'english', 'japanese']).optional(),
colorHue: z.number().min(0).max(360).optional(),
// Metadata
createdAt: z.date(),
lastEdited: z.date(),
editedBy: z.string().optional(),
});
export type Reservation = z.infer<typeof ReservationSchema>;
export type ReservationCategory = z.infer<typeof ReservationCategorySchema>;
export type ReservationStatus = z.infer<typeof ReservationStatusSchema>;
// ... other exported types
Swift Service Pattern:
protocol FirestoreDataStoreProtocol {
func insert<T: Codable>(_ item: T) async throws
func update<T: Codable>(_ item: T) async throws
func delete<T: Codable>(_ item: T) async throws
func streamReservations() -> AsyncThrowingStream<[Reservation], Error>
}
class ReservationService: ObservableObject {
@Published var reservations: [Reservation] = []
private let dataStore: FirestoreDataStoreProtocol
func addReservation(_ reservation: Reservation) async {
do {
try await dataStore.insert(reservation)
} catch {
// Handle error
}
}
}
TypeScript Service Pattern:
// Repository interface (equivalent to Swift protocol)
interface ReservationRepository {
insert(reservation: Reservation): Promise<void>;
update(reservation: Reservation): Promise<void>;
delete(id: string): Promise<void>;
findByDate(date: string): Promise<Reservation[]>;
findById(id: string): Promise<Reservation | null>;
streamReservations(
date: string,
): AsyncGenerator<Reservation[], void, unknown>;
}
// Service implementation
class ReservationService {
constructor(private repo: ReservationRepository) {}
async addReservation(data: unknown): Promise<Reservation> {
// Validate with Zod
const reservation = ReservationSchema.parse(data);
// Business logic
await this.validateTimeSlot(reservation);
// Persist
await this.repo.insert(reservation);
return reservation;
}
private async validateTimeSlot(reservation: Reservation): Promise<void> {
// Business rule validation
const conflicts = await this.repo.findConflicts(
reservation.dateString,
reservation.startTime,
reservation.endTime,
);
if (conflicts.length > 0) {
throw new Error('Time slot conflict');
}
}
}
Swift Pattern (SwiftUI):
struct ReservationListView: View {
@ObservedObject var viewModel: ReservationService
@State private var showingAddReservation = false
var body: some View {
List(viewModel.reservations) { reservation in
ReservationRow(reservation: reservation)
}
.onAppear {
viewModel.loadReservations()
}
}
}
TypeScript/React Pattern:
// Component with hooks
function ReservationListView() {
const { data: reservations, isLoading } = useReservations();
const [showingAddReservation, setShowingAddReservation] = useState(false);
return (
<div className="space-y-4">
{reservations?.map(reservation => (
<ReservationRow
key={reservation.id}
reservation={reservation}
/>
))}
</div>
);
}
// Data fetching hook
function useReservations() {
return useQuery({
queryKey: ['reservations', selectedDate],
queryFn: () => reservationService.getReservations(selectedDate),
refetchInterval: 5000, // Real-time updates
});
}
Swift Observable Pattern:
class ReservationStore: ObservableObject {
@Published var reservations: [Reservation] = []
@Published var selectedDate = Date()
@Published var isLoading = false
}
TypeScript State Management (using Zustand):
interface ReservationStore {
reservations: Reservation[];
selectedDate: string;
isLoading: boolean;
setReservations: (reservations: Reservation[]) => void;
setSelectedDate: (date: string) => void;
setLoading: (loading: boolean) => void;
}
const useReservationStore = create<ReservationStore>(set => ({
reservations: [],
selectedDate: new Date().toISOString().split('T')[0],
isLoading: false,
setReservations: reservations => set({ reservations }),
setSelectedDate: selectedDate => set({ selectedDate }),
setLoading: isLoading => set({ isLoading }),
}));
func streamReservations() -> AsyncThrowingStream<[Reservation], Error> {
AsyncThrowingStream { continuation in
let listener = db.collection("reservations")
.addSnapshotListener { querySnapshot, error in
if let error = error {
continuation.finish(throwing: error)
return
}
let reservations = querySnapshot?.documents.compactMap { doc in
try? doc.data(as: Reservation.self)
} ?? []
continuation.yield(reservations)
}
continuation.onTermination = { _ in
listener.remove()
}
}
}
class RealtimeReservationService {
private eventSource: EventSource | null = null;
private listeners: Set<(reservations: Reservation[]) => void> = new Set();
subscribe(callback: (reservations: Reservation[]) => void): () => void {
this.listeners.add(callback);
if (!this.eventSource) {
this.startListening();
}
// Return cleanup function
return () => {
this.listeners.delete(callback);
if (this.listeners.size === 0) {
this.stopListening();
}
};
}
private startListening(): void {
this.eventSource = new EventSource('/api/reservations/stream');
this.eventSource.onmessage = event => {
const reservations = ReservationSchema.array().parse(
JSON.parse(event.data),
);
this.listeners.forEach(callback => callback(reservations));
};
}
private stopListening(): void {
this.eventSource?.close();
this.eventSource = null;
}
}
Performance Migration Strategies:
Caching Strategy:
// TanStack Query for server state caching
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30 seconds
cacheTime: 5 * 60_000, // 5 minutes
refetchOnWindowFocus: false,
},
},
});
Optimistic Updates:
const addReservationMutation = useMutation({
mutationFn: reservationService.addReservation,
onMutate: async newReservation => {
// Optimistically update UI
queryClient.setQueryData(['reservations'], old => [
...(old || []),
newReservation,
]);
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(
['reservations'],
context?.previousReservations,
);
},
});
Efficient Re-renders:
// Memoize expensive calculations
const sortedReservations = useMemo(() =>
reservations.sort((a, b) => a.startTime.localeCompare(b.startTime)),
[reservations]
);
// Prevent unnecessary re-renders
const ReservationRow = memo(({ reservation }: { reservation: Reservation }) => {
return <div>{reservation.name} - {reservation.startTime}</div>;
});
// Unit tests with Vitest
describe('ReservationService', () => {
it('should validate time slot conflicts', async () => {
const service = new ReservationService(mockRepo);
const reservation = createMockReservation();
mockRepo.findConflicts.mockResolvedValue([existingReservation]);
await expect(service.addReservation(reservation)).rejects.toThrow(
'Time slot conflict',
);
});
});
// Integration tests
describe('Reservation API', () => {
it('should create and retrieve reservations', async () => {
const response = await request(app)
.post('/api/reservations')
.send(mockReservationData)
.expect(201);
expect(response.body).toMatchObject(expectedReservation);
});
});
// E2E tests with Playwright
test('should create reservation through UI', async ({ page }) => {
await page.goto('/reservations');
await page.click('[data-testid=add-reservation]');
await page.fill('[name=guestName]', 'John Doe');
await page.click('[data-testid=save-reservation]');
await expect(page.locator('.reservation-row')).toContainText('John Doe');
});
Real-Time Synchronization:
Performance Under Load:
Complex Business Logic:
Cross-Browser Compatibility:
Mobile Experience:
TypeScript Migration:
Package Management:
This migration strategy provides a roadmap from the current Swift iOS app to a modern TypeScript web application while preserving the proven business value and expanding the potential user base.