+36
.dockerignore
+36
.dockerignore
···
1
+
# Exclude everything by default
2
+
*
3
+
4
+
# Then explicitly include only what's needed
5
+
6
+
# Include Go files and modules
7
+
!go.mod
8
+
!go.sum
9
+
!server/
10
+
!db/
11
+
12
+
# For the frontend
13
+
!mast-react-vite/src/
14
+
!mast-react-vite/public/
15
+
!mast-react-vite/index.html
16
+
!mast-react-vite/package.json
17
+
!mast-react-vite/package-lock.json
18
+
!mast-react-vite/*.config.js
19
+
!mast-react-vite/*.config.ts
20
+
21
+
# Keep excluding node_modules even with the include patterns above
22
+
**/node_modules
23
+
24
+
# Exclude common large/unnecessary files
25
+
**/.git
26
+
**/.gitignore
27
+
**/.DS_Store
28
+
**/dist
29
+
**/build
30
+
**/*.log
31
+
**/*.test.*
32
+
**/*.spec.*
33
+
**/*.tmp
34
+
**/.env*
35
+
**/.vscode
36
+
**/.idea
+34
-24
Dockerfile
+34
-24
Dockerfile
···
1
-
# Dockerfile
1
+
# Base builder
2
+
FROM golang:1.21-alpine AS base
3
+
WORKDIR /app
4
+
RUN apk add --no-cache gcc musl-dev
5
+
COPY go.mod go.sum ./
6
+
RUN go mod download
7
+
COPY db/crsqlite.so ./db/
2
8
3
-
# Step 1: Use a Node.js image to build the app
4
-
FROM node:18 as builder
5
-
6
-
# Set working directory inside the container
9
+
# Frontend builder
10
+
FROM node:18-alpine AS frontend-builder
7
11
WORKDIR /app
8
-
9
-
# Copy package.json and package-lock.json
10
-
COPY ./mast-react-vite/package*.json ./
11
-
12
-
# Install dependencies
12
+
COPY ./mast-react-vite/package.json ./mast-react-vite/package-lock.json ./
13
13
RUN npm install
14
-
15
-
# Copy the rest of the app’s source code
16
-
COPY ./mast-react-vite/ .
17
-
18
-
# Build the app
14
+
COPY ./mast-react-vite/src ./src
15
+
COPY ./mast-react-vite/public ./public
16
+
COPY ./mast-react-vite/index.html ./
17
+
COPY ./mast-react-vite/*.config.js ./mast-react-vite/*.config.ts ./
19
18
RUN npm run build
20
19
20
+
# Server builder
21
+
FROM base AS server-builder
22
+
WORKDIR /app
23
+
COPY server/main.go server/auth.go server/auth.db server/test.go ./server/
24
+
RUN CGO_ENABLED=1 go build -o /app/bin/server ./server/main.go ./server/auth.go
21
25
22
-
# Step 2: Use an Nginx image to serve the static files
23
-
FROM nginx:alpine
24
-
25
-
# Copy the build files from the builder stage to the Nginx web directory
26
-
COPY --from=builder /app/dist /usr/share/nginx/html
27
-
28
-
# Expose port 80
26
+
# Frontend stage
27
+
FROM nginx:alpine AS frontend
28
+
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
29
+
RUN mkdir -p /usr/share/nginx/html/db
30
+
COPY --from=base /app/db/crsqlite.so /usr/share/nginx/html/db/crsqlite.so
29
31
EXPOSE 80
32
+
CMD ["nginx", "-g", "daemon off;"]
30
33
31
-
# Start Nginx server
32
-
CMD ["nginx", "-g", "daemon off;"]
34
+
# Server stage
35
+
FROM alpine:latest AS server
36
+
WORKDIR /app
37
+
RUN apk add --no-cache ca-certificates
38
+
COPY --from=server-builder /app/bin/server /app/
39
+
COPY --from=base /app/db/ /app/db/
40
+
RUN mkdir -p /app/rooms
41
+
EXPOSE 8080
42
+
CMD ["/app/server"]
+23
fly.server.toml
+23
fly.server.toml
···
1
+
# fly.toml file for the Go server
2
+
app = "mast-server"
3
+
kill_signal = "SIGINT"
4
+
kill_timeout = 5
5
+
primary_region = "sea"
6
+
7
+
[build]
8
+
dockerfile = "Dockerfile"
9
+
build-target = "server"
10
+
11
+
[env]
12
+
13
+
[http_service]
14
+
internal_port = 8080
15
+
force_https = true
16
+
auto_stop_machines = true
17
+
auto_start_machines = true
18
+
min_machines_running = 0
19
+
processes = ["app"]
20
+
21
+
[mounts]
22
+
source = "server_data"
23
+
destination = "/app/rooms"
+4
fly.toml
+4
fly.toml
+4
-2
mast-react-vite/src/hooks/use-sync.ts
+4
-2
mast-react-vite/src/hooks/use-sync.ts
···
23
23
useEffect(() => {
24
24
if (!worker) return;
25
25
26
-
// Start sync
26
+
// Start sync with request for unsynced changes
27
+
console.log(`Starting sync for ${dbname} with room ${room} and requesting unsynced changes`);
27
28
worker.postMessage({
28
29
type: 'START_SYNC',
29
30
dbname,
30
31
config: {
31
32
room: room,
32
-
url: endpoint
33
+
url: endpoint,
34
+
requestUnsyncedChanges: true // Request server to send unsynced changes immediately
33
35
}
34
36
});
35
37
+7
-2
mast-react-vite/src/main.tsx
+7
-2
mast-react-vite/src/main.tsx
···
222
222
223
223
// Custom sync component using your own implementation
224
224
function CustomSyncComponent({ dbname, roomId }: { dbname: string, roomId: string }) {
225
-
const endpoint = `ws://localhost:8080/sync`;
225
+
const endpoint = process.env.NODE_ENV === 'production'
226
+
? `wss://mast-server.fly.dev/sync`
227
+
: `ws://localhost:8080/sync`;
226
228
227
229
const syncStats = useCustomSync({
228
230
dbname,
···
342
344
343
345
console.log("Request body:", JSON.stringify(requestBody));
344
346
345
-
const response = await fetch('http://localhost:8080/auth/register-key', {
347
+
const authEndpoint = process.env.NODE_ENV === 'production'
348
+
? 'https://mast-server.fly.dev/auth/register-key'
349
+
: 'http://localhost:8080/auth/register-key';
350
+
const response = await fetch(authEndpoint, {
346
351
method: 'POST',
347
352
headers: {
348
353
'Content-Type': 'application/json',
+7
-2
mast-react-vite/src/worker/sync-worker.ts
+7
-2
mast-react-vite/src/worker/sync-worker.ts
···
22
22
room: string;
23
23
url: string;
24
24
isConnecting: boolean;
25
+
requestUnsyncedChanges?: boolean;
25
26
}> = {};
26
27
27
28
// Handle messages from the main thread
···
160
161
const separator = wsUrl.includes('?') ? '&' : '?';
161
162
wsUrl = `${wsUrl}${separator}room=${config.room}`;
162
163
}
164
+
165
+
// Store the requestUnsyncedChanges flag in the connection
166
+
connections[dbname].requestUnsyncedChanges = config.requestUnsyncedChanges;
163
167
logDebug(`Connecting to WebSocket at ${wsUrl}`);
164
168
165
169
const ws = new WebSocket(wsUrl);
···
181
185
});
182
186
183
187
// Initial sync - request changes from server
184
-
logDebug(`Sending initial pull request`);
188
+
logDebug(`Sending initial pull request with requestUnsyncedChanges=${connections[dbname].requestUnsyncedChanges}`);
185
189
ws.send(JSON.stringify({
186
190
type: "pull",
187
-
room: config.room
191
+
room: config.room,
192
+
requestUnsyncedChanges: connections[dbname].requestUnsyncedChanges // Request any unsynced changes from server if specified
188
193
}));
189
194
190
195
// Notify main thread of connection
+48
server/Dockerfile
+48
server/Dockerfile
···
1
+
# Server Dockerfile for Go application
2
+
3
+
# Build stage
4
+
FROM golang:1.21-alpine AS builder
5
+
6
+
WORKDIR /app
7
+
8
+
# Install build dependencies
9
+
RUN apk add --no-cache gcc musl-dev
10
+
11
+
# Copy go mod files
12
+
COPY go.mod go.sum ./
13
+
14
+
# Download dependencies
15
+
RUN go mod download
16
+
17
+
# Copy source code
18
+
COPY server/ ./server/
19
+
COPY db/ ./db/
20
+
21
+
# Create output directory
22
+
RUN mkdir -p /app/bin
23
+
24
+
# Build the application
25
+
RUN CGO_ENABLED=1 go build -o /app/bin/server-app ./server/main.go
26
+
27
+
# Final stage
28
+
FROM alpine:latest
29
+
30
+
WORKDIR /app
31
+
32
+
# Install runtime dependencies
33
+
RUN apk add --no-cache ca-certificates
34
+
35
+
# Copy binary from builder stage
36
+
COPY --from=builder /app/bin/server-app /app/
37
+
38
+
# Copy SQLite extension
39
+
COPY --from=builder /app/db/crsqlite.so /app/db/crsqlite.so
40
+
41
+
# Create directory for room databases
42
+
RUN mkdir -p /app/rooms
43
+
44
+
# Expose port
45
+
EXPOSE 8080
46
+
47
+
# Run the application
48
+
CMD ["/app/server-app"]
+15
-5
server/main.go
+15
-5
server/main.go
···
163
163
removeClientFromRoom(roomID, conn)
164
164
}()
165
165
166
-
// Send initial changes to client
167
-
sendInitialChanges(conn, db)
166
+
// We don't automatically send initial changes anymore
167
+
// The client will request them with a pull message that includes requestUnsyncedChanges
168
168
169
169
// Handle incoming messages
170
170
for {
···
189
189
switch msgType {
190
190
case "pull":
191
191
// Client is requesting changes
192
+
log.Printf("Client in room %s requested pull", roomID)
192
193
changes := getChangesFromDB(db, msg)
193
194
response := map[string]interface{}{
194
195
"type": "changes",
···
288
289
}
289
290
290
291
func getChangesFromDB(db *sql.DB, msg map[string]interface{}) []map[string]interface{} {
291
-
// Implementation to get changes based on client's request
292
-
// This would typically filter based on site_id and version
292
+
// Check if client is requesting unsynced changes
293
+
requestUnsyncedChanges, _ := msg["requestUnsyncedChanges"].(bool)
294
+
log.Printf("Client requesting changes. Unsynced changes requested: %v", requestUnsyncedChanges)
293
295
294
-
rows, err := db.Query("SELECT * FROM crsql_changes")
296
+
// Default query gets all changes
297
+
query := "SELECT * FROM crsql_changes"
298
+
299
+
// If we need to send all unsynced changes, no need to filter
300
+
// But in a more sophisticated implementation, we could filter based on timestamps
301
+
// or some other mechanism to determine what's "unsynced"
302
+
303
+
rows, err := db.Query(query)
295
304
if err != nil {
296
305
log.Println("Error querying changes:", err)
297
306
return nil
···
329
338
changes = append(changes, change)
330
339
}
331
340
341
+
log.Printf("Returning %d changes to client for sync", len(changes))
332
342
return changes
333
343
}
334
344