framer-code-link 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,180 +0,0 @@
1
- /**
2
- * WebSocket connection helper
3
- *
4
- * Thin wrapper around ws.Server that normalizes handshake and surfaces
5
- * simple callbacks. Keeps raw socket API localized.
6
- */
7
-
8
- import { WebSocketServer, WebSocket } from "ws"
9
- import type { IncomingMessage, OutgoingMessage } from "../types.js"
10
- import { debug, error } from "../utils/logging.js"
11
-
12
- export interface ConnectionCallbacks {
13
- onHandshake: (
14
- client: WebSocket,
15
- message: { projectId: string; projectName: string }
16
- ) => void
17
- onMessage: (message: IncomingMessage) => void
18
- onDisconnect: () => void
19
- onError: (error: Error) => void
20
- }
21
-
22
- export interface Connection {
23
- on(event: "handshake", handler: ConnectionCallbacks["onHandshake"]): void
24
- on(event: "message", handler: ConnectionCallbacks["onMessage"]): void
25
- on(event: "disconnect", handler: ConnectionCallbacks["onDisconnect"]): void
26
- on(event: "error", handler: ConnectionCallbacks["onError"]): void
27
- close(): void
28
- }
29
-
30
- /**
31
- * Initializes a WebSocket server and returns a connection interface
32
- * Returns a Promise that resolves when the server is ready, or rejects on startup errors
33
- */
34
- export function initConnection(port: number): Promise<Connection> {
35
- return new Promise((resolve, reject) => {
36
- const wss = new WebSocketServer({ port })
37
- const handlers: Partial<ConnectionCallbacks> = {}
38
- let connectionId = 0
39
- let isReady = false
40
-
41
- // Handle server-level errors (e.g., EADDRINUSE)
42
- wss.on("error", (err: NodeJS.ErrnoException) => {
43
- if (!isReady) {
44
- // Startup error - reject the promise with a helpful message
45
- if (err.code === "EADDRINUSE") {
46
- error(`Port ${port} is already in use.`)
47
- error(
48
- `This usually means another instance of Code Link is already running.`
49
- )
50
- error(``)
51
- error(`To fix this:`)
52
- error(
53
- ` 1. Close any other terminal running Code Link for this project`
54
- )
55
- error(` 2. Or run: lsof -i :${port} | grep LISTEN`)
56
- error(` Then kill the process: kill -9 <PID>`)
57
- reject(new Error(`Port ${port} is already in use`))
58
- } else {
59
- error(`Failed to start WebSocket server: ${err.message}`)
60
- reject(err)
61
- }
62
- return
63
- }
64
- // Runtime error - log but don't crash
65
- error(`WebSocket server error: ${err.message}`)
66
- })
67
-
68
- // Server is ready when it starts listening
69
- wss.on("listening", () => {
70
- isReady = true
71
- debug(`WebSocket server listening on port ${port}`)
72
-
73
- wss.on("connection", (ws: WebSocket) => {
74
- const connId = ++connectionId
75
- debug(`Client connected (conn ${connId})`)
76
-
77
- ws.on("message", (data: Buffer) => {
78
- try {
79
- const message = JSON.parse(data.toString()) as IncomingMessage
80
-
81
- // Special handling for handshake
82
- if (message.type === "handshake") {
83
- debug(`Received handshake (conn ${connId})`)
84
- handlers.onHandshake?.(ws, message)
85
- } else {
86
- handlers.onMessage?.(message)
87
- }
88
- } catch (err) {
89
- error(`Failed to parse message:`, err)
90
- }
91
- })
92
-
93
- ws.on("close", (code, reason) => {
94
- debug(
95
- `Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
96
- )
97
- handlers.onDisconnect?.()
98
- })
99
-
100
- ws.on("error", (err) => {
101
- error(`WebSocket error:`, err)
102
- })
103
- })
104
-
105
- resolve({
106
- on(
107
- event: "handshake" | "message" | "disconnect" | "error",
108
- handler: any
109
- ): void {
110
- if (event === "handshake") {
111
- handlers.onHandshake = handler
112
- } else if (event === "message") {
113
- handlers.onMessage = handler
114
- } else if (event === "disconnect") {
115
- handlers.onDisconnect = handler
116
- } else if (event === "error") {
117
- handlers.onError = handler
118
- }
119
- },
120
-
121
- close(): void {
122
- wss.close()
123
- },
124
- })
125
- })
126
- })
127
- }
128
-
129
- /**
130
- * WebSocket readyState constants for reference
131
- */
132
- const READY_STATE = {
133
- CONNECTING: 0,
134
- OPEN: 1,
135
- CLOSING: 2,
136
- CLOSED: 3,
137
- } as const
138
-
139
- function readyStateToString(state: number): string {
140
- switch (state) {
141
- case 0:
142
- return "CONNECTING"
143
- case 1:
144
- return "OPEN"
145
- case 2:
146
- return "CLOSING"
147
- case 3:
148
- return "CLOSED"
149
- default:
150
- return `UNKNOWN(${state})`
151
- }
152
- }
153
-
154
- /**
155
- * Sends a message to a connected socket
156
- * Returns false if the socket is not open (instead of throwing)
157
- */
158
- export function sendMessage(
159
- socket: WebSocket,
160
- message: OutgoingMessage
161
- ): Promise<boolean> {
162
- return new Promise((resolve) => {
163
- // Check socket state before attempting to send
164
- if (socket.readyState !== READY_STATE.OPEN) {
165
- const stateStr = readyStateToString(socket.readyState)
166
- debug(`Cannot send ${message.type}: socket is ${stateStr}`)
167
- resolve(false)
168
- return
169
- }
170
-
171
- socket.send(JSON.stringify(message), (err) => {
172
- if (err) {
173
- debug(`Send error for ${message.type}: ${err.message}`)
174
- resolve(false)
175
- } else {
176
- resolve(true)
177
- }
178
- })
179
- })
180
- }
@@ -1,117 +0,0 @@
1
- import fs from "fs/promises"
2
- import os from "os"
3
- import path from "path"
4
- import { describe, it, expect } from "vitest"
5
- import { autoResolveConflicts, detectConflicts } from "./files.js"
6
- import type { Conflict } from "../types.js"
7
- import { hashFileContent } from "../utils/state-persistence.js"
8
-
9
- function makeConflict(overrides: Partial<Conflict> = {}): Conflict {
10
- return {
11
- fileName: overrides.fileName ?? "Test.tsx",
12
- localContent: overrides.localContent ?? "local",
13
- remoteContent: overrides.remoteContent ?? "remote",
14
- localModifiedAt: overrides.localModifiedAt ?? Date.now(),
15
- remoteModifiedAt: overrides.remoteModifiedAt ?? Date.now(),
16
- lastSyncedAt: overrides.lastSyncedAt ?? Date.now(),
17
- localClean: overrides.localClean,
18
- }
19
- }
20
-
21
- describe("autoResolveConflicts", () => {
22
- it("classifies conflicts as local when remote unchanged and local changed", () => {
23
- const conflict = makeConflict({
24
- lastSyncedAt: 5_000,
25
- localClean: false,
26
- })
27
-
28
- const result = autoResolveConflicts(
29
- [conflict],
30
- [{ fileName: conflict.fileName, latestRemoteVersionMs: 5_000 }]
31
- )
32
-
33
- expect(result.autoResolvedLocal).toHaveLength(1)
34
- expect(result.autoResolvedRemote).toHaveLength(0)
35
- expect(result.remainingConflicts).toHaveLength(0)
36
- })
37
-
38
- it("classifies conflicts as remote when local is clean and remote changed", () => {
39
- const conflict = makeConflict({
40
- lastSyncedAt: 5_000,
41
- localClean: true,
42
- })
43
-
44
- const result = autoResolveConflicts(
45
- [conflict],
46
- [{ fileName: conflict.fileName, latestRemoteVersionMs: 10_000 }]
47
- )
48
-
49
- expect(result.autoResolvedRemote).toHaveLength(1)
50
- expect(result.autoResolvedLocal).toHaveLength(0)
51
- })
52
-
53
- it("keeps conflicts that have both sides changed", () => {
54
- const conflict = makeConflict({
55
- lastSyncedAt: 5_000,
56
- localClean: false,
57
- })
58
-
59
- const result = autoResolveConflicts(
60
- [conflict],
61
- [{ fileName: conflict.fileName, latestRemoteVersionMs: 7_500 }]
62
- )
63
-
64
- expect(result.remainingConflicts).toHaveLength(1)
65
- expect(result.autoResolvedLocal).toHaveLength(0)
66
- expect(result.autoResolvedRemote).toHaveLength(0)
67
- })
68
-
69
- it("keeps conflicts when version data is missing", () => {
70
- const conflict = makeConflict({
71
- lastSyncedAt: 5_000,
72
- localClean: true,
73
- })
74
-
75
- const result = autoResolveConflicts([conflict], [])
76
-
77
- expect(result.remainingConflicts).toHaveLength(1)
78
- })
79
- })
80
-
81
- describe("detectConflicts", () => {
82
- it("marks conflicts as localClean when local matches persisted state", async () => {
83
- const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cl-test-"))
84
- try {
85
- const filesDir = path.join(tmpRoot, "files")
86
- await fs.mkdir(filesDir, { recursive: true })
87
-
88
- const localContent = "local content"
89
- await fs.writeFile(path.join(filesDir, "Test.tsx"), localContent, "utf-8")
90
-
91
- const persistedState = new Map([
92
- [
93
- "Test.tsx",
94
- { contentHash: hashFileContent(localContent), timestamp: 1_000 },
95
- ],
96
- ])
97
-
98
- const result = await detectConflicts(
99
- [
100
- {
101
- name: "Test.tsx",
102
- content: "remote content",
103
- modifiedAt: 2_000,
104
- },
105
- ],
106
- filesDir,
107
- { persistedState }
108
- )
109
-
110
- expect(result.writes).toHaveLength(0)
111
- expect(result.conflicts).toHaveLength(1)
112
- expect(result.conflicts[0]?.localClean).toBe(true)
113
- } finally {
114
- await fs.rm(tmpRoot, { recursive: true, force: true })
115
- }
116
- })
117
- })