Skip to content

Commit 2ec89ff

Browse files
chore(table): remove table-row sockets, both sides
Tables don't use realtime sockets in prod — strip the dead path so we stop paying the per-row HTTP forward + socket emit on every cell write. Polling on running execs already covers reconciliation. Sim side: - service.ts: drop notifyTableRowUpdated/Deleted, notifyTableDeleted, the postRealtimeBridge helper, and all callsites. - hooks/queries/tables.ts: drop the socket subscription block in useTableRows; poll-on-running stays. Remove useEffect / useSocket imports. - app/.../tables/[tableId]/hooks/use-table.ts: drop the merge-on-event useEffect and unused imports. - app/workspace/providers/socket-provider.tsx: drop joinTable/leaveTable, onTableRowUpdated/Deleted/onTableDeleted, currentTableId state, related events + types. Realtime side: - handlers/tables.ts deleted; index.ts no longer wires it. - routes/http.ts: drop /api/table-row-updated, /api/table-row-deleted, /api/table-deleted endpoints. - rooms/{memory,redis}-manager.ts: drop emitToTable, handleTableRowUpdated/ Deleted, handleTableDeleted, related imports. - rooms/types.ts: drop method declarations, TableRowUpdatedPayload type, tableRoomName helper. - middleware/permissions.ts: drop unused verifyTableAccess. Bonus from parallel work: - cell-content typewriter trigger refinement.
1 parent ba7e565 commit 2ec89ff

13 files changed

Lines changed: 94 additions & 685 deletions

File tree

apps/realtime/src/handlers/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { setupConnectionHandlers } from '@/handlers/connection'
22
import { setupOperationsHandlers } from '@/handlers/operations'
33
import { setupPresenceHandlers } from '@/handlers/presence'
44
import { setupSubblocksHandlers } from '@/handlers/subblocks'
5-
import { setupTableHandlers } from '@/handlers/tables'
65
import { setupVariablesHandlers } from '@/handlers/variables'
76
import { setupWorkflowHandlers } from '@/handlers/workflow'
87
import type { AuthenticatedSocket } from '@/middleware/auth'
@@ -14,6 +13,5 @@ export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoom
1413
setupSubblocksHandlers(socket, roomManager)
1514
setupVariablesHandlers(socket, roomManager)
1615
setupPresenceHandlers(socket, roomManager)
17-
setupTableHandlers(socket, roomManager)
1816
setupConnectionHandlers(socket, roomManager)
1917
}

apps/realtime/src/handlers/tables.ts

Lines changed: 0 additions & 73 deletions
This file was deleted.

apps/realtime/src/middleware/permissions.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -131,51 +131,3 @@ export async function verifyWorkflowAccess(
131131
return { hasAccess: false }
132132
}
133133
}
134-
135-
/**
136-
* Verify a user has read access to a table by virtue of workspace permission.
137-
* Mirrors `verifyWorkflowAccess` for the table-room socket join check.
138-
*/
139-
export async function verifyTableAccess(
140-
userId: string,
141-
tableId: string
142-
): Promise<{ hasAccess: boolean; workspaceId?: string }> {
143-
try {
144-
const { userTableDefinitions, permissions } = await import('@sim/db')
145-
const tableData = await db
146-
.select({ workspaceId: userTableDefinitions.workspaceId })
147-
.from(userTableDefinitions)
148-
.where(and(eq(userTableDefinitions.id, tableId), isNull(userTableDefinitions.archivedAt)))
149-
.limit(1)
150-
151-
if (!tableData.length) {
152-
logger.warn(`Table ${tableId} not found`)
153-
return { hasAccess: false }
154-
}
155-
const { workspaceId } = tableData[0]
156-
if (!workspaceId) return { hasAccess: false }
157-
158-
const [permissionRow] = await db
159-
.select({ permissionType: permissions.permissionType })
160-
.from(permissions)
161-
.where(
162-
and(
163-
eq(permissions.userId, userId),
164-
eq(permissions.entityType, 'workspace'),
165-
eq(permissions.entityId, workspaceId)
166-
)
167-
)
168-
.limit(1)
169-
170-
if (!permissionRow?.permissionType) {
171-
logger.warn(
172-
`User ${userId} has no permission for workspace ${workspaceId} (table ${tableId})`
173-
)
174-
return { hasAccess: false }
175-
}
176-
return { hasAccess: true, workspaceId }
177-
} catch (error) {
178-
logger.error(`Error verifying table access for user ${userId}, table ${tableId}:`, error)
179-
return { hasAccess: false }
180-
}
181-
}

