From 6d2999225178f5a3e8436f8ffd5763091aafcada Mon Sep 17 00:00:00 2001 From: Kallyn Gowdy Date: Thu, 7 Nov 2024 17:09:43 -0500 Subject: [PATCH] wip: Continue working on supporting packages --- src/aux-records/SubscriptionConfigBuilder.ts | 28 ++ src/aux-records/SubscriptionConfiguration.ts | 2 +- .../packages/MemoryPackageRecordsStore.ts | 356 +++--------------- .../packages/PackageRecordsController.spec.ts | 60 +-- .../packages/PackageRecordsController.ts | 74 ++-- .../packages/PackageRecordsStore.ts | 12 +- 6 files changed, 130 insertions(+), 402 deletions(-) diff --git a/src/aux-records/SubscriptionConfigBuilder.ts b/src/aux-records/SubscriptionConfigBuilder.ts index 14d0567d8..173291557 100644 --- a/src/aux-records/SubscriptionConfigBuilder.ts +++ b/src/aux-records/SubscriptionConfigBuilder.ts @@ -15,6 +15,7 @@ import { FileFeaturesConfiguration, InstsFeaturesConfiguration, NotificationFeaturesConfiguration, + PackageFeaturesConfiguration, PublicInstsConfiguration, RecordFeaturesConfiguration, StudioComIdFeaturesConfiguration, @@ -239,6 +240,33 @@ export class FeaturesBuilder { return this; } + withPackages(features?: PackageFeaturesConfiguration): this { + this._features.packages = features ?? { + allowed: true, + }; + return this; + } + + withPackagesMaxItems(maxItems: number): this { + this._features.packages.maxItems = maxItems; + return this; + } + + withPackagesMaxVersions(maxVersions: number): this { + this._features.packages.maxPackageVersions = maxVersions; + return this; + } + + withPackagesMaxVersionSizeInBytes(maxSize: number): this { + this._features.packages.maxPackageVersionSizeInBytes = maxSize; + return this; + } + + withPackagesMaxBytesTotal(maxBytes: number): this { + this._features.packages.maxPackageBytesTotal = maxBytes; + return this; + } + get features() { return this._features; } diff --git a/src/aux-records/SubscriptionConfiguration.ts b/src/aux-records/SubscriptionConfiguration.ts index e1cd83324..fae55db15 100644 --- a/src/aux-records/SubscriptionConfiguration.ts +++ b/src/aux-records/SubscriptionConfiguration.ts @@ -444,7 +444,7 @@ export const subscriptionFeaturesSchema = z.object({ .boolean() .describe('Whether packages are allowed for the subscription.'), - maxPackages: z + maxItems: z .number() .describe( 'The maximum number of packages that are allowed for the subscription. If not specified, then there is no limit.' diff --git a/src/aux-records/packages/MemoryPackageRecordsStore.ts b/src/aux-records/packages/MemoryPackageRecordsStore.ts index 97d7fa11e..4570c79a4 100644 --- a/src/aux-records/packages/MemoryPackageRecordsStore.ts +++ b/src/aux-records/packages/MemoryPackageRecordsStore.ts @@ -1,351 +1,91 @@ import { MemoryCrudRecordsStore } from '../crud/MemoryCrudRecordsStore'; import { - NotificationRecord, PackageRecordsStore, - NotificationSubscription, - NotificationSubscriptionMetrics, - SaveSubscriptionResult, - SentNotification, - NotificationPushSubscription, - SentPushNotification, - PushSubscriptionUser, - UserPushSubscription, + PackageRecord, + PackageSubscriptionMetrics, + PackageVersion, + PackageRecordVersion, + ListedPackageVersion, } from './PackageRecordsStore'; import { SubscriptionFilter } from '../MetricsStore'; -import { uniqBy } from 'lodash'; /** * A Memory-based implementation of the PackageRecordsStore. */ export class MemoryPackageRecordsStore - extends MemoryCrudRecordsStore + extends MemoryCrudRecordsStore implements PackageRecordsStore { - private _pushSubscriptions: NotificationPushSubscription[] = []; - private _pushSubscriptionUsers: PushSubscriptionUser[] = []; - private _subscriptions: NotificationSubscription[] = []; - private _sentNotifications: SentNotification[] = []; - private _sentPushNotifications: SentPushNotification[] = []; - - get subscriptions() { - return this._subscriptions; - } - - get sentNotifications() { - return this._sentNotifications; - } - - get sentPushNotifications() { - return this._sentPushNotifications; - } - - get pushSubscriptions() { - return this._pushSubscriptions; - } - - get pushSubscriptionUsers() { - return this._pushSubscriptionUsers; - } - - async savePushSubscription( - pushSubscription: NotificationPushSubscription - ): Promise { - const index = this._pushSubscriptions.findIndex( - (s) => s.id === pushSubscription.id + private _packageVersions: PackageRecordVersion[] = []; + + async addPackageVersion(version: PackageRecordVersion): Promise { + const index = this._packageVersions.findIndex( + (v) => + v.recordName === version.recordName && + v.address === version.address && + v.version.major === version.version.major && + v.version.minor === version.version.minor && + v.version.patch === version.version.patch && + v.version.tag === version.version.tag ); if (index >= 0) { - this._pushSubscriptions[index] = { - ...pushSubscription, - }; - } else { - this._pushSubscriptions.push({ - ...pushSubscription, - }); + throw new Error('Version already exists'); } - } - - async savePushSubscriptionUser( - pushSubscription: PushSubscriptionUser - ): Promise { - const index = this._pushSubscriptionUsers.findIndex( - (u) => - u.pushSubscriptionId === pushSubscription.pushSubscriptionId && - u.userId === pushSubscription.userId - ); - - if (index >= 0) { - this._pushSubscriptionUsers[index] = { - ...pushSubscription, - }; - } else { - this._pushSubscriptionUsers.push({ - ...pushSubscription, - }); - } - } - - async markPushSubscriptionsInactiveAndDeleteUserRelations( - ids: string[] - ): Promise { - for (let id of ids) { - const index = this._pushSubscriptions.findIndex((s) => s.id === id); - if (index >= 0) { - this._pushSubscriptions[index] = { - ...this._pushSubscriptions[index], - active: false, - }; - } - } - - this._pushSubscriptionUsers = this._pushSubscriptionUsers.filter((u) => - ids.every((id) => id !== u.pushSubscriptionId) - ); - } - - async getSubscriptionByRecordAddressAndUserId( - recordName: string, - notificationAddress: string, - userId: string - ): Promise { - const subscription = this._subscriptions.find( - (s) => - s.recordName === recordName && - s.notificationAddress === notificationAddress && - s.userId === userId - ); - - return subscription || null; - } - - async getSubscriptionByRecordAddressAndPushSubscriptionId( - recordName: string, - notificationAddress: string, - pushSubscriptionId: string - ): Promise { - const subscription = this._subscriptions.find( - (s) => - s.recordName === recordName && - s.notificationAddress === notificationAddress && - s.pushSubscriptionId === pushSubscriptionId - ); - - return subscription || null; - } - async createSentPushNotifications( - notifications: SentPushNotification[] - ): Promise { - for (let notification of notifications) { - await this.saveSentPushNotification(notification); - } + this._packageVersions.push(version); } - async listActivePushSubscriptionsForNotification( - recordName: string, - notificationAddress: string - ): Promise { - const subscriptions = this._subscriptions.filter( - (s) => - s.recordName === recordName && - s.notificationAddress === notificationAddress - ); - const users: UserPushSubscription[] = []; - - for (let subscription of subscriptions) { - if (subscription.userId) { - const subscribedUsers = this._pushSubscriptionUsers.filter( - (u) => u.userId === subscription.userId - ); - for (let user of subscribedUsers) { - const sub = this._pushSubscriptions.find( - (s) => s.active && s.id === user.pushSubscriptionId - ); - if (sub) { - users.push({ - ...sub, - userId: user.userId, - subscriptionId: subscription.id, - }); - } - } - } else if (subscription.pushSubscriptionId) { - const sub = this._pushSubscriptions.find( - (s) => s.active && s.id === subscription.pushSubscriptionId - ); - if (sub) { - users.push({ - ...sub, - subscriptionId: subscription.id, - userId: null, - }); - } - } - } - - return uniqBy(users, (u) => u.id); - } - - async getSubscriptionById( - id: string - ): Promise { - return this._subscriptions.find((s) => s.id === id) || null; - } - - async countSubscriptionsForNotification( + async listPackageVersions( recordName: string, address: string - ): Promise { - let count = 0; - for (let s of this._subscriptions) { - if ( - s.recordName === recordName && - s.notificationAddress === address - ) { - count++; - } - } - - return count; - } - - async saveSubscription( - subscription: NotificationSubscription - ): Promise { - const exists = this._subscriptions.some( - (s) => - s.recordName === subscription.recordName && - s.notificationAddress === subscription.notificationAddress && - ((s.userId && s.userId === subscription.userId) || - (s.pushSubscriptionId && - s.pushSubscriptionId === - subscription.pushSubscriptionId)) - ); - - if (exists) { - return { - success: false, - errorCode: 'subscription_already_exists', - errorMessage: - 'This user is already subscribed to this notification.', - }; - } - - const index = this._subscriptions.findIndex( - (s) => s.id === subscription.id + ): Promise { + const versions = this._packageVersions.filter( + (v) => v.recordName === recordName && v.address === address ); - if (index >= 0) { - this._subscriptions[index] = { - ...subscription, - }; - } else { - this._subscriptions.push({ - ...subscription, - }); - } - - return { - success: true, - }; - } - - async deleteSubscription(id: string): Promise { - const index = this._subscriptions.findIndex((s) => s.id === id); - if (index >= 0) { - this._subscriptions.splice(index, 1); - } - } - - async saveSentNotification(notification: SentNotification): Promise { - const index = this._sentNotifications.findIndex( - (s) => s.id === notification.id - ); - if (index >= 0) { - this._sentNotifications[index] = { - ...notification, - }; - } else { - this._sentNotifications.push({ - ...notification, - }); - } - } - - async saveSentPushNotification( - notification: SentPushNotification - ): Promise { - const index = this._sentPushNotifications.findIndex( - (s) => s.id === notification.id - ); - if (index >= 0) { - this._sentPushNotifications[index] = { - ...notification, - }; - } else { - this._sentPushNotifications.push({ - ...notification, - }); - } - } - - async listSubscriptionsForNotification( - recordName: string, - notificationAddress: string - ): Promise { - return this._subscriptions.filter( - (s) => - s.recordName === recordName && - s.notificationAddress === notificationAddress - ); - } - - async listSubscriptionsForUser( - userId: string - ): Promise { - return this._subscriptions.filter((s) => s.userId === userId); + return versions.map((v) => ({ + recordName: v.recordName, + address: v.address, + version: v.version, + sha256: v.sha256, + auxSha256: v.auxSha256, + scriptSha256: v.scriptSha256, + entitlements: v.entitlements, + sizeInBytes: v.sizeInBytes, + createdAtMs: v.createdAtMs, + })); } async getSubscriptionMetrics( filter: SubscriptionFilter - ): Promise { + ): Promise { const info = await super.getSubscriptionMetrics(filter); - let totalItems = 0; - let totalSentNotificationsInPeriod = 0; - let totalSentPushNotificationsInPeriod = 0; + let totalPackages = 0; + let totalPackageVersions = 0; + let totalPackageVersionBytes = 0; const records = filter.ownerId ? await this.store.listRecordsByOwnerId(filter.ownerId) : await this.store.listRecordsByStudioId(filter.studioId); for (let record of records) { - totalItems += this.getItemRecord(record.name).size; - } + totalPackages += this.getItemRecord(record.name).size; - for (let send of this._sentNotifications) { - if (!records.some((r) => r.name === send.recordName)) { - continue; - } - - if ( - !info.currentPeriodStartMs || - send.sentTimeMs >= info.currentPeriodStartMs || - send.sentTimeMs <= info.currentPeriodEndMs - ) { - totalSentNotificationsInPeriod++; - } - - for (let u of this._sentPushNotifications) { - if (u.sentNotificationId === send.id) { - totalSentPushNotificationsInPeriod++; + for (let version of this._packageVersions) { + if (version.recordName !== record.name) { + continue; } + + totalPackageVersions++; + totalPackageVersionBytes += version.sizeInBytes; } } return { ...info, - totalItems, - totalSentNotificationsInPeriod, - totalSentPushNotificationsInPeriod, + totalPackages, + totalPackageVersions, + totalPackageVersionBytes, }; } } diff --git a/src/aux-records/packages/PackageRecordsController.spec.ts b/src/aux-records/packages/PackageRecordsController.spec.ts index 24ca89e60..3cc69b23c 100644 --- a/src/aux-records/packages/PackageRecordsController.spec.ts +++ b/src/aux-records/packages/PackageRecordsController.spec.ts @@ -3,15 +3,7 @@ import { TestControllers, testCrudRecordsController, } from '../crud/CrudRecordsControllerTests'; -import { MemoryNotificationRecordsStore } from './MemoryNotificationRecordsStore'; -import { - NotificationRecord, - NotificationRecordsStore, -} from './NotificationRecordsStore'; -import { - NotificationRecordsController, - SubscribeToNotificationSuccess, -} from './NotificationRecordsController'; +import { PackageRecordsController } from './PackageRecordsController'; import { buildSubscriptionConfig, subscriptionConfigBuilder, @@ -24,31 +16,25 @@ import { PRIVATE_MARKER, PUBLIC_READ_MARKER, } from '@casual-simulation/aux-common'; -import { - SUBSCRIPTION_ID_NAMESPACE, - WebPushInterface, -} from './WebPushInterface'; import { v5 as uuidv5 } from 'uuid'; +import { PackageRecord, PackageRecordsStore } from './PackageRecordsStore'; +import { MemoryPackageRecordsStore } from './MemoryPackageRecordsStore'; console.log = jest.fn(); console.error = jest.fn(); -describe('NotificationRecordsController', () => { +describe('PackageRecordsController', () => { testCrudRecordsController< - NotificationRecord, - NotificationRecordsStore, - NotificationRecordsController + PackageRecord, + PackageRecordsStore, + PackageRecordsController >( false, 'notification', - (services) => new MemoryNotificationRecordsStore(services.store), + (services) => new MemoryPackageRecordsStore(services.store), (config, services) => - new NotificationRecordsController({ + new PackageRecordsController({ ...config, - pushInterface: { - getServerApplicationKey: jest.fn(), - sendNotification: jest.fn(), - }, }), (item) => ({ address: item.address, @@ -66,16 +52,15 @@ describe('NotificationRecordsController', () => { ); let store: MemoryStore; - let itemsStore: MemoryNotificationRecordsStore; + let itemsStore: MemoryPackageRecordsStore; let records: RecordsController; let policies: PolicyController; - let manager: NotificationRecordsController; + let manager: PackageRecordsController; let key: string; let subjectlessKey: string; let realDateNow: any; let dateNowMock: jest.Mock; let services: TestControllers; - let pushInterface: jest.Mocked; let userId: string; let sessionKey: string; @@ -95,26 +80,21 @@ describe('NotificationRecordsController', () => { // }; const context = await setupTestContext< - NotificationRecord, - NotificationRecordsStore, - NotificationRecordsController + PackageRecord, + PackageRecordsStore, + PackageRecordsController >( - (services) => new MemoryNotificationRecordsStore(services.store), + (services) => new MemoryPackageRecordsStore(services.store), (config, services) => { - pushInterface = { - getServerApplicationKey: jest.fn(), - sendNotification: jest.fn(), - }; - return new NotificationRecordsController({ + return new PackageRecordsController({ ...config, - pushInterface, }); } ); services = context.services; store = context.store; - itemsStore = context.itemsStore as MemoryNotificationRecordsStore; + itemsStore = context.itemsStore as MemoryPackageRecordsStore; records = context.services.records; policies = context.services.policies; manager = context.manager; @@ -201,15 +181,15 @@ describe('NotificationRecordsController', () => { describe('recordItem()', () => { describe('create', () => { - it('should return subscription_limit_reached when the user has reached limit of notifications', async () => { + it('should return subscription_limit_reached when the user has reached limit of packages', async () => { store.subscriptionConfiguration = buildSubscriptionConfig( (config) => config.addSubscription('sub1', (sub) => sub .withTier('tier1') .withAllDefaultFeatures() - .withNotifications() - .withNotificationsMaxItems(1) + .withPackages() + .withPackagesMaxItems(1) ) ); diff --git a/src/aux-records/packages/PackageRecordsController.ts b/src/aux-records/packages/PackageRecordsController.ts index 70788d6b8..d0fed0d71 100644 --- a/src/aux-records/packages/PackageRecordsController.ts +++ b/src/aux-records/packages/PackageRecordsController.ts @@ -18,23 +18,17 @@ import { CheckSubscriptionMetricsSuccess, } from '../crud'; import { - NotificationAction, - NotificationActionUI, - NotificationRecord, PackageRecordsStore, - NotificationSubscription, - NotificationSubscriptionMetrics, - SentPushNotification, - UserPushSubscription, + PackageRecord, + PackageSubscriptionMetrics, } from './PackageRecordsStore'; import { getNotificationFeatures, + getPackageFeatures, NotificationFeaturesConfiguration, + PackageFeaturesConfiguration, SubscriptionConfiguration, } from '../SubscriptionConfiguration'; -import { traced } from '../tracing/TracingDecorators'; -import { SpanStatusCode, trace } from '@opentelemetry/api'; -import { v7 as uuidv7, v5 as uuidv5 } from 'uuid'; const TRACE_NAME = 'PackageRecordsController'; @@ -43,7 +37,7 @@ const TRACE_NAME = 'PackageRecordsController'; */ export interface PackageRecordsConfiguration extends Omit< - CrudRecordsConfiguration, + CrudRecordsConfiguration, 'resourceKind' | 'allowRecordKeys' | 'name' > {} @@ -51,7 +45,7 @@ export interface PackageRecordsConfiguration * Defines a controller that can be used to interact with NotificationRecords. */ export class PackageRecordsController extends CrudRecordsController< - NotificationRecord, + PackageRecord, PackageRecordsStore > { constructor(config: PackageRecordsConfiguration) { @@ -68,15 +62,15 @@ export class PackageRecordsController extends CrudRecordsController< authorization: | AuthorizeUserAndInstancesSuccess | AuthorizeUserAndInstancesForResourcesSuccess, - item?: NotificationRecord - ): Promise { + item?: PackageRecord + ): Promise { const config = await this.config.getSubscriptionConfiguration(); const metrics = await this.store.getSubscriptionMetrics({ ownerId: context.recordOwnerId, studioId: context.recordStudioId, }); - const features = getNotificationFeatures( + const features = getPackageFeatures( config, metrics.subscriptionStatus, metrics.subscriptionId, @@ -89,8 +83,7 @@ export class PackageRecordsController extends CrudRecordsController< return { success: false, errorCode: 'not_authorized', - errorMessage: - 'Notifications are not allowed for this subscription.', + errorMessage: 'Packages are not allowed for this subscription.', }; } @@ -100,41 +93,7 @@ export class PackageRecordsController extends CrudRecordsController< success: false, errorCode: 'subscription_limit_reached', errorMessage: - 'The maximum number of notification items has been reached for your subscription.', - }; - } - } - - if ( - action === 'subscribe' && - typeof features.maxSubscribersPerItem === 'number' - ) { - const totalSubscriptions = - await this.store.countSubscriptionsForNotification( - context.recordName, - item.address - ); - if (totalSubscriptions >= features.maxSubscribersPerItem) { - return { - success: false, - errorCode: 'subscription_limit_reached', - errorMessage: - 'The maximum number of subscriptions has been reached for this notification.', - }; - } - } - - if (action === 'send') { - if ( - typeof features.maxSentNotificationsPerPeriod === 'number' && - metrics.totalSentNotificationsInPeriod >= - features.maxSentNotificationsPerPeriod - ) { - return { - success: false, - errorCode: 'subscription_limit_reached', - errorMessage: - 'The maximum number of sent notifications has been reached for this period.', + 'The maximum number of package items has been reached for your subscription.', }; } } @@ -147,3 +106,14 @@ export class PackageRecordsController extends CrudRecordsController< }; } } + +export type PackageRecordsSubscriptionMetricsResult = + | PackageRecordsSubscriptionMetricsSuccess + | CheckSubscriptionMetricsFailure; + +export interface PackageRecordsSubscriptionMetricsSuccess + extends CheckSubscriptionMetricsSuccess { + config: SubscriptionConfiguration; + metrics: PackageSubscriptionMetrics; + features: PackageFeaturesConfiguration; +} diff --git a/src/aux-records/packages/PackageRecordsStore.ts b/src/aux-records/packages/PackageRecordsStore.ts index 4127dc756..778e40f3a 100644 --- a/src/aux-records/packages/PackageRecordsStore.ts +++ b/src/aux-records/packages/PackageRecordsStore.ts @@ -96,6 +96,11 @@ export interface PackageRecordVersion { */ readme: string; + /** + * The size of the package version in bytes. + */ + sizeInBytes: number; + /** * The unix time in miliseconds that this package version was created at. */ @@ -138,6 +143,11 @@ export interface ListedPackageVersion { */ entitlements: string[]; + /** + * The size of the version in bytes. + */ + sizeInBytes: number; + /** * The unix time in miliseconds that this version was created at. */ @@ -171,7 +181,7 @@ export interface PackageSubscriptionMetrics extends CrudSubscriptionMetrics { /** * The total number of packages stored in the subscription. */ - totalPackages: number; + totalItems: number; /** * The total number of package versions stored in the subscription.