oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import RastrilloApple
3
4@MainActor extension SheetModel {
5 func watch() async {
6 guard page != nil else { return }
7 let source = grid
8 // Paces reconnects: a server's Retry-After is a deadline no wake crosses;
9 // otherwise back off, cut short by a connectivity change, with no timer
10 // while the path is down. The backoff resets only after a stream that
11 // stayed open for 5 s, timed from onOpen.
12 let reconnect = NativeReconnect(connectivity: connectivity, now: reconnectClock)
13 while !Task.isCancelled && grid == source {
14 live = connectivity.isOffline(client.origin) ? "Offline" : "Connecting"
15 do {
16 // Subscribe before fetching the snapshot in onOpen; buffered deltas
17 // then reconcile by version once the initial fetch finishes.
18 let parser = SheetEventParser()
19 try await client.stream(
20 source + "/stream",
21 onOpen: { [weak self] in
22 await reconnect.opened()
23 await self?.streamOpened(source: source)
24 }
25 ) { [weak self] line in
26 let event = await parser.receive(line)
27 guard let event else { return }
28 await self?.received(event, source: source)
29 }
30 } catch is CancellationError { return } catch {
31 guard !Task.isCancelled, grid == source else { return }
32 reconnect.failed(error)
33 // The failure itself says whether this server was unreachable; the
34 // connectivity record of it arrives asynchronously.
35 var offline = connectivity.isOffline(client.origin)
36 if case NativeClientError.offline = error { offline = true }
37 live = offline ? "Offline" : "Reconnecting"
38 }
39 if live == "Access changed" || live == "Paused" { return }
40 if !connectivity.isPathSatisfied { live = "Offline" }
41 await reconnect.pause()
42 if Task.isCancelled { return }
43 }
44 }
45 private func streamOpened(source: String) async {
46 guard !Task.isCancelled, grid == source else { return }
47 await refresh()
48 guard !Task.isCancelled, grid == source else { return }
49 live = "Live"
50 }
51 func received(_ event: SheetEvent, source: String) async {
52 guard !Task.isCancelled, grid == source else { return }
53 switch event.name {
54 case "cells":
55 live = "Live"
56 if let response = try? await wire.decode(Data(event.data.utf8)) {
57 guard !Task.isCancelled, grid == source else { return }
58 await accept(response)
59 } else {
60 guard !Task.isCancelled, grid == source else { return }
61 await refresh()
62 }
63 case "resync": await refresh()
64 case "bye":
65 if event.data.contains("access-changed") {
66 live = "Access changed"
67 cells = [:]
68 styles = [:]
69 error = "Your access changed. Reopen the sheet to continue."
70 } else if event.data.contains("idle") {
71 live = "Paused"
72 }
73 default: live = "Live"
74 }
75 }
76}
77struct SheetEvent: Sendable {
78 let name: String
79 let data: String
80}
81actor SheetEventParser {
82 private var name = ""
83 private var data = ""
84 func receive(_ line: String) -> SheetEvent? {
85 if line.isEmpty {
86 let event = SheetEvent(name: name, data: data)
87 name = ""
88 data = ""
89 return event
90 }
91 if line.hasPrefix("event:") {
92 name = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces)
93 }
94 if line.hasPrefix("data:") {
95 if !data.isEmpty { data += "\n" }
96 data += line.dropFirst(5).trimmingCharacters(in: .whitespaces)
97 }
98 return nil
99 }
100}
101