apps/realtime/src/rooms/memory-manager.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import type { Server } from 'socket.io'
3-
import {
4-
type IRoomManager,
5-
type TableRowUpdatedPayload,
6-
tableRoomName,
7-
type UserPresence,
8-
type UserSession,
9-
type WorkflowRoom,
10-
} from '@/rooms/types'
3+
import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types'
114

125
const logger = createLogger('MemoryRoomManager')
136

@@ -262,23 +255,4 @@ export class MemoryRoomManager implements IRoomManager {
262255

263256
logger.info(`Notified ${room.users.size} users about workflow deployment change: ${workflowId}`)
264257
}
265-
266-
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
267-
this._io.to(tableRoomName(tableId)).emit(event, payload)
268-
}
269-
270-
async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
271-
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
272-
}
273-
274-
async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
275-
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
276-
}
277-
278-
async handleTableDeleted(tableId: string): Promise<void> {
279-
logger.info(`Handling table deletion notification for ${tableId}`)
280-
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
281-
// Eject sockets so they don't hold a stale room. Cross-pod safe via socket.io.
282-
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
283-
}
284258
}

apps/realtime/src/rooms/redis-manager.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { createClient, type RedisClientType } from 'redis'
33
import type { Server } from 'socket.io'
4-
import {
5-
type IRoomManager,
6-
type TableRowUpdatedPayload,
7-
tableRoomName,
8-
type UserPresence,
9-
type UserSession,
10-
} from '@/rooms/types'
4+
import type { IRoomManager, UserPresence, UserSession } from '@/rooms/types'
115

126
const logger = createLogger('RedisRoomManager')
137

@@ -463,23 +457,4 @@ export class RedisRoomManager implements IRoomManager {
463457
const userCount = await this.getUniqueUserCount(workflowId)
464458
logger.info(`Notified ${userCount} users about workflow deployment change: ${workflowId}`)
465459
}
466-
467-
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
468-
this._io.to(tableRoomName(tableId)).emit(event, payload)
469-
}
470-
471-
async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
472-
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
473-
}
474-
475-
async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
476-
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
477-
}
478-
479-
async handleTableDeleted(tableId: string): Promise<void> {
480-
logger.info(`Handling table deletion notification for ${tableId}`)
481-
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
482-
// Eject sockets across all pods via socket.io's Redis adapter.
483-
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
484-
}
485460
}

apps/realtime/src/rooms/types.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -143,45 +143,4 @@ export interface IRoomManager {
143143
* Handle workflow deployment change - notify users to refresh deployment state
144144
*/
145145
handleWorkflowDeployed(workflowId: string): Promise<void>
146-
147-
/**
148-
* Emit an event to all clients in a table room (`table:${tableId}`).
149-
* Tables don't track presence/last-modified state — just pub/sub.
150-
*/
151-
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void
152-
153-
/**
154-
* Notify all clients in a table room of a row write (insert/update/cell-state-change).
155-
* Sim API calls this via the `/api/table-row-updated` HTTP bridge after every successful
156-
* row commit; the client merges the delta into its React Query cache.
157-
*/
158-
handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void>
159-
160-
/**
161-
* Notify all clients in a table room that a row has been deleted.
162-
*/
163-
handleTableRowDeleted(tableId: string, rowId: string): Promise<void>
164-
165-
/**
166-
* Notify all clients in a table room that the table has been deleted; eject sockets.
167-
*/
168-
handleTableDeleted(tableId: string): Promise<void>
169-
}
170-
171-
/**
172-
* Payload broadcast on `table-row-updated`. Mirrors the shape of `TableRow.data` so
173-
* the client can merge directly into its React Query rows cache. `position` and
174-
* `updatedAt` are included for cache reconciliation; `data` is the full row data
175-
* (not a per-cell delta) — see plan Notes.
176-
*/
177-
export interface TableRowUpdatedPayload {
178-
rowId: string
179-
data: Record<string, unknown>
180-
/** Per-workflow-group execution state. Keyed by `WorkflowGroup.id`. */
181-
executions?: Record<string, unknown>
182-
position: number
183-
updatedAt: string | number
184146
}
185-
186-
/** Socket.IO room name for a table. Namespaced from workflow rooms. */
187-
export const tableRoomName = (tableId: string): string => `table:${tableId}`

