UNPKG

@towns-protocol/sdk

Version:

For more details, visit the following resources:

224 lines 8.74 kB
import { PersistedEventSchema, PersistedMiniblockSchema, } from '@towns-protocol/proto'; import { bin_toHexString } from '@towns-protocol/dlog'; import { create } from '@bufbuild/protobuf'; import { isDefined, logNever } from './check'; import { snakeCase } from 'lodash-es'; export function isPersistedEvent(event, direction) { if (!event.event) { return false; } switch (event.event.payload.case) { case 'channelPayload': return true; case 'dmChannelPayload': return true; case 'gdmChannelPayload': return true; case 'mediaPayload': return true; case 'userPayload': switch (event.event.payload.value.content.case) { case 'blockchainTransaction': return true; case 'receivedBlockchainTransaction': return true; default: return direction === 'forward' ? true : false; } case 'userSettingsPayload': return direction === 'forward' ? true : false; case 'miniblockHeader': return true; case 'userMetadataPayload': return direction === 'forward' ? true : false; case 'memberPayload': { switch (event.event.payload.value.content.case) { case 'keySolicitation': return direction === 'forward' ? true : false; case 'keyFulfillment': return direction === 'forward' ? true : false; case 'memberBlockchainTransaction': return true; case undefined: return false; default: return direction === 'forward' ? true : false; } } case 'spacePayload': return direction === 'forward' ? true : false; case 'userInboxPayload': return direction === 'forward' ? true : false; case 'metadataPayload': return false; case undefined: return false; default: logNever(event.event.payload, `unsupported event payload ${event.event.payload}`); return false; } } export function persistedEventToParsedEvent(event) { if (!event.event) { return undefined; } return { event: event.event, hash: event.hash, hashStr: bin_toHexString(event.hash), signature: event.signature, creatorUserId: event.creatorUserId, ephemeral: false, // Persisted events are never ephemeral }; } export function persistedMiniblockToParsedMiniblock(miniblock) { if (!miniblock.header) { return undefined; } return { hash: miniblock.hash, header: miniblock.header, events: miniblock.events.map(persistedEventToParsedEvent).filter(isDefined), }; } export function parsedMiniblockToPersistedMiniblock(miniblock, direction) { // always zero out the snapshot since we save it separately const header = { ...miniblock.header, snapshot: undefined, snapshotHash: computeBackwardsCompatibleSnapshotHash(miniblock.header), }; return create(PersistedMiniblockSchema, { hash: miniblock.hash, header: header, events: miniblock.events .filter((event) => isPersistedEvent(event, direction)) .map(parsedEventToPersistedEvent), // Note: partial property is not stored in PersistedMiniblock }); } function parsedEventToPersistedEvent(event) { // always zero out the snapshot since we save it separately if (event.event?.payload.case === 'miniblockHeader') { event.event.payload.value = { ...event.event.payload.value, snapshot: undefined, snapshotHash: computeBackwardsCompatibleSnapshotHash(event.event.payload.value), }; } return create(PersistedEventSchema, { event: event.event, hash: event.hash, signature: event.signature, creatorUserId: event.creatorUserId, }); } export function persistedSyncedStreamToParsedSyncedStream(streamId, stream) { if (!stream.syncCookie) { return undefined; } return { streamId, syncCookie: stream.syncCookie, lastSnapshotMiniblockNum: stream.lastSnapshotMiniblockNum, minipoolEvents: stream.minipoolEvents.map(persistedEventToParsedEvent).filter(isDefined), lastMiniblockNum: stream.lastMiniblockNum, }; } /// deprecated backfill /// if we have a snapshot, we don't want to save it here, but we do want to indicate that we have a snapshot /// if we don't have a snapshot hash but we do have an old snapshot (which is the deprecated case) /// we make up a fake string in place of the hash /// const snapshotHash = function computeBackwardsCompatibleSnapshotHash(header) { if (header.snapshotHash) { return header.snapshotHash; } if (header.snapshot) { return new Uint8Array(16).fill(1); } return undefined; } /** * Applies exclusion filters to miniblocks, similar to the Go backend implementation. * Returns new miniblocks with filtered events and partial flag set if any events were excluded. * * This function filters events based on their payload and content types, matching the behavior * of the Go backend's applyExclusionFilter function. Events that match any filter in the * exclusionFilter array are excluded from the returned miniblocks. * * @param miniblocks - Array of miniblocks to filter * @param exclusionFilter - Array of filters specifying which events to exclude * @returns Array of filtered miniblocks with partial flag set if events were excluded */ export function applyExclusionFilterToMiniblocks(miniblocks, exclusionFilter) { return miniblocks.map((miniblock) => { const originalEventCount = miniblock.events.length; const filteredEvents = miniblock.events.filter((event) => !shouldExcludeEvent(event, exclusionFilter)); // If no events were filtered, return original miniblock if (filteredEvents.length === originalEventCount) { return miniblock; } // Create new miniblock with filtered events and partial flag return { ...miniblock, events: filteredEvents, partial: true, // Set partial flag since events were excluded }; }); } /** * Determines if an event should be excluded based on exclusion filters. * Similar to the Go backend's shouldExcludeEvent function. */ export function shouldExcludeEvent(event, exclusionFilter) { // Extract payload type and content type const { payloadType, contentType } = extractEventTypeInfo(event.event); // Check if any filter matches this event for (const filter of exclusionFilter) { if (matchesEventFilter(payloadType, contentType, filter)) { return true; } } return false; } /** * Extracts payload type and content type from a StreamEvent. * Similar to the Go backend's extractEventTypeInfo function. */ export function extractEventTypeInfo(event) { const payload = event.payload; if (!payload || !payload.case) { return { payloadType: 'unknown', contentType: 'unknown' }; } // Get payload type name (convert camelCase to snake_case for consistency with Go) const payloadTypeName = snakeCase(payload.case); // Get the payload value const payloadValue = payload.value; if (!payloadValue || !payloadValue.content) { return { payloadType: payloadTypeName, contentType: 'none' }; } // Get content type name (convert camelCase to snake_case for consistency with Go) const contentTypeName = snakeCase(payloadValue.content.case || 'unknown'); return { payloadType: payloadTypeName, contentType: contentTypeName }; } /** * Checks if the payload/content types match the EventFilter. * Similar to the Go backend's matchesEventFilter function. */ export function matchesEventFilter(payloadType, contentType, filter) { // Convert filter payload to snake_case for comparison const filterPayloadType = filter.payload === '*' ? '*' : snakeCase(filter.payload); // Check payload type match if (filterPayloadType !== '*' && filterPayloadType !== payloadType) { return false; } // Check content type match (wildcard or exact match) if (filter.content === '*') { return true; } // Convert filter content to snake_case for comparison const filterContentType = snakeCase(filter.content); return filterContentType === contentType; } //# sourceMappingURL=streamUtils.js.map