Skip to content

Commit

Permalink
feat: initialize tiles from s3 if possible
Browse files Browse the repository at this point in the history
  • Loading branch information
cazala committed Dec 17, 2024
1 parent 9efbc3c commit 46161b4
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 22 deletions.
73 changes: 58 additions & 15 deletions src/modules/map/component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import {
IBaseComponent,
IConfigComponent,
IStatusCheckCapableComponent,
ITracerComponent,
} from '@well-known-components/interfaces'
import { EventEmitter } from 'events'
import future from 'fp-future'
import { IApiComponent, NFT, Result } from '../api/types'
import { NFT, Result } from '../api/types'
import { IMapComponent, Tile, MapEvents } from './types'
import { addSpecialTiles, computeEstate, isExpired, sleep } from './utils'
import { IS3Component } from '../s3/component'
import { toLegacyTiles } from '../../adapters/legacy-tiles'
import { ILoggerComponent } from '@well-known-components/interfaces'
import { AppComponents } from '../../types'

export async function createMapComponent(
Expand Down Expand Up @@ -133,6 +129,10 @@ export async function createMapComponent(
// Upload v2 format (current)
lastUploadedTilesUrl.v2 = await s3.uploadTilesJson('v2', tilesData)
componentLogger.info(`Uploaded v2 tiles to ${lastUploadedTilesUrl.v2}`)

// Upload timestamp separately
await s3.uploadTimestamp(lastUpdatedAt)
componentLogger.info(`Uploaded timestamp to S3`)
} catch (error) {
componentLogger.warn(
`Failed to upload tiles to S3 (will serve from memory): ${error}`
Expand All @@ -149,19 +149,62 @@ export async function createMapComponent(
async start() {
events.emit(MapEvents.INIT)
try {
const result = await batchApi.fetchData()
lastUpdatedAt = result.updatedAt
const newTiles = addSpecialTiles(addTiles(result.tiles, {}))
tiles.resolve(newTiles)
parcels.resolve(addParcels(result.parcels, {}))
estates.resolve(addEstates(result.estates, {}))
tokens.resolve(addTokens(result.parcels, result.estates, {}))
ready = true
await uploadTilesToS3(newTiles)
events.emit(MapEvents.READY, result)
// Try to load cached tiles and timestamp from S3
const [cachedTiles, cachedTimestamp] = await Promise.all([
s3.getTilesJson('v2'),
s3.getTimestamp(),
])

if (cachedTiles && cachedTimestamp) {
componentLogger.info(
'Found cached tiles in S3, using them to initialize the component'
)
lastUpdatedAt = cachedTimestamp
const newTiles = addSpecialTiles(
addTiles(Object.values(cachedTiles), {})
)
tiles.resolve(newTiles)

// Set the lastUploadedTilesUrl when loading from cache
lastUploadedTilesUrl = {
v1: await s3.getFileUrl('v1'),
v2: await s3.getFileUrl('v2'),
}

// Fetch only the necessary data for parcels and estates
const result = await batchApi.fetchData()
parcels.resolve(addParcels(result.parcels, {}))
estates.resolve(addEstates(result.estates, {}))
tokens.resolve(addTokens(result.parcels, result.estates, {}))

ready = true
events.emit(MapEvents.READY, {
tiles: Object.values(newTiles),
parcels: result.parcels,
estates: result.estates,
updatedAt: lastUpdatedAt,
})
} else {
// Fallback to original initialization if no cache exists
componentLogger.info(
'No cached tiles found in S3, fetching fresh data'
)
const result = await batchApi.fetchData()
lastUpdatedAt = result.updatedAt
const newTiles = addSpecialTiles(addTiles(result.tiles, {}))
tiles.resolve(newTiles)
parcels.resolve(addParcels(result.parcels, {}))
estates.resolve(addEstates(result.estates, {}))
tokens.resolve(addTokens(result.parcels, result.estates, {}))
ready = true
await uploadTilesToS3(newTiles)
events.emit(MapEvents.READY, result)
}

await sleep(refreshInterval)
poll()
} catch (error) {
componentLogger.error(`Failed to initialize map component: ${error}`)
tiles.reject(error as Error)
}
},
Expand Down
115 changes: 108 additions & 7 deletions src/modules/s3/component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { S3Client, PutObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import {
S3Client,
PutObjectCommand,
GetObjectCommand,
S3ClientConfig,
} from '@aws-sdk/client-s3'
import {
IConfigComponent,
ILoggerComponent,
} from '@well-known-components/interfaces'
import { Tile, LegacyTile } from '../map/types'

export type IS3Component = {
uploadTilesJson: (
version: 'v1' | 'v2',
tiles: Record<string, Partial<Tile> | Partial<LegacyTile>>
) => Promise<string>
export interface IS3Component {
uploadTilesJson(
version: string,
tiles: Record<string, Partial<Tile | LegacyTile>>
): Promise<string>
uploadTimestamp(timestamp: number): Promise<void>
getTilesJson(version: string): Promise<Record<string, Tile> | null>
getTimestamp(): Promise<number | null>
getFileUrl(version: string): Promise<string>
}

export async function createS3Component(components: {
Expand Down Expand Up @@ -50,7 +59,7 @@ export async function createS3Component(components: {

async function uploadTilesJson(
version: 'v1' | 'v2',
tiles: Record<string, Partial<Tile> | Partial<LegacyTile>>
tiles: Record<string, Tile | LegacyTile>
): Promise<string> {
const key = `tiles/${version}/latest.json`

Expand All @@ -77,8 +86,85 @@ export async function createS3Component(components: {
}
}

async function uploadTimestamp(timestamp: number): Promise<void> {
const key = 'tiles/timestamp.json'

try {
await s3Client.send(
new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: JSON.stringify({ timestamp }),
ContentType: 'application/json',
CacheControl: 'public, max-age=60',
})
)
componentLogger.info(`Uploaded timestamp to S3`)
} catch (error) {
componentLogger.error(`Failed to upload timestamp to S3: ${error}`)
throw error
}
}

async function getTilesJson(
version: string
): Promise<Record<string, Tile> | null> {
const key = `tiles/${version}/latest.json`

try {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: key,
})
)

const body = await response.Body?.transformToString()
if (!body) return null

const data = JSON.parse(body)
return data.ok ? data.data : null
} catch (error) {
componentLogger.warn(`Failed to get tiles from S3: ${error}`)
return null
}
}

async function getTimestamp(): Promise<number | null> {
const key = 'tiles/timestamp.json'

try {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: key,
})
)

const body = await response.Body?.transformToString()
if (!body) return null

const data = JSON.parse(body)
return data.timestamp
} catch (error) {
componentLogger.warn(`Failed to get timestamp from S3: ${error}`)
return null
}
}

async function getFileUrl(version: string): Promise<string> {
const key = `tiles/${version}/latest.json`
return endpoint
? `${endpoint}/${bucketName}/${key}`
: `https://${bucketName}.s3.amazonaws.com/${key}`
}

return {
uploadTilesJson,
uploadTimestamp,
getTilesJson,
getTimestamp,
getFileUrl,
}
} catch (error) {
componentLogger.warn(
Expand All @@ -96,5 +182,20 @@ function createStubS3Component(
logger.debug('Stub S3 component - upload operation ignored')
return ''
},
uploadTimestamp: async () => {
logger.debug('Stub S3 component - timestamp upload ignored')
},
getTilesJson: async () => {
logger.debug('Stub S3 component - get tiles operation ignored')
return null
},
getTimestamp: async () => {
logger.debug('Stub S3 component - get timestamp operation ignored')
return null
},
getFileUrl: async () => {
logger.debug('Stub S3 component - get file URL operation ignored')
return ''
},
}
}

0 comments on commit 46161b4

Please sign in to comment.