-
Notifications
You must be signed in to change notification settings - Fork 6
feat: P0 Phase 1 — UI i18n infrastructure & realtime protocol unification #604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { | ||
| PresenceStatus, | ||
| RealtimeRecordAction, | ||
| BasePresenceSchema, | ||
| type BasePresence, | ||
| } from './realtime-shared.zod'; | ||
|
|
||
| describe('PresenceStatus (Shared)', () => { | ||
| it('should accept valid presence statuses', () => { | ||
| const statuses = ['online', 'away', 'busy', 'offline']; | ||
| statuses.forEach(status => { | ||
| expect(() => PresenceStatus.parse(status)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should reject invalid presence statuses', () => { | ||
| expect(() => PresenceStatus.parse('idle')).toThrow(); | ||
| expect(() => PresenceStatus.parse('dnd')).toThrow(); | ||
| expect(() => PresenceStatus.parse('')).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('RealtimeRecordAction (Shared)', () => { | ||
| it('should accept valid record actions', () => { | ||
| const actions = ['created', 'updated', 'deleted']; | ||
| actions.forEach(action => { | ||
| expect(() => RealtimeRecordAction.parse(action)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should reject invalid record actions', () => { | ||
| expect(() => RealtimeRecordAction.parse('inserted')).toThrow(); | ||
| expect(() => RealtimeRecordAction.parse('modified')).toThrow(); | ||
| expect(() => RealtimeRecordAction.parse('')).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('BasePresenceSchema (Shared)', () => { | ||
| it('should accept valid minimal presence', () => { | ||
| const presence: BasePresence = { | ||
| userId: 'user-123', | ||
| status: 'online', | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| }; | ||
|
|
||
| const result = BasePresenceSchema.parse(presence); | ||
| expect(result.userId).toBe('user-123'); | ||
| expect(result.status).toBe('online'); | ||
| expect(result.metadata).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should accept presence with metadata', () => { | ||
| const presence = { | ||
| userId: 'user-456', | ||
| status: 'away', | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| metadata: { | ||
| currentPage: '/dashboard', | ||
| customStatus: 'In a meeting', | ||
| }, | ||
| }; | ||
|
|
||
| const result = BasePresenceSchema.parse(presence); | ||
| expect(result.metadata).toBeDefined(); | ||
| expect(result.metadata?.currentPage).toBe('/dashboard'); | ||
| }); | ||
|
|
||
| it('should accept all presence statuses', () => { | ||
| const statuses: Array<BasePresence['status']> = ['online', 'away', 'busy', 'offline']; | ||
|
|
||
| statuses.forEach(status => { | ||
| const presence = { | ||
| userId: 'user-789', | ||
| status, | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| }; | ||
|
|
||
| const parsed = BasePresenceSchema.parse(presence); | ||
| expect(parsed.status).toBe(status); | ||
| }); | ||
| }); | ||
|
|
||
| it('should validate datetime format', () => { | ||
| expect(() => BasePresenceSchema.parse({ | ||
| userId: 'user-123', | ||
| status: 'online', | ||
| lastSeen: 'not-a-datetime', | ||
| })).toThrow(); | ||
| }); | ||
|
|
||
| it('should reject presence without required fields', () => { | ||
| expect(() => BasePresenceSchema.parse({ | ||
| status: 'online', | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| })).toThrow(); | ||
|
|
||
| expect(() => BasePresenceSchema.parse({ | ||
| userId: 'user-123', | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| })).toThrow(); | ||
|
|
||
| expect(() => BasePresenceSchema.parse({ | ||
| userId: 'user-123', | ||
| status: 'online', | ||
| })).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Cross-protocol consistency', () => { | ||
| it('should ensure PresenceStatus is used by both realtime and websocket protocols', async () => { | ||
| // Verify that both realtime.zod and websocket.zod use the shared PresenceStatus | ||
| const realtime = await import('./realtime.zod'); | ||
| const websocket = await import('./websocket.zod'); | ||
|
|
||
| // Both should accept the same presence status values | ||
| const testStatuses = ['online', 'away', 'busy', 'offline']; | ||
|
|
||
| testStatuses.forEach(status => { | ||
| expect(() => realtime.RealtimePresenceStatus.parse(status)).not.toThrow(); | ||
| expect(() => websocket.WebSocketPresenceStatus.parse(status)).not.toThrow(); | ||
| expect(() => PresenceStatus.parse(status)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should ensure RealtimePresenceSchema and BasePresenceSchema are compatible', () => { | ||
| const testPresence = { | ||
| userId: 'user-123', | ||
| status: 'online' as const, | ||
| lastSeen: '2024-01-15T10:30:00Z', | ||
| metadata: { page: '/dashboard' }, | ||
| }; | ||
|
|
||
| // Both schemas should parse the same data | ||
| const shared = BasePresenceSchema.parse(testPresence); | ||
| expect(shared.userId).toBe('user-123'); | ||
| expect(shared.status).toBe('online'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. | ||
|
|
||
| import { z } from 'zod'; | ||
|
|
||
| /** | ||
| * Realtime Shared Protocol | ||
| * | ||
| * Shared schemas and types for real-time communication protocols. | ||
| * This module consolidates overlapping definitions between the transport-level | ||
| * realtime protocol (SSE/Polling/WebSocket) and the WebSocket collaboration protocol. | ||
| * | ||
| * **Architecture:** | ||
| * - `realtime-shared.zod.ts` — Shared base schemas (Presence, Event types) | ||
| * - `realtime.zod.ts` — Transport-layer protocol (Channel, Subscription, Transport selection) | ||
| * - `websocket.zod.ts` — Collaboration protocol (Cursor, OT editing, Advanced presence) | ||
| * | ||
| * @see realtime.zod.ts for transport-layer configuration | ||
| * @see websocket.zod.ts for collaborative editing protocol | ||
| */ | ||
|
|
||
| // ========================================== | ||
| // Shared Presence Status | ||
| // ========================================== | ||
|
|
||
| /** | ||
| * Presence Status Enum (Unified) | ||
| * | ||
| * Canonical user presence status shared across all realtime protocols. | ||
| * Used by both transport-level presence tracking (realtime.zod.ts) | ||
| * and WebSocket collaboration presence (websocket.zod.ts). | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { PresenceStatus } from './realtime-shared.zod'; | ||
| * const status = PresenceStatus.parse('online'); // ✅ | ||
| * ``` | ||
| */ | ||
| export const PresenceStatus = z.enum([ | ||
| 'online', // User is actively connected | ||
| 'away', // User is idle/inactive | ||
| 'busy', // User is busy (do not disturb) | ||
| 'offline', // User is disconnected | ||
| ]); | ||
|
|
||
| export type PresenceStatus = z.infer<typeof PresenceStatus>; | ||
|
|
||
| // ========================================== | ||
| // Shared Realtime Actions | ||
| // ========================================== | ||
|
|
||
| /** | ||
| * Realtime Record Action Enum (Unified) | ||
| * | ||
| * Canonical action types for real-time record change events. | ||
| * Shared between transport-level events and WebSocket event messages. | ||
| */ | ||
| export const RealtimeRecordAction = z.enum([ | ||
| 'created', | ||
| 'updated', | ||
| 'deleted', | ||
| ]); | ||
|
|
||
| export type RealtimeRecordAction = z.infer<typeof RealtimeRecordAction>; | ||
|
|
||
| // ========================================== | ||
| // Shared Base Presence Schema | ||
| // ========================================== | ||
|
|
||
| /** | ||
| * Base Presence Schema (Unified) | ||
| * | ||
| * Core presence fields shared across all realtime protocols. | ||
| * Transport-level (realtime.zod.ts) and collaboration-level (websocket.zod.ts) | ||
| * presence schemas extend this base with protocol-specific fields. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const presence = BasePresenceSchema.parse({ | ||
| * userId: 'user-123', | ||
| * status: 'online', | ||
| * lastSeen: '2024-01-15T10:30:00Z', | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export const BasePresenceSchema = z.object({ | ||
| /** User identifier */ | ||
| userId: z.string().describe('User identifier'), | ||
|
|
||
| /** Current presence status */ | ||
| status: PresenceStatus.describe('Current presence status'), | ||
|
|
||
| /** Last activity timestamp */ | ||
| lastSeen: z.string().datetime().describe('ISO 8601 datetime of last activity'), | ||
|
|
||
| /** Custom metadata */ | ||
| metadata: z.record(z.string(), z.unknown()).optional().describe('Custom presence data (e.g., current page, custom status)'), | ||
| }); | ||
|
|
||
| export type BasePresence = z.infer<typeof BasePresenceSchema>; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,10 @@ | |||||
|
|
||||||
| import { z } from 'zod'; | ||||||
| import { EventNameSchema } from '../shared/identifiers.zod'; | ||||||
| import { RealtimePresenceStatus } from './realtime.zod'; | ||||||
| import { PresenceStatus } from './realtime-shared.zod'; | ||||||
|
|
||||||
| // Re-export shared PresenceStatus for backward compatibility | ||||||
|
||||||
| // Re-export shared PresenceStatus for backward compatibility | |
| // Re-export canonical PresenceStatus for convenience and cross-protocol consistency |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSDoc says
RealtimePresenceSchema“extends”BasePresenceSchema, but the code now aliases it directly (export const RealtimePresenceSchema = BasePresenceSchema). Either update the comment to reflect that it’s an alias, or actually extend the base with transport-specific fields if that’s the intent.