-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathSupabaseConnector.swift
135 lines (112 loc) · 5.04 KB
/
SupabaseConnector.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import Auth
import SwiftUI
import Supabase
import PowerSync
import AnyCodable
private struct PostgresFatalCodes {
/// Postgres Response codes that we cannot recover from by retrying.
static let fatalResponseCodes: [String] = [
// Class 22 — Data Exception
// Examples include data type mismatch.
"22...",
// Class 23 — Integrity Constraint Violation.
// Examples include NOT NULL, FOREIGN KEY and UNIQUE violations.
"23...",
// INSUFFICIENT PRIVILEGE - typically a row-level security violation
"42501"
]
static func isFatalError(_ code: String) -> Bool {
return fatalResponseCodes.contains { pattern in
code.range(of: pattern, options: [.regularExpression]) != nil
}
}
static func extractErrorCode(from error: any Error) -> String? {
// Look for code: Optional("XXXXX") pattern
let errorString = String(describing: error)
if let range = errorString.range(of: "code: Optional\\(\"([^\"]+)\"\\)", options: .regularExpression),
let codeRange = errorString[range].range(of: "\"([^\"]+)\"", options: .regularExpression) {
// Extract just the code from within the quotes
let code = errorString[codeRange].dropFirst().dropLast()
return String(code)
}
return nil
}
}
@Observable
class SupabaseConnector: PowerSyncBackendConnector {
let powerSyncEndpoint: String = Secrets.powerSyncEndpoint
let client: SupabaseClient = SupabaseClient(supabaseURL: Secrets.supabaseURL, supabaseKey: Secrets.supabaseAnonKey)
var session: Session?
private var errorCode: String?
@ObservationIgnored
private var observeAuthStateChangesTask: Task<Void, Error>?
override init() {
super.init()
observeAuthStateChangesTask = Task { [weak self] in
guard let self = self else { return }
for await (event, session) in self.client.auth.authStateChanges {
guard [.initialSession, .signedIn, .signedOut].contains(event) else { throw AuthError.sessionMissing }
self.session = session
}
}
}
var currentUserID: String {
guard let id = session?.user.id else {
preconditionFailure("Required session.")
}
return id.uuidString.lowercased()
}
override func fetchCredentials() async throws -> PowerSyncCredentials? {
session = try await client.auth.session
if (self.session == nil) {
throw AuthError.sessionMissing
}
let token = session!.accessToken
return PowerSyncCredentials(endpoint: self.powerSyncEndpoint, token: token)
}
override func uploadData(database: PowerSyncDatabaseProtocol) async throws {
guard let transaction = try await database.getCrudBatch() else { return }
var lastEntry: CrudEntry?
do {
try await Task.detached {
for entry in transaction.crud {
lastEntry = entry
let tableName = entry.table
let table = self.client.from(tableName)
switch entry.op {
case .put:
var data: [String: AnyCodable] = entry.opData?.mapValues { AnyCodable($0) } ?? [:]
data["id"] = AnyCodable(entry.id)
try await table.upsert(data).execute();
case .patch:
guard let opData = entry.opData else { continue }
let encodableData = opData.mapValues { AnyCodable($0) }
try await table.update(encodableData).eq("id", value: entry.id).execute()
case .delete:
try await table.delete().eq( "id", value: entry.id).execute()
}
}
_ = try await transaction.complete.invoke(p1: nil)
}.value
} catch {
if let errorCode = PostgresFatalCodes.extractErrorCode(from: error),
PostgresFatalCodes.isFatalError(errorCode) {
/// Instead of blocking the queue with these errors,
/// discard the (rest of the) transaction.
///
/// Note that these errors typically indicate a bug in the application.
/// If protecting against data loss is important, save the failing records
/// elsewhere instead of discarding, and/or notify the user.
print("Data upload error: \(error)")
print("Discarding entry: \(lastEntry!)")
_ = try await transaction.complete.invoke(p1: nil)
return
}
print("Data upload error - retrying last entry: \(lastEntry!), \(error)")
throw error
}
}
deinit {
observeAuthStateChangesTask?.cancel()
}
}