| ... | @@ -15,28 +15,41 @@ import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.t | ... | @@ -15,28 +15,41 @@ import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.t |
| 15 | | 15 | |
| 16 | export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()( | 16 | export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()( |
| 17 | "DeviceRegistrationPersistenceError", | 17 | "DeviceRegistrationPersistenceError", |
| 18 | { cause: Schema.Defect() }, | 18 | { |
| | 19 | userId: Schema.String, |
| | 20 | deviceId: Schema.String, |
| | 21 | stage: Schema.Literals(["claim-push-token", "claim-push-to-start-token", "upsert-device"]), |
| | 22 | cause: Schema.Defect(), |
| | 23 | }, |
| 19 | ) { | 24 | ) { |
| 20 | override get message(): string { | 25 | override get message(): string { |
| 21 | return "Failed to persist mobile device registration"; | 26 | return `Failed to persist mobile device registration for ${this.userId}/${this.deviceId} during ${this.stage}.`; |
| 22 | } | 27 | } |
| 23 | } | 28 | } |
| 24 | | 29 | |
| 25 | export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()( | 30 | export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()( |
| 26 | "DeviceUnregistrationPersistenceError", | 31 | "DeviceUnregistrationPersistenceError", |
| 27 | { cause: Schema.Defect() }, | 32 | { |
| | 33 | userId: Schema.String, |
| | 34 | deviceId: Schema.String, |
| | 35 | stage: Schema.Literals(["delete-live-activity", "delete-device"]), |
| | 36 | cause: Schema.Defect(), |
| | 37 | }, |
| 28 | ) { | 38 | ) { |
| 29 | override get message(): string { | 39 | override get message(): string { |
| 30 | return "Failed to unregister mobile device"; | 40 | return `Failed to unregister mobile device ${this.userId}/${this.deviceId} during ${this.stage}.`; |
| 31 | } | 41 | } |
| 32 | } | 42 | } |
| 33 | | 43 | |
| 34 | export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()( | 44 | export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()( |
| 35 | "DeviceListPersistenceError", | 45 | "DeviceListPersistenceError", |
| 36 | { cause: Schema.Defect() }, | 46 | { |
| | 47 | userId: Schema.String, |
| | 48 | cause: Schema.Defect(), |
| | 49 | }, |
| 37 | ) { | 50 | ) { |
| 38 | override get message(): string { | 51 | override get message(): string { |
| 39 | return "Failed to list mobile devices"; | 52 | return `Failed to list mobile devices for ${this.userId}.`; |
| 40 | } | 53 | } |
| 41 | } | 54 | } |
| 42 | | 55 | |
| ... | @@ -61,130 +74,181 @@ export const make = Effect.gen(function* () { | ... | @@ -61,130 +74,181 @@ export const make = Effect.gen(function* () { |
| 61 | const db = yield* RelayDb.RelayDb; | 74 | const db = yield* RelayDb.RelayDb; |
| 62 | | 75 | |
| 63 | return Devices.of({ | 76 | return Devices.of({ |
| 64 | register: Effect.fn("relay.devices.register")( | 77 | register: Effect.fn("relay.devices.register")(function* (input) { |
| 65 | function* (input) { | 78 | yield* Effect.annotateCurrentSpan({ |
| 66 | yield* Effect.annotateCurrentSpan({ | 79 | "relay.mobile.device_id": input.registration.deviceId, |
| 67 | "relay.mobile.device_id": input.registration.deviceId, | 80 | }); |
| 68 | }); | 81 | const updatedAt = DateTime.formatIso(yield* DateTime.now); |
| 69 | const updatedAt = DateTime.formatIso(yield* DateTime.now); | 82 | const registration = input.registration; |
| 70 | const registration = input.registration; | | |
| 71 | | 83 | |
| 72 | yield* Effect.all( | 84 | yield* Effect.all( |
| 73 | [ | 85 | [ |
| 74 | registration.pushToken | 86 | registration.pushToken |
| 75 | ? db | 87 | ? db |
| 76 | .update(relayMobileDevices) | 88 | .update(relayMobileDevices) |
| 77 | .set({ pushToken: null, updatedAt }) | 89 | .set({ pushToken: null, updatedAt }) |
| 78 | .where(eq(relayMobileDevices.pushToken, registration.pushToken)) | 90 | .where(eq(relayMobileDevices.pushToken, registration.pushToken)) |
| 79 | : Effect.void, | 91 | .pipe( |
| 80 | registration.pushToStartToken | 92 | Effect.mapError( |
| 81 | ? db | 93 | (cause) => |
| 82 | .update(relayMobileDevices) | 94 | new DeviceRegistrationPersistenceError({ |
| 83 | .set({ pushToStartToken: null, updatedAt }) | 95 | userId: input.userId, |
| 84 | .where(eq(relayMobileDevices.pushToStartToken, registration.pushToStartToken)) | 96 | deviceId: registration.deviceId, |
| 85 | : Effect.void, | 97 | stage: "claim-push-token", |
| 86 | ], | 98 | cause, |
| 87 | { concurrency: 2, discard: true }, | 99 | }), |
| 88 | ); | 100 | ), |
| | 101 | ) |
| | 102 | : Effect.void, |
| | 103 | registration.pushToStartToken |
| | 104 | ? db |
| | 105 | .update(relayMobileDevices) |
| | 106 | .set({ pushToStartToken: null, updatedAt }) |
| | 107 | .where(eq(relayMobileDevices.pushToStartToken, registration.pushToStartToken)) |
| | 108 | .pipe( |
| | 109 | Effect.mapError( |
| | 110 | (cause) => |
| | 111 | new DeviceRegistrationPersistenceError({ |
| | 112 | userId: input.userId, |
| | 113 | deviceId: registration.deviceId, |
| | 114 | stage: "claim-push-to-start-token", |
| | 115 | cause, |
| | 116 | }), |
| | 117 | ), |
| | 118 | ) |
| | 119 | : Effect.void, |
| | 120 | ], |
| | 121 | { discard: true }, |
| | 122 | ); |
| 89 | | 123 | |
| 90 | yield* db | 124 | yield* db |
| 91 | .insert(relayMobileDevices) | 125 | .insert(relayMobileDevices) |
| 92 | .values({ | 126 | .values({ |
| 93 | userId: input.userId, | 127 | userId: input.userId, |
| 94 | deviceId: registration.deviceId, | 128 | deviceId: registration.deviceId, |
| 95 | label: registration.label, | 129 | label: registration.label, |
| | 130 | platform: registration.platform, |
| | 131 | iosMajorVersion: registration.iosMajorVersion, |
| | 132 | appVersion: registration.appVersion ?? null, |
| | 133 | pushToken: registration.pushToken ?? null, |
| | 134 | pushToStartToken: registration.pushToStartToken ?? null, |
| | 135 | preferencesJson: registration.preferences, |
| | 136 | createdAt: updatedAt, |
| | 137 | updatedAt, |
| | 138 | }) |
| | 139 | .onConflictDoUpdate({ |
| | 140 | target: [relayMobileDevices.userId, relayMobileDevices.deviceId], |
| | 141 | set: { |
| 96 | platform: registration.platform, | 142 | platform: registration.platform, |
| | 143 | label: registration.label, |
| 97 | iosMajorVersion: registration.iosMajorVersion, | 144 | iosMajorVersion: registration.iosMajorVersion, |
| 98 | appVersion: registration.appVersion ?? null, | 145 | appVersion: registration.appVersion ?? null, |
| 99 | pushToken: registration.pushToken ?? null, | 146 | pushToken: sql`coalesce(excluded.push_token, ${relayMobileDevices.pushToken})`, |
| 100 | pushToStartToken: registration.pushToStartToken ?? null, | 147 | pushToStartToken: sql`coalesce( |
| 101 | preferencesJson: registration.preferences, | | |
| 102 | createdAt: updatedAt, | | |
| 103 | updatedAt, | | |
| 104 | }) | | |
| 105 | .onConflictDoUpdate({ | | |
| 106 | target: [relayMobileDevices.userId, relayMobileDevices.deviceId], | | |
| 107 | set: { | | |
| 108 | platform: registration.platform, | | |
| 109 | label: registration.label, | | |
| 110 | iosMajorVersion: registration.iosMajorVersion, | | |
| 111 | appVersion: registration.appVersion ?? null, | | |
| 112 | pushToken: sql`coalesce(excluded.push_token, ${relayMobileDevices.pushToken})`, | | |
| 113 | pushToStartToken: sql`coalesce( | | |
| 114 | excluded.push_to_start_token, | 148 | excluded.push_to_start_token, |
| 115 | ${relayMobileDevices.pushToStartToken} | 149 | ${relayMobileDevices.pushToStartToken} |
| 116 | )`, | 150 | )`, |
| 117 | preferencesJson: registration.preferences, | 151 | preferencesJson: registration.preferences, |
| 118 | updatedAt, | 152 | updatedAt, |
| 119 | }, | 153 | }, |
| 120 | }); | 154 | }) |
| 121 | }, | 155 | .pipe( |
| 122 | Effect.mapError((cause) => new DeviceRegistrationPersistenceError({ cause })), | 156 | Effect.mapError( |
| 123 | ), | 157 | (cause) => |
| 124 | unregister: Effect.fn("relay.devices.unregister")( | 158 | new DeviceRegistrationPersistenceError({ |
| 125 | function* (input) { | 159 | userId: input.userId, |
| 126 | yield* Effect.annotateCurrentSpan({ | 160 | deviceId: registration.deviceId, |
| 127 | "relay.mobile.device_id": input.deviceId, | 161 | stage: "upsert-device", |
| 128 | }); | 162 | cause, |
| 129 | yield* Effect.all( | 163 | }), |
| 130 | [ | 164 | ), |
| 131 | db | 165 | ); |
| 132 | .delete(relayLiveActivities) | 166 | }), |
| 133 | .where( | 167 | unregister: Effect.fn("relay.devices.unregister")(function* (input) { |
| 134 | and( | 168 | yield* Effect.annotateCurrentSpan({ |
| 135 | eq(relayLiveActivities.userId, input.userId), | 169 | "relay.mobile.device_id": input.deviceId, |
| 136 | eq(relayLiveActivities.deviceId, input.deviceId), | 170 | }); |
| 137 | ), | 171 | yield* Effect.all( |
| | 172 | [ |
| | 173 | db |
| | 174 | .delete(relayLiveActivities) |
| | 175 | .where( |
| | 176 | and( |
| | 177 | eq(relayLiveActivities.userId, input.userId), |
| | 178 | eq(relayLiveActivities.deviceId, input.deviceId), |
| 138 | ), | 179 | ), |
| 139 | db | 180 | ) |
| 140 | .delete(relayMobileDevices) | 181 | .pipe( |
| 141 | .where( | 182 | Effect.mapError( |
| 142 | and( | 183 | (cause) => |
| 143 | eq(relayMobileDevices.userId, input.userId), | 184 | new DeviceUnregistrationPersistenceError({ |
| 144 | eq(relayMobileDevices.deviceId, input.deviceId), | 185 | userId: input.userId, |
| 145 | ), | 186 | deviceId: input.deviceId, |
| | 187 | stage: "delete-live-activity", |
| | 188 | cause, |
| | 189 | }), |
| 146 | ), | 190 | ), |
| 147 | ], | 191 | ), |
| 148 | { concurrency: 2, discard: true }, | 192 | db |
| | 193 | .delete(relayMobileDevices) |
| | 194 | .where( |
| | 195 | and( |
| | 196 | eq(relayMobileDevices.userId, input.userId), |
| | 197 | eq(relayMobileDevices.deviceId, input.deviceId), |
| | 198 | ), |
| | 199 | ) |
| | 200 | .pipe( |
| | 201 | Effect.mapError( |
| | 202 | (cause) => |
| | 203 | new DeviceUnregistrationPersistenceError({ |
| | 204 | userId: input.userId, |
| | 205 | deviceId: input.deviceId, |
| | 206 | stage: "delete-device", |
| | 207 | cause, |
| | 208 | }), |
| | 209 | ), |
| | 210 | ), |
| | 211 | ], |
| | 212 | { discard: true }, |
| | 213 | ); |
| | 214 | }), |
| | 215 | listForUser: Effect.fn("relay.devices.listForUser")(function* (input) { |
| | 216 | const rows = yield* db |
| | 217 | .select({ |
| | 218 | deviceId: relayMobileDevices.deviceId, |
| | 219 | label: relayMobileDevices.label, |
| | 220 | platform: relayMobileDevices.platform, |
| | 221 | iosMajorVersion: relayMobileDevices.iosMajorVersion, |
| | 222 | appVersion: relayMobileDevices.appVersion, |
| | 223 | preferences: relayMobileDevices.preferencesJson, |
| | 224 | updatedAt: relayMobileDevices.updatedAt, |
| | 225 | }) |
| | 226 | .from(relayMobileDevices) |
| | 227 | .where(eq(relayMobileDevices.userId, input.userId)) |
| | 228 | .pipe( |
| | 229 | Effect.mapError( |
| | 230 | (cause) => new DeviceListPersistenceError({ userId: input.userId, cause }), |
| | 231 | ), |
| 149 | ); | 232 | ); |
| 150 | }, | 233 | return rows.map((row) => ({ |
| 151 | Effect.mapError((cause) => new DeviceUnregistrationPersistenceError({ cause })), | 234 | deviceId: row.deviceId, |
| 152 | ), | 235 | label: row.label, |
| 153 | listForUser: Effect.fn("relay.devices.listForUser")( | 236 | platform: row.platform, |
| 154 | function* (input) { | 237 | iosMajorVersion: row.iosMajorVersion, |
| 155 | const rows = yield* db | 238 | appVersion: row.appVersion, |
| 156 | .select({ | 239 | notifications: { |
| 157 | deviceId: relayMobileDevices.deviceId, | 240 | enabled: row.preferences.notificationsEnabled, |
| 158 | label: relayMobileDevices.label, | 241 | notifyOnApproval: row.preferences.notifyOnApproval, |
| 159 | platform: relayMobileDevices.platform, | 242 | notifyOnInput: row.preferences.notifyOnInput, |
| 160 | iosMajorVersion: relayMobileDevices.iosMajorVersion, | 243 | notifyOnCompletion: row.preferences.notifyOnCompletion, |
| 161 | appVersion: relayMobileDevices.appVersion, | 244 | notifyOnFailure: row.preferences.notifyOnFailure, |
| 162 | preferences: relayMobileDevices.preferencesJson, | 245 | }, |
| 163 | updatedAt: relayMobileDevices.updatedAt, | 246 | liveActivities: { |
| 164 | }) | 247 | enabled: row.preferences.liveActivitiesEnabled, |
| 165 | .from(relayMobileDevices) | 248 | }, |
| 166 | .where(eq(relayMobileDevices.userId, input.userId)); | 249 | updatedAt: row.updatedAt, |
| 167 | return rows.map((row) => ({ | 250 | })); |
| 168 | deviceId: row.deviceId, | 251 | }), |
| 169 | label: row.label, | | |
| 170 | platform: row.platform, | | |
| 171 | iosMajorVersion: row.iosMajorVersion, | | |
| 172 | appVersion: row.appVersion, | | |
| 173 | notifications: { | | |
| 174 | enabled: row.preferences.notificationsEnabled, | | |
| 175 | notifyOnApproval: row.preferences.notifyOnApproval, | | |
| 176 | notifyOnInput: row.preferences.notifyOnInput, | | |
| 177 | notifyOnCompletion: row.preferences.notifyOnCompletion, | | |
| 178 | notifyOnFailure: row.preferences.notifyOnFailure, | | |
| 179 | }, | | |
| 180 | liveActivities: { | | |
| 181 | enabled: row.preferences.liveActivitiesEnabled, | | |
| 182 | }, | | |
| 183 | updatedAt: row.updatedAt, | | |
| 184 | })); | | |
| 185 | }, | | |
| 186 | Effect.mapError((cause) => new DeviceListPersistenceError({ cause })), | | |
| 187 | ), | | |
| 188 | }); | 252 | }); |
| 189 | }); | 253 | }); |
| 190 | | 254 | |