apps/realtime/src/routes/http.ts

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -150,52 +150,6 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) {
150150
return
151151
}
152152

153-
// Handle table row write notifications from the Sim API
154-
if (req.method === 'POST' && req.url === '/api/table-row-updated') {
155-
try {
156-
const body = await readRequestBody(req)
157-
const { tableId, rowId, data, executions, position, updatedAt } = JSON.parse(body)
158-
await roomManager.handleTableRowUpdated(tableId, {
159-
rowId,
160-
data,
161-
executions,
162-
position,
163-
updatedAt,
164-
})
165-
sendSuccess(res)
166-
} catch (error) {
167-
logger.error('Error handling table row update notification:', error)
168-
sendError(res, 'Failed to process table row update')
169-
}
170-
return
171-
}
172-
173-
if (req.method === 'POST' && req.url === '/api/table-row-deleted') {
174-
try {
175-
const body = await readRequestBody(req)
176-
const { tableId, rowId } = JSON.parse(body)
177-
await roomManager.handleTableRowDeleted(tableId, rowId)
178-
sendSuccess(res)
179-
} catch (error) {
180-
logger.error('Error handling table row deletion notification:', error)
181-
sendError(res, 'Failed to process table row deletion')
182-
}
183-
return
184-
}
185-
186-
if (req.method === 'POST' && req.url === '/api/table-deleted') {
187-
try {
188-
const body = await readRequestBody(req)
189-
const { tableId } = JSON.parse(body)
190-
await roomManager.handleTableDeleted(tableId)
191-
sendSuccess(res)
192-
} catch (error) {
193-
logger.error('Error handling table deletion notification:', error)
194-
sendError(res, 'Failed to process table deletion')
195-
}
196-
return
197-
}
198-
199153
res.writeHead(404, { 'Content-Type': 'application/json' })
200154
res.end(JSON.stringify({ error: 'Not found' }))
201155
}

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

3+
import { useRef } from 'react'
34
import type { RowExecutionMetadata } from '@/lib/table'
45
import type { SaveReason } from '../../../types'
56
import type { DisplayColumn } from '../types'
6-
import { CellRender, resolveCellRender } from './cell-render'
7+
import { CellRender, type CellRenderKind, resolveCellRender } from './cell-render'
78
import { InlineEditor } from './inline-editors'
89

910
interface CellContentProps {
@@ -39,7 +40,10 @@ export function CellContent({
3940
onCancel,
4041
waitingOnLabels,
4142
}: CellContentProps) {
42-
const kind = resolveCellRender({ value, exec, column, waitingOnLabels })
43+
const kind = useTypewriterTrigger(
44+
resolveCellRender({ value, exec, column, waitingOnLabels }),
45+
column
46+
)
4347

4448
return (
4549
<>
@@ -58,3 +62,29 @@ export function CellContent({
5862
</>
5963
)
6064
}
65+
66+
/**
67+
* Sets `animateMount: true` on a workflow-output `value` kind when the cell
68+
* just transitioned into the value state from a non-value one (queued /
69+
* running / waiting / error / cancelled / empty) — i.e., the worker just
70+
* filled it in — or when an existing value's text changed. Stays `false` on
71+
* initial page load (cell mounts already filled) and on no-op refetches.
72+
*/
73+
function useTypewriterTrigger(kind: CellRenderKind, column: DisplayColumn): CellRenderKind {
74+
const mountedRef = useRef(false)
75+
const lastKindRef = useRef<CellRenderKind['kind'] | null>(null)
76+
const lastValueTextRef = useRef<string | null>(null)
77+
if (!column.workflowGroupId) return kind
78+
const isFirstRender = !mountedRef.current
79+
mountedRef.current = true
80+
const prevKind = lastKindRef.current
81+
const prevText = lastValueTextRef.current
82+
lastKindRef.current = kind.kind
83+
if (kind.kind === 'value') lastValueTextRef.current = kind.text
84+
if (kind.kind !== 'value') return kind
85+
if (isFirstRender) return kind
86+
// Just transitioned into value from a non-value state, or text changed.
87+
const justTransitioned = prevKind !== 'value' || prevText !== kind.text
88+
if (!justTransitioned) return kind
89+
return { ...kind, animateMount: true }
90+
}

0 commit comments

Comments
 (0)