Skip to content

Instantly share code, notes, and snippets.

@izakfilmalter
Created June 23, 2025 11:48
Show Gist options
  • Save izakfilmalter/85eeafd46e444a9c33288971cf56842a to your computer and use it in GitHub Desktop.
Save izakfilmalter/85eeafd46e444a9c33288971cf56842a to your computer and use it in GitHub Desktop.
import { HttpApiClient, type HttpApiError, type HttpClientError } from '@effect/platform'
import { RefreshApi } from '@openfaith/pco/refreshApi'
import { TokenKey, TokenManager, type TokenState } from '@openfaith/pco/tokenManager'
import { Clock, Context, Effect, HashMap, Layer, Option, type ParseResult, Ref } from 'effect'
// Internal service tag
export class PcoAuth extends Context.Tag('Pco/PcoAuth')<
PcoAuth,
{
readonly getValidAccessToken: Effect.Effect<
string,
HttpApiError.HttpApiDecodeError | HttpClientError.HttpClientError | ParseResult.ParseError,
TokenKey
>
}
>() {}
// Live implementation of the PcoAuth service
export const PcoAuthLive = Layer.scoped(
PcoAuth,
Effect.gen(function* () {
const tokenManager = yield* TokenManager
const clock = yield* Clock.Clock
// This lock is for the REFRESH operation.
const tokenRefreshLock = yield* Effect.makeSemaphore(1)
// This lock is for the INITIAL LOAD operation for a new key.
const tokenLoadLock = yield* Effect.makeSemaphore(1)
// const tokenCache = yield* Ref.make(new Map<string, Ref.Ref<TokenState>>())
const tokenCache = yield* Ref.make(HashMap.empty<string, Ref.Ref<TokenState>>())
const refreshClient = yield* HttpApiClient.make(RefreshApi, {
baseUrl: 'https://api.planningcenteronline.com',
})
const getOrgTokenStateRef = (tokenKey?: string) =>
Effect.gen(function* () {
const key = tokenKey ?? 'default'
const cached = yield* Ref.get(tokenCache).pipe(Effect.map(HashMap.get(key)))
if (Option.isSome(cached)) {
return cached.value
}
// Cache miss. We need to load from the user's TokenManager.
// Use a lock to ensure only one fiber loads the token for a new key.
return yield* tokenLoadLock.withPermits(1)(
Effect.gen(function* () {
// Double-check after acquiring the lock.
const cachedAfterLock = yield* Ref.get(tokenCache).pipe(Effect.map(HashMap.get(key)))
if (Option.isSome(cachedAfterLock)) {
return cachedAfterLock.value
}
// We are the designated loader for this key.
const initialState = yield* tokenManager.loadTokenState(tokenKey)
const newRef = yield* Ref.make(initialState)
yield* Ref.update(tokenCache, HashMap.set(key, newRef))
return newRef
}),
)
})
const getValidAccessToken = Effect.gen(function* () {
const tokenKey = yield* TokenKey
const tokenStateRef = yield* getOrgTokenStateRef(tokenKey)
const state = yield* Ref.get(tokenStateRef)
const now = yield* clock.currentTimeMillis
if (state.expiresAt.getTime() - 60_000 >= now) {
return state.accessToken
}
const refreshEffect = Effect.gen(function* () {
const currentState = yield* Ref.get(tokenStateRef)
const nowAfterLock = yield* clock.currentTimeMillis
if (currentState.expiresAt.getTime() - 60_000 >= nowAfterLock) {
return currentState.accessToken
}
console.log(`Token for key "${tokenKey}" expired. Refreshing now...`)
const newTokens = yield* refreshClient.auth.refreshToken({
payload: {
grant_type: 'refresh_token',
refresh_token: currentState.refreshToken,
},
})
const newExpiry = new Date((yield* clock.currentTimeMillis) + newTokens.expires_in * 1000)
const newState: TokenState = {
accessToken: newTokens.access_token,
expiresAt: newExpiry,
refreshToken: newTokens.refresh_token,
tokenKey: currentState.tokenKey, // Preserve the original key
}
// Save and update the state
yield* tokenManager.saveTokenState(newState)
yield* Ref.set(tokenStateRef, newState)
console.log(`Token for key "${tokenKey}" refresh successful.`)
return newState.accessToken
})
return yield* tokenRefreshLock.withPermits(1)(refreshEffect)
})
return PcoAuth.of({ getValidAccessToken })
}),
)
import { Context, type Effect } from 'effect'
export class TokenKey extends Context.Tag('Pco/TokenKey')<TokenKey, string>() {}
// The shape of the token data your library needs to manage.
export interface TokenState {
readonly accessToken: string
readonly refreshToken: string
readonly expiresAt: Date
readonly tokenKey: string
}
// The service interface the user must implement.
export class TokenManager extends Context.Tag('Pco/TokenManager')<
TokenManager,
{
// How to load the initial token state from the user's storage.
readonly loadTokenState: (lookupKey?: string) => Effect.Effect<TokenState>
// How to save the new token state after a successful refresh.
readonly saveTokenState: (state: TokenState) => Effect.Effect<void>
}
>() {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment