d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
174 lines
4.2 KiB
TypeScript
174 lines
4.2 KiB
TypeScript
import {
|
|
getLargeValueMaterializationError,
|
|
isLargeValueRef,
|
|
type LargeValueRef,
|
|
} from '@/lib/execution/payloads/large-value-ref'
|
|
|
|
const FALLBACK_TTL_MS = 15 * 60 * 1000
|
|
const MAX_IN_MEMORY_BYTES = 256 * 1024 * 1024
|
|
|
|
interface LargeValueCacheScope {
|
|
workspaceId?: string
|
|
workflowId?: string
|
|
executionId?: string
|
|
largeValueExecutionIds?: string[]
|
|
largeValueKeys?: string[]
|
|
allowLargeValueWorkflowScope?: boolean
|
|
}
|
|
|
|
const inMemoryValues = new Map<
|
|
string,
|
|
{
|
|
value: unknown
|
|
size: number
|
|
expiresAt: number
|
|
scope?: LargeValueCacheScope
|
|
recoverable: boolean
|
|
}
|
|
>()
|
|
let inMemoryBytes = 0
|
|
|
|
export function clearLargeValueCacheForTests(): void {
|
|
inMemoryValues.clear()
|
|
inMemoryBytes = 0
|
|
}
|
|
|
|
function cleanupExpiredValues(now = Date.now()): void {
|
|
for (const [id, entry] of inMemoryValues.entries()) {
|
|
if (entry.expiresAt <= now) {
|
|
inMemoryValues.delete(id)
|
|
inMemoryBytes -= entry.size
|
|
}
|
|
}
|
|
}
|
|
|
|
export function cacheLargeValue(
|
|
id: string,
|
|
value: unknown,
|
|
size: number,
|
|
scope?: LargeValueCacheScope,
|
|
options: { recoverable?: boolean } = {}
|
|
): boolean {
|
|
if (size > MAX_IN_MEMORY_BYTES) {
|
|
return false
|
|
}
|
|
|
|
cleanupExpiredValues()
|
|
|
|
const existing = inMemoryValues.get(id)
|
|
if (existing) {
|
|
inMemoryValues.delete(id)
|
|
inMemoryBytes -= existing.size
|
|
}
|
|
|
|
while (inMemoryBytes + size > MAX_IN_MEMORY_BYTES && inMemoryValues.size > 0) {
|
|
const oldestRecoverableId = Array.from(inMemoryValues.entries()).find(
|
|
([, entry]) => entry.recoverable
|
|
)?.[0]
|
|
if (!oldestRecoverableId) break
|
|
const oldest = inMemoryValues.get(oldestRecoverableId)
|
|
inMemoryValues.delete(oldestRecoverableId)
|
|
inMemoryBytes -= oldest?.size ?? 0
|
|
}
|
|
|
|
if (inMemoryBytes + size > MAX_IN_MEMORY_BYTES) {
|
|
if (existing) {
|
|
inMemoryValues.set(id, existing)
|
|
inMemoryBytes += existing.size
|
|
}
|
|
return false
|
|
}
|
|
|
|
inMemoryValues.set(id, {
|
|
value,
|
|
size,
|
|
scope,
|
|
recoverable: options.recoverable ?? false,
|
|
expiresAt: Date.now() + FALLBACK_TTL_MS,
|
|
})
|
|
inMemoryBytes += size
|
|
return true
|
|
}
|
|
|
|
function scopeMatchesRef(
|
|
ref: LargeValueRef,
|
|
cachedScope: LargeValueCacheScope | undefined,
|
|
callerScope?: LargeValueCacheScope
|
|
): boolean {
|
|
if (!cachedScope?.executionId) {
|
|
return false
|
|
}
|
|
if (ref.executionId && ref.executionId !== cachedScope.executionId) {
|
|
return false
|
|
}
|
|
if (!callerScope) {
|
|
return Boolean(ref.key) && (!ref.executionId || ref.executionId === cachedScope.executionId)
|
|
}
|
|
|
|
const allowedExecutionIds = new Set([
|
|
callerScope.executionId,
|
|
...(callerScope.largeValueExecutionIds ?? []),
|
|
])
|
|
if (ref.key && callerScope.largeValueKeys?.includes(ref.key)) {
|
|
return true
|
|
}
|
|
const workflowScopeAllowed =
|
|
callerScope.allowLargeValueWorkflowScope &&
|
|
callerScope.workspaceId === cachedScope.workspaceId &&
|
|
callerScope.workflowId === cachedScope.workflowId
|
|
|
|
return allowedExecutionIds.has(cachedScope.executionId) || Boolean(workflowScopeAllowed)
|
|
}
|
|
|
|
export function materializeLargeValueRefSync(
|
|
ref: LargeValueRef,
|
|
callerScope?: LargeValueCacheScope
|
|
): unknown {
|
|
cleanupExpiredValues()
|
|
const cached = inMemoryValues.get(ref.id)
|
|
if (!cached || !scopeMatchesRef(ref, cached.scope, callerScope)) {
|
|
return undefined
|
|
}
|
|
return cached.value
|
|
}
|
|
|
|
export function materializeLargeValueRefSyncOrThrow(
|
|
ref: LargeValueRef,
|
|
callerScope?: LargeValueCacheScope
|
|
): unknown {
|
|
const materialized = materializeLargeValueRefSync(ref, callerScope)
|
|
if (materialized === undefined) {
|
|
throw getLargeValueMaterializationError(ref)
|
|
}
|
|
return materialized
|
|
}
|
|
|
|
export function materializeLargeValueRefsSync(
|
|
value: unknown,
|
|
seen = new WeakSet<object>()
|
|
): unknown {
|
|
if (isLargeValueRef(value)) {
|
|
return materializeLargeValueRefsSync(materializeLargeValueRefSyncOrThrow(value), seen)
|
|
}
|
|
|
|
if (!value || typeof value !== 'object') {
|
|
return value
|
|
}
|
|
|
|
if (seen.has(value)) {
|
|
return value
|
|
}
|
|
seen.add(value)
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => materializeLargeValueRefsSync(item, seen))
|
|
}
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, entryValue]) => [
|
|
key,
|
|
materializeLargeValueRefsSync(entryValue, seen),
|
|
])
|
|
)
|
|
}
|