|
| 1 | +import { |
| 2 | + PushRegistrationError, |
| 3 | + type AuthSessionId, |
| 4 | + type PushRegistrationsRegisterInput, |
| 5 | +} from "@t3tools/contracts"; |
| 6 | +import * as Context from "effect/Context"; |
| 7 | +import * as DateTime from "effect/DateTime"; |
| 8 | +import * as Effect from "effect/Effect"; |
| 9 | +import * as Layer from "effect/Layer"; |
| 10 | +import * as SqlClient from "effect/sql/SqlClient"; |
| 11 | + |
| 12 | +export interface PushTarget { |
| 13 | + readonly installId: string; |
| 14 | + readonly expoPushToken: string; |
| 15 | +} |
| 16 | + |
| 17 | +export class PushRegistrations extends Context.Service< |
| 18 | + PushRegistrations, |
| 19 | + { |
| 20 | + readonly register: ( |
| 21 | + sessionId: AuthSessionId, |
| 22 | + input: PushRegistrationsRegisterInput, |
| 23 | + ) => Effect.Effect<void, PushRegistrationError>; |
| 24 | + readonly unregister: (installId: string) => Effect.Effect<void, PushRegistrationError>; |
| 25 | + /** Phones whose session is still live. Rows of revoked or expired sessions are deleted. */ |
| 26 | + readonly listDeliverable: Effect.Effect<ReadonlyArray<PushTarget>, PushRegistrationError>; |
| 27 | + /** Expo reported these tokens as no longer registered on their device. */ |
| 28 | + readonly removeTokens: ( |
| 29 | + tokens: ReadonlyArray<string>, |
| 30 | + ) => Effect.Effect<void, PushRegistrationError>; |
| 31 | + } |
| 32 | +>()("t3/push/PushRegistrations") {} |
| 33 | + |
| 34 | +const toError = (cause: unknown) => new PushRegistrationError({ cause }); |
| 35 | + |
| 36 | +const make = Effect.gen(function* () { |
| 37 | + const sql = yield* SqlClient.SqlClient; |
| 38 | + |
| 39 | + const register = Effect.fn("PushRegistrations.register")(function* ( |
| 40 | + sessionId: AuthSessionId, |
| 41 | + input: PushRegistrationsRegisterInput, |
| 42 | + ) { |
| 43 | + const now = DateTime.formatIso(yield* DateTime.now); |
| 44 | + yield* sql |
| 45 | + .withTransaction( |
| 46 | + Effect.gen(function* () { |
| 47 | + // A reinstall keeps the token but gets a new install id; one phone, one row. |
| 48 | + yield* sql`DELETE FROM push_registrations |
| 49 | + WHERE expo_push_token = ${input.expoPushToken} AND install_id <> ${input.installId}`; |
| 50 | + yield* sql` |
| 51 | + INSERT INTO push_registrations (install_id, session_id, expo_push_token, label, updated_at) |
| 52 | + VALUES (${input.installId}, ${sessionId}, ${input.expoPushToken}, ${input.label ?? null}, ${now}) |
| 53 | + ON CONFLICT (install_id) DO UPDATE SET |
| 54 | + session_id = excluded.session_id, |
| 55 | + expo_push_token = excluded.expo_push_token, |
| 56 | + label = excluded.label, |
| 57 | + updated_at = excluded.updated_at |
| 58 | + `; |
| 59 | + }), |
| 60 | + ) |
| 61 | + .pipe(Effect.mapError(toError)); |
| 62 | + }); |
| 63 | + |
| 64 | + const unregister = Effect.fn("PushRegistrations.unregister")(function* (installId: string) { |
| 65 | + yield* sql`DELETE FROM push_registrations WHERE install_id = ${installId}`.pipe( |
| 66 | + Effect.mapError(toError), |
| 67 | + ); |
| 68 | + }); |
| 69 | + |
| 70 | + const listDeliverable = Effect.gen(function* () { |
| 71 | + const now = DateTime.formatIso(yield* DateTime.now); |
| 72 | + yield* sql` |
| 73 | + DELETE FROM push_registrations |
| 74 | + WHERE session_id NOT IN ( |
| 75 | + SELECT session_id FROM auth_sessions WHERE revoked_at IS NULL AND expires_at > ${now} |
| 76 | + ) |
| 77 | + `; |
| 78 | + return yield* sql<PushTarget>` |
| 79 | + SELECT install_id AS "installId", expo_push_token AS "expoPushToken" |
| 80 | + FROM push_registrations |
| 81 | + ORDER BY install_id |
| 82 | + `; |
| 83 | + }).pipe(Effect.mapError(toError), Effect.withSpan("PushRegistrations.listDeliverable")); |
| 84 | + |
| 85 | + const removeTokens = Effect.fn("PushRegistrations.removeTokens")(function* ( |
| 86 | + tokens: ReadonlyArray<string>, |
| 87 | + ) { |
| 88 | + if (tokens.length === 0) return; |
| 89 | + yield* sql`DELETE FROM push_registrations WHERE ${sql.in("expo_push_token", tokens)}`.pipe( |
| 90 | + Effect.mapError(toError), |
| 91 | + ); |
| 92 | + }); |
| 93 | + |
| 94 | + return PushRegistrations.of({ register, unregister, listDeliverable, removeTokens }); |
| 95 | +}); |
| 96 | + |
| 97 | +export const layer = Layer.effect(PushRegistrations, make); |
0 commit comments