perspectapi-ts-sdk 6.1.1 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +238 -2
- package/dist/index.mjs +231 -1
- package/package.json +1 -1
- package/src/ab/ab-client.ts +262 -0
- package/src/ab/bucketing.ts +56 -0
- package/src/index.ts +16 -1
- package/src/loaders.ts +77 -0
package/dist/index.d.mts
CHANGED
|
@@ -4129,6 +4129,21 @@ declare class CloudflareKVCacheAdapter implements CacheAdapter {
|
|
|
4129
4129
|
clear(): Promise<void>;
|
|
4130
4130
|
}
|
|
4131
4131
|
|
|
4132
|
+
/**
|
|
4133
|
+
* Simple in-memory cache adapter primarily suited for development and testing.
|
|
4134
|
+
*/
|
|
4135
|
+
|
|
4136
|
+
declare class InMemoryCacheAdapter implements CacheAdapter {
|
|
4137
|
+
private store;
|
|
4138
|
+
get(key: string): Promise<string | undefined>;
|
|
4139
|
+
set(key: string, value: string, options?: {
|
|
4140
|
+
ttlSeconds?: number;
|
|
4141
|
+
}): Promise<void>;
|
|
4142
|
+
delete(key: string): Promise<void>;
|
|
4143
|
+
deleteMany(keys: string[]): Promise<void>;
|
|
4144
|
+
clear(): Promise<void>;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4132
4147
|
/**
|
|
4133
4148
|
* No-op cache adapter that disables caching while preserving the cache contract.
|
|
4134
4149
|
*/
|
|
@@ -4141,6 +4156,64 @@ declare class NoopCacheAdapter implements CacheAdapter {
|
|
|
4141
4156
|
clear(): Promise<void>;
|
|
4142
4157
|
}
|
|
4143
4158
|
|
|
4159
|
+
interface CreatePerspectAbOptions {
|
|
4160
|
+
apiKey: string;
|
|
4161
|
+
siteName: string;
|
|
4162
|
+
baseUrl?: string;
|
|
4163
|
+
/** Pluggable cache adapter — CloudflareKVCacheAdapter, InMemoryCacheAdapter, or NoopCacheAdapter. */
|
|
4164
|
+
cache?: CacheAdapter;
|
|
4165
|
+
/** ExecutionContext (CF Workers) or any object with waitUntil — enables background tasks. */
|
|
4166
|
+
ctx?: {
|
|
4167
|
+
waitUntil(p: Promise<unknown>): void;
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
interface AbForRequestResult {
|
|
4171
|
+
ab: AbClient;
|
|
4172
|
+
/** Merge into the outgoing response — may carry a freshly minted perspect_vid cookie. */
|
|
4173
|
+
responseHeaders: Headers;
|
|
4174
|
+
}
|
|
4175
|
+
interface AbVariantResult {
|
|
4176
|
+
variant: string | null;
|
|
4177
|
+
enrolled: boolean;
|
|
4178
|
+
config: Record<string, unknown> | null;
|
|
4179
|
+
}
|
|
4180
|
+
interface AbClient {
|
|
4181
|
+
getVariant(flagKey: string): Promise<AbVariantResult>;
|
|
4182
|
+
track(event: string, properties?: Record<string, unknown>): Promise<void>;
|
|
4183
|
+
}
|
|
4184
|
+
interface PerspectAb {
|
|
4185
|
+
forRequest(request: Request): Promise<AbForRequestResult>;
|
|
4186
|
+
refreshConfig(): Promise<void>;
|
|
4187
|
+
}
|
|
4188
|
+
declare function createPerspectAb(options: CreatePerspectAbOptions): PerspectAb;
|
|
4189
|
+
|
|
4190
|
+
/**
|
|
4191
|
+
* Deterministic bucketing for A/B assignments.
|
|
4192
|
+
*
|
|
4193
|
+
* Both the site SDK (local assignment) and the platform event endpoint
|
|
4194
|
+
* (server-side validation) compute the same hash → variant mapping.
|
|
4195
|
+
* Any change here must ship to both simultaneously, or assignments and
|
|
4196
|
+
* validation will diverge.
|
|
4197
|
+
*/
|
|
4198
|
+
/**
|
|
4199
|
+
* FNV-1a 32-bit over UTF-8 bytes, reduced mod 10000.
|
|
4200
|
+
* Pure function, no crypto — determinism and portability beat pre-image
|
|
4201
|
+
* resistance here. The attack surface on the mapping is the same for
|
|
4202
|
+
* both sides; what matters is that site and platform agree bit-for-bit.
|
|
4203
|
+
*/
|
|
4204
|
+
declare function bucketFor(visitorId: string, flagKey: string, version: number): number;
|
|
4205
|
+
/**
|
|
4206
|
+
* Resolve a bucket to a variant using cumulative weightBp ranges.
|
|
4207
|
+
* Variants are consumed in the order given — callers must sort variants
|
|
4208
|
+
* consistently on both sides (we sort by key).
|
|
4209
|
+
*
|
|
4210
|
+
* Returns null iff bucket >= trafficAllocationBp (visitor not enrolled).
|
|
4211
|
+
*/
|
|
4212
|
+
declare function variantForBucket(bucket: number, variants: {
|
|
4213
|
+
key: string;
|
|
4214
|
+
weightBp: number;
|
|
4215
|
+
}[], trafficAllocationBp: number): string | null;
|
|
4216
|
+
|
|
4144
4217
|
/**
|
|
4145
4218
|
* Cloudflare Image Resizing Integration
|
|
4146
4219
|
* Transforms images on-the-fly using Cloudflare's Image Resizing service
|
|
@@ -4383,6 +4456,17 @@ interface LoadContentBySlugOptions extends LoaderOptions {
|
|
|
4383
4456
|
* Load a single content item (post or page) by slug.
|
|
4384
4457
|
*/
|
|
4385
4458
|
declare function loadContentBySlug(options: LoadContentBySlugOptions): Promise<BlogPost | null>;
|
|
4459
|
+
interface LoadBlockBySlugOptions extends LoaderOptions {
|
|
4460
|
+
slug: string;
|
|
4461
|
+
}
|
|
4462
|
+
/**
|
|
4463
|
+
* Load published blocks for a site.
|
|
4464
|
+
*/
|
|
4465
|
+
declare function loadBlocks(options: LoadContentOptions): Promise<BlogPost[]>;
|
|
4466
|
+
/**
|
|
4467
|
+
* Load a single block by slug.
|
|
4468
|
+
*/
|
|
4469
|
+
declare function loadBlockBySlug(options: LoadBlockBySlugOptions): Promise<BlogPost | null>;
|
|
4386
4470
|
/**
|
|
4387
4471
|
* Load all published content (pages + posts).
|
|
4388
4472
|
*/
|
|
@@ -4434,4 +4518,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
|
|
|
4434
4518
|
error: string;
|
|
4435
4519
|
}>;
|
|
4436
4520
|
|
|
4437
|
-
export { type AddCollectionItemRequest, type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, BaseClient, type BlogPost, type BundleCollection, type BundleCollectionItem, type BundleCollectionItemWithProduct, BundlesClient, type CacheConfig, CacheManager, type CancelSubscriptionRequest, type CancelSubscriptionResponse, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, CloudflareKVCacheAdapter, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateBundleCollectionRequest, type CreateBundleGroupRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateProductSkuRequest, type CreateSiteRequest, type CreateWebhookRequest, type CreditBalance, type CreditBalanceWithTransactions, type CreditTransaction, DEFAULT_IMAGE_SIZES, type GrantCreditRequest, HttpClient, type HttpMethod, type ImageTransformOptions, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, type NewsletterCampaignDetail, type NewsletterCampaignListResponse, type NewsletterCampaignSummary, type NewsletterCampaignTestSendRequest, type NewsletterCampaignTestSendResponse, NewsletterClient, type NewsletterConfirmResponse, type NewsletterExportCreateRequest, type NewsletterExportCreateResponse, type NewsletterList, type NewsletterManagementCampaign, type NewsletterManagementCampaignListResponse, NewsletterManagementClient, type NewsletterManagementList, type NewsletterManagementListMembership, type NewsletterManagementPagination, type NewsletterManagementSeries, type NewsletterManagementStatsResponse, type NewsletterManagementSubscription, type NewsletterManagementSubscriptionsListResponse, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterSubscriptionImportRowRequest, type NewsletterSubscriptionMembershipUpdateRequest, type NewsletterSubscriptionSyncRequest, type NewsletterSubscriptionSyncResponse, type NewsletterSubscriptionsBulkAction, type NewsletterSubscriptionsBulkOutcome, type NewsletterSubscriptionsBulkUpdateRequest, type NewsletterSubscriptionsBulkUpdateResponse, type NewsletterSubscriptionsImportRequest, type NewsletterSubscriptionsImportResponse, type NewsletterSubscriptionsImportRowResult, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, PerspectApiV2Client, PerspectV2Error, type Product, type ProductBundleGroup, type ProductQueryParams, type ProductSku, type ProductSkuMediaItem, type ProductSkuOption, ProductsClient, type RequestOptions, type RequestOtpRequest, type ResponsiveImageSizes, type SetProfileValueRequest, type Site, type SiteUser, type SiteUserOrder, type SiteUserProfile, type SiteUserSubscription, SiteUsersClient, SitesClient, type SubscriptionCancellationMode, type UpdateApiKeyRequest, type UpdateContentRequest, type UpdateSiteUserRequest, type User, V1_DEPRECATION_NOTICE, V1_SUNSET_DATE, type V2ApiKey, type V2CancelSubscriptionResult, type V2Category, type V2CategoryCreateParams, type V2CategoryUpdateParams, type V2Collection, type V2CollectionCreateParams, type V2CollectionItem, type V2CollectionUpdateParams, type V2ContactSubmission, type V2Content, type V2ContentCreateParams, type V2ContentListParams, type V2ContentUpdateParams, type V2CreditBalance, type V2CreditTransaction, type V2Deleted, type V2Error, type V2ErrorType, type V2GrantCreditParams, type V2GrantCreditResult, type V2List, type V2Media, type V2NewsletterCampaign, type V2NewsletterImportRequest, type V2NewsletterImportResult, type V2NewsletterList, type V2NewsletterListCreateParams, type V2NewsletterListUpdateParams, type V2NewsletterSubscription, type V2NewsletterSubscriptionListMembershipUpdate, type V2NewsletterSyncInput, type V2NewsletterSyncResult, type V2NewsletterTrackingResponse, type V2Object, type V2Order, type V2OrderAddress, type V2OrderCreateParams, type V2OrderCreateResult, type V2OrderFulfillmentUpdate, type V2OrderLineItem, type V2OrderLineItemPriceData, type V2OrderListParams, type V2OrderTaxRequest, type V2Organization, type V2PaginationParams, type V2Product, type V2ProductCreateParams, type V2ProductListParams, type V2ProductUpdateParams, type V2Site, type V2SiteUser, type V2SiteUserListParams, type V2SiteUserMeUpdateParams, type V2SiteUserProfile, type V2SiteUserSubscription, type V2SiteUserUpdateParams, type V2SiteUserWithProfile, type V2SubscriptionCancelParams, type V2SubscriptionChangePlanParams, type V2SubscriptionPauseParams, type V2Webhook, type V2WebhookCreateParams, type V2WebhookUpdateParams, type VerifyOtpRequest, type VerifyOtpResponse, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, createPerspectApiV2Client, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
|
4521
|
+
export { type AbClient, type AbForRequestResult, type AbVariantResult, type AddCollectionItemRequest, type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, BaseClient, type BlogPost, type BundleCollection, type BundleCollectionItem, type BundleCollectionItemWithProduct, BundlesClient, type CacheAdapter, type CacheConfig, CacheManager, type CancelSubscriptionRequest, type CancelSubscriptionResponse, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, CloudflareKVCacheAdapter, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateBundleCollectionRequest, type CreateBundleGroupRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreatePerspectAbOptions, type CreateProductRequest, type CreateProductSkuRequest, type CreateSiteRequest, type CreateWebhookRequest, type CreditBalance, type CreditBalanceWithTransactions, type CreditTransaction, DEFAULT_IMAGE_SIZES, type GrantCreditRequest, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadBlockBySlugOptions, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, type NewsletterCampaignDetail, type NewsletterCampaignListResponse, type NewsletterCampaignSummary, type NewsletterCampaignTestSendRequest, type NewsletterCampaignTestSendResponse, NewsletterClient, type NewsletterConfirmResponse, type NewsletterExportCreateRequest, type NewsletterExportCreateResponse, type NewsletterList, type NewsletterManagementCampaign, type NewsletterManagementCampaignListResponse, NewsletterManagementClient, type NewsletterManagementList, type NewsletterManagementListMembership, type NewsletterManagementPagination, type NewsletterManagementSeries, type NewsletterManagementStatsResponse, type NewsletterManagementSubscription, type NewsletterManagementSubscriptionsListResponse, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterSubscriptionImportRowRequest, type NewsletterSubscriptionMembershipUpdateRequest, type NewsletterSubscriptionSyncRequest, type NewsletterSubscriptionSyncResponse, type NewsletterSubscriptionsBulkAction, type NewsletterSubscriptionsBulkOutcome, type NewsletterSubscriptionsBulkUpdateRequest, type NewsletterSubscriptionsBulkUpdateResponse, type NewsletterSubscriptionsImportRequest, type NewsletterSubscriptionsImportResponse, type NewsletterSubscriptionsImportRowResult, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, type PerspectAb, PerspectApiClient, type PerspectApiConfig, PerspectApiV2Client, PerspectV2Error, type Product, type ProductBundleGroup, type ProductQueryParams, type ProductSku, type ProductSkuMediaItem, type ProductSkuOption, ProductsClient, type RequestOptions, type RequestOtpRequest, type ResponsiveImageSizes, type SetProfileValueRequest, type Site, type SiteUser, type SiteUserOrder, type SiteUserProfile, type SiteUserSubscription, SiteUsersClient, SitesClient, type SubscriptionCancellationMode, type UpdateApiKeyRequest, type UpdateContentRequest, type UpdateSiteUserRequest, type User, V1_DEPRECATION_NOTICE, V1_SUNSET_DATE, type V2ApiKey, type V2CancelSubscriptionResult, type V2Category, type V2CategoryCreateParams, type V2CategoryUpdateParams, type V2Collection, type V2CollectionCreateParams, type V2CollectionItem, type V2CollectionUpdateParams, type V2ContactSubmission, type V2Content, type V2ContentCreateParams, type V2ContentListParams, type V2ContentUpdateParams, type V2CreditBalance, type V2CreditTransaction, type V2Deleted, type V2Error, type V2ErrorType, type V2GrantCreditParams, type V2GrantCreditResult, type V2List, type V2Media, type V2NewsletterCampaign, type V2NewsletterImportRequest, type V2NewsletterImportResult, type V2NewsletterList, type V2NewsletterListCreateParams, type V2NewsletterListUpdateParams, type V2NewsletterSubscription, type V2NewsletterSubscriptionListMembershipUpdate, type V2NewsletterSyncInput, type V2NewsletterSyncResult, type V2NewsletterTrackingResponse, type V2Object, type V2Order, type V2OrderAddress, type V2OrderCreateParams, type V2OrderCreateResult, type V2OrderFulfillmentUpdate, type V2OrderLineItem, type V2OrderLineItemPriceData, type V2OrderListParams, type V2OrderTaxRequest, type V2Organization, type V2PaginationParams, type V2Product, type V2ProductCreateParams, type V2ProductListParams, type V2ProductUpdateParams, type V2Site, type V2SiteUser, type V2SiteUserListParams, type V2SiteUserMeUpdateParams, type V2SiteUserProfile, type V2SiteUserSubscription, type V2SiteUserUpdateParams, type V2SiteUserWithProfile, type V2SubscriptionCancelParams, type V2SubscriptionChangePlanParams, type V2SubscriptionPauseParams, type V2Webhook, type V2WebhookCreateParams, type V2WebhookUpdateParams, type VerifyOtpRequest, type VerifyOtpResponse, type Webhook, WebhooksClient, bucketFor, buildImageUrl, createApiError, createCheckoutSession, createPerspectAb, createPerspectApiClient, createPerspectApiV2Client, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadBlockBySlug, loadBlocks, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct, variantForBucket };
|
package/dist/index.d.ts
CHANGED
|
@@ -4129,6 +4129,21 @@ declare class CloudflareKVCacheAdapter implements CacheAdapter {
|
|
|
4129
4129
|
clear(): Promise<void>;
|
|
4130
4130
|
}
|
|
4131
4131
|
|
|
4132
|
+
/**
|
|
4133
|
+
* Simple in-memory cache adapter primarily suited for development and testing.
|
|
4134
|
+
*/
|
|
4135
|
+
|
|
4136
|
+
declare class InMemoryCacheAdapter implements CacheAdapter {
|
|
4137
|
+
private store;
|
|
4138
|
+
get(key: string): Promise<string | undefined>;
|
|
4139
|
+
set(key: string, value: string, options?: {
|
|
4140
|
+
ttlSeconds?: number;
|
|
4141
|
+
}): Promise<void>;
|
|
4142
|
+
delete(key: string): Promise<void>;
|
|
4143
|
+
deleteMany(keys: string[]): Promise<void>;
|
|
4144
|
+
clear(): Promise<void>;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4132
4147
|
/**
|
|
4133
4148
|
* No-op cache adapter that disables caching while preserving the cache contract.
|
|
4134
4149
|
*/
|
|
@@ -4141,6 +4156,64 @@ declare class NoopCacheAdapter implements CacheAdapter {
|
|
|
4141
4156
|
clear(): Promise<void>;
|
|
4142
4157
|
}
|
|
4143
4158
|
|
|
4159
|
+
interface CreatePerspectAbOptions {
|
|
4160
|
+
apiKey: string;
|
|
4161
|
+
siteName: string;
|
|
4162
|
+
baseUrl?: string;
|
|
4163
|
+
/** Pluggable cache adapter — CloudflareKVCacheAdapter, InMemoryCacheAdapter, or NoopCacheAdapter. */
|
|
4164
|
+
cache?: CacheAdapter;
|
|
4165
|
+
/** ExecutionContext (CF Workers) or any object with waitUntil — enables background tasks. */
|
|
4166
|
+
ctx?: {
|
|
4167
|
+
waitUntil(p: Promise<unknown>): void;
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
interface AbForRequestResult {
|
|
4171
|
+
ab: AbClient;
|
|
4172
|
+
/** Merge into the outgoing response — may carry a freshly minted perspect_vid cookie. */
|
|
4173
|
+
responseHeaders: Headers;
|
|
4174
|
+
}
|
|
4175
|
+
interface AbVariantResult {
|
|
4176
|
+
variant: string | null;
|
|
4177
|
+
enrolled: boolean;
|
|
4178
|
+
config: Record<string, unknown> | null;
|
|
4179
|
+
}
|
|
4180
|
+
interface AbClient {
|
|
4181
|
+
getVariant(flagKey: string): Promise<AbVariantResult>;
|
|
4182
|
+
track(event: string, properties?: Record<string, unknown>): Promise<void>;
|
|
4183
|
+
}
|
|
4184
|
+
interface PerspectAb {
|
|
4185
|
+
forRequest(request: Request): Promise<AbForRequestResult>;
|
|
4186
|
+
refreshConfig(): Promise<void>;
|
|
4187
|
+
}
|
|
4188
|
+
declare function createPerspectAb(options: CreatePerspectAbOptions): PerspectAb;
|
|
4189
|
+
|
|
4190
|
+
/**
|
|
4191
|
+
* Deterministic bucketing for A/B assignments.
|
|
4192
|
+
*
|
|
4193
|
+
* Both the site SDK (local assignment) and the platform event endpoint
|
|
4194
|
+
* (server-side validation) compute the same hash → variant mapping.
|
|
4195
|
+
* Any change here must ship to both simultaneously, or assignments and
|
|
4196
|
+
* validation will diverge.
|
|
4197
|
+
*/
|
|
4198
|
+
/**
|
|
4199
|
+
* FNV-1a 32-bit over UTF-8 bytes, reduced mod 10000.
|
|
4200
|
+
* Pure function, no crypto — determinism and portability beat pre-image
|
|
4201
|
+
* resistance here. The attack surface on the mapping is the same for
|
|
4202
|
+
* both sides; what matters is that site and platform agree bit-for-bit.
|
|
4203
|
+
*/
|
|
4204
|
+
declare function bucketFor(visitorId: string, flagKey: string, version: number): number;
|
|
4205
|
+
/**
|
|
4206
|
+
* Resolve a bucket to a variant using cumulative weightBp ranges.
|
|
4207
|
+
* Variants are consumed in the order given — callers must sort variants
|
|
4208
|
+
* consistently on both sides (we sort by key).
|
|
4209
|
+
*
|
|
4210
|
+
* Returns null iff bucket >= trafficAllocationBp (visitor not enrolled).
|
|
4211
|
+
*/
|
|
4212
|
+
declare function variantForBucket(bucket: number, variants: {
|
|
4213
|
+
key: string;
|
|
4214
|
+
weightBp: number;
|
|
4215
|
+
}[], trafficAllocationBp: number): string | null;
|
|
4216
|
+
|
|
4144
4217
|
/**
|
|
4145
4218
|
* Cloudflare Image Resizing Integration
|
|
4146
4219
|
* Transforms images on-the-fly using Cloudflare's Image Resizing service
|
|
@@ -4383,6 +4456,17 @@ interface LoadContentBySlugOptions extends LoaderOptions {
|
|
|
4383
4456
|
* Load a single content item (post or page) by slug.
|
|
4384
4457
|
*/
|
|
4385
4458
|
declare function loadContentBySlug(options: LoadContentBySlugOptions): Promise<BlogPost | null>;
|
|
4459
|
+
interface LoadBlockBySlugOptions extends LoaderOptions {
|
|
4460
|
+
slug: string;
|
|
4461
|
+
}
|
|
4462
|
+
/**
|
|
4463
|
+
* Load published blocks for a site.
|
|
4464
|
+
*/
|
|
4465
|
+
declare function loadBlocks(options: LoadContentOptions): Promise<BlogPost[]>;
|
|
4466
|
+
/**
|
|
4467
|
+
* Load a single block by slug.
|
|
4468
|
+
*/
|
|
4469
|
+
declare function loadBlockBySlug(options: LoadBlockBySlugOptions): Promise<BlogPost | null>;
|
|
4386
4470
|
/**
|
|
4387
4471
|
* Load all published content (pages + posts).
|
|
4388
4472
|
*/
|
|
@@ -4434,4 +4518,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
|
|
|
4434
4518
|
error: string;
|
|
4435
4519
|
}>;
|
|
4436
4520
|
|
|
4437
|
-
export { type AddCollectionItemRequest, type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, BaseClient, type BlogPost, type BundleCollection, type BundleCollectionItem, type BundleCollectionItemWithProduct, BundlesClient, type CacheConfig, CacheManager, type CancelSubscriptionRequest, type CancelSubscriptionResponse, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, CloudflareKVCacheAdapter, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateBundleCollectionRequest, type CreateBundleGroupRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateProductSkuRequest, type CreateSiteRequest, type CreateWebhookRequest, type CreditBalance, type CreditBalanceWithTransactions, type CreditTransaction, DEFAULT_IMAGE_SIZES, type GrantCreditRequest, HttpClient, type HttpMethod, type ImageTransformOptions, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, type NewsletterCampaignDetail, type NewsletterCampaignListResponse, type NewsletterCampaignSummary, type NewsletterCampaignTestSendRequest, type NewsletterCampaignTestSendResponse, NewsletterClient, type NewsletterConfirmResponse, type NewsletterExportCreateRequest, type NewsletterExportCreateResponse, type NewsletterList, type NewsletterManagementCampaign, type NewsletterManagementCampaignListResponse, NewsletterManagementClient, type NewsletterManagementList, type NewsletterManagementListMembership, type NewsletterManagementPagination, type NewsletterManagementSeries, type NewsletterManagementStatsResponse, type NewsletterManagementSubscription, type NewsletterManagementSubscriptionsListResponse, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterSubscriptionImportRowRequest, type NewsletterSubscriptionMembershipUpdateRequest, type NewsletterSubscriptionSyncRequest, type NewsletterSubscriptionSyncResponse, type NewsletterSubscriptionsBulkAction, type NewsletterSubscriptionsBulkOutcome, type NewsletterSubscriptionsBulkUpdateRequest, type NewsletterSubscriptionsBulkUpdateResponse, type NewsletterSubscriptionsImportRequest, type NewsletterSubscriptionsImportResponse, type NewsletterSubscriptionsImportRowResult, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, PerspectApiV2Client, PerspectV2Error, type Product, type ProductBundleGroup, type ProductQueryParams, type ProductSku, type ProductSkuMediaItem, type ProductSkuOption, ProductsClient, type RequestOptions, type RequestOtpRequest, type ResponsiveImageSizes, type SetProfileValueRequest, type Site, type SiteUser, type SiteUserOrder, type SiteUserProfile, type SiteUserSubscription, SiteUsersClient, SitesClient, type SubscriptionCancellationMode, type UpdateApiKeyRequest, type UpdateContentRequest, type UpdateSiteUserRequest, type User, V1_DEPRECATION_NOTICE, V1_SUNSET_DATE, type V2ApiKey, type V2CancelSubscriptionResult, type V2Category, type V2CategoryCreateParams, type V2CategoryUpdateParams, type V2Collection, type V2CollectionCreateParams, type V2CollectionItem, type V2CollectionUpdateParams, type V2ContactSubmission, type V2Content, type V2ContentCreateParams, type V2ContentListParams, type V2ContentUpdateParams, type V2CreditBalance, type V2CreditTransaction, type V2Deleted, type V2Error, type V2ErrorType, type V2GrantCreditParams, type V2GrantCreditResult, type V2List, type V2Media, type V2NewsletterCampaign, type V2NewsletterImportRequest, type V2NewsletterImportResult, type V2NewsletterList, type V2NewsletterListCreateParams, type V2NewsletterListUpdateParams, type V2NewsletterSubscription, type V2NewsletterSubscriptionListMembershipUpdate, type V2NewsletterSyncInput, type V2NewsletterSyncResult, type V2NewsletterTrackingResponse, type V2Object, type V2Order, type V2OrderAddress, type V2OrderCreateParams, type V2OrderCreateResult, type V2OrderFulfillmentUpdate, type V2OrderLineItem, type V2OrderLineItemPriceData, type V2OrderListParams, type V2OrderTaxRequest, type V2Organization, type V2PaginationParams, type V2Product, type V2ProductCreateParams, type V2ProductListParams, type V2ProductUpdateParams, type V2Site, type V2SiteUser, type V2SiteUserListParams, type V2SiteUserMeUpdateParams, type V2SiteUserProfile, type V2SiteUserSubscription, type V2SiteUserUpdateParams, type V2SiteUserWithProfile, type V2SubscriptionCancelParams, type V2SubscriptionChangePlanParams, type V2SubscriptionPauseParams, type V2Webhook, type V2WebhookCreateParams, type V2WebhookUpdateParams, type VerifyOtpRequest, type VerifyOtpResponse, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, createPerspectApiV2Client, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
|
4521
|
+
export { type AbClient, type AbForRequestResult, type AbVariantResult, type AddCollectionItemRequest, type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, BaseClient, type BlogPost, type BundleCollection, type BundleCollectionItem, type BundleCollectionItemWithProduct, BundlesClient, type CacheAdapter, type CacheConfig, CacheManager, type CancelSubscriptionRequest, type CancelSubscriptionResponse, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, CloudflareKVCacheAdapter, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateBundleCollectionRequest, type CreateBundleGroupRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreatePerspectAbOptions, type CreateProductRequest, type CreateProductSkuRequest, type CreateSiteRequest, type CreateWebhookRequest, type CreditBalance, type CreditBalanceWithTransactions, type CreditTransaction, DEFAULT_IMAGE_SIZES, type GrantCreditRequest, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadBlockBySlugOptions, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, type NewsletterCampaignDetail, type NewsletterCampaignListResponse, type NewsletterCampaignSummary, type NewsletterCampaignTestSendRequest, type NewsletterCampaignTestSendResponse, NewsletterClient, type NewsletterConfirmResponse, type NewsletterExportCreateRequest, type NewsletterExportCreateResponse, type NewsletterList, type NewsletterManagementCampaign, type NewsletterManagementCampaignListResponse, NewsletterManagementClient, type NewsletterManagementList, type NewsletterManagementListMembership, type NewsletterManagementPagination, type NewsletterManagementSeries, type NewsletterManagementStatsResponse, type NewsletterManagementSubscription, type NewsletterManagementSubscriptionsListResponse, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterSubscriptionImportRowRequest, type NewsletterSubscriptionMembershipUpdateRequest, type NewsletterSubscriptionSyncRequest, type NewsletterSubscriptionSyncResponse, type NewsletterSubscriptionsBulkAction, type NewsletterSubscriptionsBulkOutcome, type NewsletterSubscriptionsBulkUpdateRequest, type NewsletterSubscriptionsBulkUpdateResponse, type NewsletterSubscriptionsImportRequest, type NewsletterSubscriptionsImportResponse, type NewsletterSubscriptionsImportRowResult, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, type PerspectAb, PerspectApiClient, type PerspectApiConfig, PerspectApiV2Client, PerspectV2Error, type Product, type ProductBundleGroup, type ProductQueryParams, type ProductSku, type ProductSkuMediaItem, type ProductSkuOption, ProductsClient, type RequestOptions, type RequestOtpRequest, type ResponsiveImageSizes, type SetProfileValueRequest, type Site, type SiteUser, type SiteUserOrder, type SiteUserProfile, type SiteUserSubscription, SiteUsersClient, SitesClient, type SubscriptionCancellationMode, type UpdateApiKeyRequest, type UpdateContentRequest, type UpdateSiteUserRequest, type User, V1_DEPRECATION_NOTICE, V1_SUNSET_DATE, type V2ApiKey, type V2CancelSubscriptionResult, type V2Category, type V2CategoryCreateParams, type V2CategoryUpdateParams, type V2Collection, type V2CollectionCreateParams, type V2CollectionItem, type V2CollectionUpdateParams, type V2ContactSubmission, type V2Content, type V2ContentCreateParams, type V2ContentListParams, type V2ContentUpdateParams, type V2CreditBalance, type V2CreditTransaction, type V2Deleted, type V2Error, type V2ErrorType, type V2GrantCreditParams, type V2GrantCreditResult, type V2List, type V2Media, type V2NewsletterCampaign, type V2NewsletterImportRequest, type V2NewsletterImportResult, type V2NewsletterList, type V2NewsletterListCreateParams, type V2NewsletterListUpdateParams, type V2NewsletterSubscription, type V2NewsletterSubscriptionListMembershipUpdate, type V2NewsletterSyncInput, type V2NewsletterSyncResult, type V2NewsletterTrackingResponse, type V2Object, type V2Order, type V2OrderAddress, type V2OrderCreateParams, type V2OrderCreateResult, type V2OrderFulfillmentUpdate, type V2OrderLineItem, type V2OrderLineItemPriceData, type V2OrderListParams, type V2OrderTaxRequest, type V2Organization, type V2PaginationParams, type V2Product, type V2ProductCreateParams, type V2ProductListParams, type V2ProductUpdateParams, type V2Site, type V2SiteUser, type V2SiteUserListParams, type V2SiteUserMeUpdateParams, type V2SiteUserProfile, type V2SiteUserSubscription, type V2SiteUserUpdateParams, type V2SiteUserWithProfile, type V2SubscriptionCancelParams, type V2SubscriptionChangePlanParams, type V2SubscriptionPauseParams, type V2Webhook, type V2WebhookCreateParams, type V2WebhookUpdateParams, type VerifyOtpRequest, type VerifyOtpResponse, type Webhook, WebhooksClient, bucketFor, buildImageUrl, createApiError, createCheckoutSession, createPerspectAb, createPerspectApiClient, createPerspectApiV2Client, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadBlockBySlug, loadBlocks, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct, variantForBucket };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
ContentClient: () => ContentClient,
|
|
33
33
|
DEFAULT_IMAGE_SIZES: () => DEFAULT_IMAGE_SIZES,
|
|
34
34
|
HttpClient: () => HttpClient,
|
|
35
|
+
InMemoryCacheAdapter: () => InMemoryCacheAdapter,
|
|
35
36
|
NewsletterClient: () => NewsletterClient,
|
|
36
37
|
NewsletterManagementClient: () => NewsletterManagementClient,
|
|
37
38
|
NoopCacheAdapter: () => NoopCacheAdapter,
|
|
@@ -45,9 +46,11 @@ __export(index_exports, {
|
|
|
45
46
|
V1_DEPRECATION_NOTICE: () => V1_DEPRECATION_NOTICE,
|
|
46
47
|
V1_SUNSET_DATE: () => V1_SUNSET_DATE,
|
|
47
48
|
WebhooksClient: () => WebhooksClient,
|
|
49
|
+
bucketFor: () => bucketFor,
|
|
48
50
|
buildImageUrl: () => buildImageUrl,
|
|
49
51
|
createApiError: () => createApiError,
|
|
50
52
|
createCheckoutSession: () => createCheckoutSession,
|
|
53
|
+
createPerspectAb: () => createPerspectAb,
|
|
51
54
|
createPerspectApiClient: () => createPerspectApiClient,
|
|
52
55
|
createPerspectApiV2Client: () => createPerspectApiV2Client,
|
|
53
56
|
default: () => perspect_api_client_default,
|
|
@@ -56,6 +59,8 @@ __export(index_exports, {
|
|
|
56
59
|
generateSizesAttribute: () => generateSizesAttribute,
|
|
57
60
|
generateSrcSet: () => generateSrcSet,
|
|
58
61
|
loadAllContent: () => loadAllContent,
|
|
62
|
+
loadBlockBySlug: () => loadBlockBySlug,
|
|
63
|
+
loadBlocks: () => loadBlocks,
|
|
59
64
|
loadContentBySlug: () => loadContentBySlug,
|
|
60
65
|
loadPages: () => loadPages,
|
|
61
66
|
loadPosts: () => loadPosts,
|
|
@@ -63,7 +68,8 @@ __export(index_exports, {
|
|
|
63
68
|
loadProducts: () => loadProducts,
|
|
64
69
|
transformContent: () => transformContent,
|
|
65
70
|
transformMediaItem: () => transformMediaItem,
|
|
66
|
-
transformProduct: () => transformProduct
|
|
71
|
+
transformProduct: () => transformProduct,
|
|
72
|
+
variantForBucket: () => variantForBucket
|
|
67
73
|
});
|
|
68
74
|
module.exports = __toCommonJS(index_exports);
|
|
69
75
|
|
|
@@ -4137,6 +4143,180 @@ var CloudflareKVCacheAdapter = class {
|
|
|
4137
4143
|
}
|
|
4138
4144
|
};
|
|
4139
4145
|
|
|
4146
|
+
// src/cache/in-memory-adapter.ts
|
|
4147
|
+
var InMemoryCacheAdapter = class {
|
|
4148
|
+
store = /* @__PURE__ */ new Map();
|
|
4149
|
+
async get(key) {
|
|
4150
|
+
const entry = this.store.get(key);
|
|
4151
|
+
if (!entry) {
|
|
4152
|
+
return void 0;
|
|
4153
|
+
}
|
|
4154
|
+
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
|
|
4155
|
+
this.store.delete(key);
|
|
4156
|
+
return void 0;
|
|
4157
|
+
}
|
|
4158
|
+
return entry.value;
|
|
4159
|
+
}
|
|
4160
|
+
async set(key, value, options) {
|
|
4161
|
+
const expiresAt = options?.ttlSeconds && options.ttlSeconds > 0 ? Date.now() + options.ttlSeconds * 1e3 : void 0;
|
|
4162
|
+
this.store.set(key, { value, expiresAt });
|
|
4163
|
+
}
|
|
4164
|
+
async delete(key) {
|
|
4165
|
+
this.store.delete(key);
|
|
4166
|
+
}
|
|
4167
|
+
async deleteMany(keys) {
|
|
4168
|
+
keys.forEach((key) => this.store.delete(key));
|
|
4169
|
+
}
|
|
4170
|
+
async clear() {
|
|
4171
|
+
this.store.clear();
|
|
4172
|
+
}
|
|
4173
|
+
};
|
|
4174
|
+
|
|
4175
|
+
// src/ab/bucketing.ts
|
|
4176
|
+
var BUCKET_SPACE = 1e4;
|
|
4177
|
+
function bucketFor(visitorId, flagKey, version) {
|
|
4178
|
+
const input = `${visitorId}|${flagKey}|${version}`;
|
|
4179
|
+
const bytes = new TextEncoder().encode(input);
|
|
4180
|
+
let hash = 2166136261;
|
|
4181
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
4182
|
+
hash ^= bytes[i];
|
|
4183
|
+
hash = Math.imul(hash, 16777619);
|
|
4184
|
+
}
|
|
4185
|
+
return (hash >>> 0) % BUCKET_SPACE;
|
|
4186
|
+
}
|
|
4187
|
+
function variantForBucket(bucket, variants, trafficAllocationBp) {
|
|
4188
|
+
if (bucket >= trafficAllocationBp) return null;
|
|
4189
|
+
const sorted = [...variants].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
4190
|
+
const totalWeight = sorted.reduce((acc, v) => acc + v.weightBp, 0);
|
|
4191
|
+
if (totalWeight <= 0) return null;
|
|
4192
|
+
const scaled = Math.floor(bucket * totalWeight / trafficAllocationBp);
|
|
4193
|
+
let cursor = 0;
|
|
4194
|
+
for (const v of sorted) {
|
|
4195
|
+
cursor += v.weightBp;
|
|
4196
|
+
if (scaled < cursor) return v.key;
|
|
4197
|
+
}
|
|
4198
|
+
return sorted[sorted.length - 1].key;
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// src/ab/ab-client.ts
|
|
4202
|
+
var CONFIG_TTL_SECONDS = 3600;
|
|
4203
|
+
var VID_COOKIE = "perspect_vid";
|
|
4204
|
+
var VID_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2;
|
|
4205
|
+
var BASE_URL_DEFAULT = "https://api.perspect.com";
|
|
4206
|
+
function createPerspectAb(options) {
|
|
4207
|
+
const { apiKey, siteName, ctx } = options;
|
|
4208
|
+
const baseUrl = (options.baseUrl ?? BASE_URL_DEFAULT).replace(/\/$/, "");
|
|
4209
|
+
return {
|
|
4210
|
+
async forRequest(request) {
|
|
4211
|
+
const { visitorId, mintedNew } = readOrMintVid(request);
|
|
4212
|
+
const responseHeaders = new Headers();
|
|
4213
|
+
if (mintedNew) {
|
|
4214
|
+
responseHeaders.append(
|
|
4215
|
+
"Set-Cookie",
|
|
4216
|
+
`${VID_COOKIE}=${visitorId}; Path=/; Max-Age=${VID_COOKIE_MAX_AGE}; SameSite=Lax; Secure; HttpOnly`
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
const config = await getConfig(options.cache, apiKey, siteName, baseUrl);
|
|
4220
|
+
return { ab: buildAbClient(apiKey, siteName, baseUrl, ctx, config, visitorId), responseHeaders };
|
|
4221
|
+
},
|
|
4222
|
+
async refreshConfig() {
|
|
4223
|
+
await fetchAndStoreConfig(options.cache, apiKey, siteName, baseUrl);
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
4227
|
+
function buildAbClient(apiKey, siteName, baseUrl, ctx, config, visitorId) {
|
|
4228
|
+
return {
|
|
4229
|
+
async getVariant(flagKey) {
|
|
4230
|
+
const flag = config.flags[flagKey];
|
|
4231
|
+
if (!flag || flag.status !== "running") {
|
|
4232
|
+
return { variant: flag?.defaultVariant ?? null, enrolled: false, config: null };
|
|
4233
|
+
}
|
|
4234
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
4235
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
4236
|
+
if (assigned === null) {
|
|
4237
|
+
return { variant: flag.defaultVariant, enrolled: false, config: null };
|
|
4238
|
+
}
|
|
4239
|
+
const variantCfg = flag.variants.find((v) => v.key === assigned)?.config ?? null;
|
|
4240
|
+
emit(apiKey, baseUrl, ctx, {
|
|
4241
|
+
site: siteName,
|
|
4242
|
+
kind: "exposure",
|
|
4243
|
+
flag: flagKey,
|
|
4244
|
+
version: flag.currentVersion,
|
|
4245
|
+
variant: assigned,
|
|
4246
|
+
visitorId,
|
|
4247
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, "exposure"),
|
|
4248
|
+
ts: Math.floor(Date.now() / 1e3)
|
|
4249
|
+
});
|
|
4250
|
+
return { variant: assigned, enrolled: true, config: variantCfg };
|
|
4251
|
+
},
|
|
4252
|
+
async track(event, properties) {
|
|
4253
|
+
for (const [flagKey, flag] of Object.entries(config.flags)) {
|
|
4254
|
+
if (flag.status !== "running") continue;
|
|
4255
|
+
const goals = flag.goalsByVersion[String(flag.currentVersion)] ?? [];
|
|
4256
|
+
if (!goals.includes(event)) continue;
|
|
4257
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
4258
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
4259
|
+
if (assigned === null) continue;
|
|
4260
|
+
emit(apiKey, baseUrl, ctx, {
|
|
4261
|
+
site: siteName,
|
|
4262
|
+
kind: "conversion",
|
|
4263
|
+
flag: flagKey,
|
|
4264
|
+
version: flag.currentVersion,
|
|
4265
|
+
variant: assigned,
|
|
4266
|
+
visitorId,
|
|
4267
|
+
event,
|
|
4268
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, event),
|
|
4269
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
4270
|
+
properties
|
|
4271
|
+
});
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
function configCacheKey(siteName) {
|
|
4277
|
+
return `ab:config:${siteName}`;
|
|
4278
|
+
}
|
|
4279
|
+
async function getConfig(cache, apiKey, siteName, baseUrl) {
|
|
4280
|
+
if (cache) {
|
|
4281
|
+
const raw = await cache.get(configCacheKey(siteName));
|
|
4282
|
+
if (raw) return JSON.parse(raw);
|
|
4283
|
+
}
|
|
4284
|
+
return fetchAndStoreConfig(cache, apiKey, siteName, baseUrl);
|
|
4285
|
+
}
|
|
4286
|
+
async function fetchAndStoreConfig(cache, apiKey, siteName, baseUrl) {
|
|
4287
|
+
const res = await fetch(`${baseUrl}/_ab/config?site=${encodeURIComponent(siteName)}`, {
|
|
4288
|
+
headers: { "x-api-key": apiKey }
|
|
4289
|
+
});
|
|
4290
|
+
if (!res.ok) return { schemaVersion: 1, flags: {} };
|
|
4291
|
+
const config = await res.json();
|
|
4292
|
+
if (cache) {
|
|
4293
|
+
await cache.set(configCacheKey(siteName), JSON.stringify(config), { ttlSeconds: CONFIG_TTL_SECONDS });
|
|
4294
|
+
}
|
|
4295
|
+
return config;
|
|
4296
|
+
}
|
|
4297
|
+
function emit(apiKey, baseUrl, ctx, body) {
|
|
4298
|
+
const task = fetch(`${baseUrl}/_ab/event`, {
|
|
4299
|
+
method: "POST",
|
|
4300
|
+
headers: { "content-type": "application/json", "x-api-key": apiKey },
|
|
4301
|
+
body: JSON.stringify(body)
|
|
4302
|
+
}).then(() => void 0).catch(() => void 0);
|
|
4303
|
+
if (ctx) ctx.waitUntil(task);
|
|
4304
|
+
}
|
|
4305
|
+
function readOrMintVid(request) {
|
|
4306
|
+
const cookie = request.headers.get("cookie") ?? "";
|
|
4307
|
+
const match = cookie.match(/(?:^|; )perspect_vid=([^;]+)/);
|
|
4308
|
+
if (match) return { visitorId: decodeURIComponent(match[1]), mintedNew: false };
|
|
4309
|
+
return { visitorId: crypto.randomUUID().replace(/-/g, ""), mintedNew: true };
|
|
4310
|
+
}
|
|
4311
|
+
async function dedupFor(visitorId, flagKey, version, variant, event) {
|
|
4312
|
+
const input = `${visitorId}|${flagKey}|${version}|${variant}|${event}`;
|
|
4313
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
4314
|
+
const bytes = new Uint8Array(digest);
|
|
4315
|
+
let hex = "";
|
|
4316
|
+
for (let i = 0; i < 16; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
4317
|
+
return hex;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4140
4320
|
// src/utils/image-transform.ts
|
|
4141
4321
|
var DEFAULT_IMAGE_SIZES = {
|
|
4142
4322
|
thumbnail: {
|
|
@@ -4639,6 +4819,56 @@ async function loadContentBySlug(options) {
|
|
|
4639
4819
|
return null;
|
|
4640
4820
|
}
|
|
4641
4821
|
}
|
|
4822
|
+
async function loadBlocks(options) {
|
|
4823
|
+
const { client, siteName, logger = noopLogger, params } = options;
|
|
4824
|
+
if (!client) {
|
|
4825
|
+
log(logger, "warn", "[PerspectAPI] No client configured, cannot load blocks");
|
|
4826
|
+
return [];
|
|
4827
|
+
}
|
|
4828
|
+
try {
|
|
4829
|
+
log(logger, "info", `[PerspectAPI] Loading blocks for site "${siteName}"`);
|
|
4830
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
4831
|
+
const resolvedLimit = validateLimit(
|
|
4832
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
4833
|
+
"blocks query"
|
|
4834
|
+
);
|
|
4835
|
+
const response = await client.content.getContent(
|
|
4836
|
+
siteName,
|
|
4837
|
+
{
|
|
4838
|
+
...params,
|
|
4839
|
+
page_status: params?.page_status ?? "publish",
|
|
4840
|
+
page_type: "block",
|
|
4841
|
+
limit: resolvedLimit
|
|
4842
|
+
}
|
|
4843
|
+
);
|
|
4844
|
+
if (!response.data) {
|
|
4845
|
+
return [];
|
|
4846
|
+
}
|
|
4847
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
4848
|
+
} catch (error) {
|
|
4849
|
+
log(logger, "error", "[PerspectAPI] Error loading blocks", error);
|
|
4850
|
+
return [];
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
async function loadBlockBySlug(options) {
|
|
4854
|
+
const { client, siteName, slug, logger = noopLogger } = options;
|
|
4855
|
+
if (!client) {
|
|
4856
|
+
log(logger, "warn", "[PerspectAPI] No client configured, cannot load block");
|
|
4857
|
+
return null;
|
|
4858
|
+
}
|
|
4859
|
+
try {
|
|
4860
|
+
log(logger, "info", `[PerspectAPI] Loading block slug "${slug}" for site "${siteName}"`);
|
|
4861
|
+
const response = await client.content.getContentBySlug(siteName, slug);
|
|
4862
|
+
if (!response.data) {
|
|
4863
|
+
log(logger, "warn", `[PerspectAPI] Block not found for slug "${slug}"`);
|
|
4864
|
+
return null;
|
|
4865
|
+
}
|
|
4866
|
+
return transformContent(response.data, logger);
|
|
4867
|
+
} catch (error) {
|
|
4868
|
+
log(logger, "error", `[PerspectAPI] Error loading block slug "${slug}"`, error);
|
|
4869
|
+
return null;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4642
4872
|
async function loadAllContent(options) {
|
|
4643
4873
|
const { logger = noopLogger } = options;
|
|
4644
4874
|
const [pages, posts] = await Promise.all([
|
|
@@ -4758,6 +4988,7 @@ async function createCheckoutSession(options) {
|
|
|
4758
4988
|
ContentClient,
|
|
4759
4989
|
DEFAULT_IMAGE_SIZES,
|
|
4760
4990
|
HttpClient,
|
|
4991
|
+
InMemoryCacheAdapter,
|
|
4761
4992
|
NewsletterClient,
|
|
4762
4993
|
NewsletterManagementClient,
|
|
4763
4994
|
NoopCacheAdapter,
|
|
@@ -4771,9 +5002,11 @@ async function createCheckoutSession(options) {
|
|
|
4771
5002
|
V1_DEPRECATION_NOTICE,
|
|
4772
5003
|
V1_SUNSET_DATE,
|
|
4773
5004
|
WebhooksClient,
|
|
5005
|
+
bucketFor,
|
|
4774
5006
|
buildImageUrl,
|
|
4775
5007
|
createApiError,
|
|
4776
5008
|
createCheckoutSession,
|
|
5009
|
+
createPerspectAb,
|
|
4777
5010
|
createPerspectApiClient,
|
|
4778
5011
|
createPerspectApiV2Client,
|
|
4779
5012
|
generateResponsiveImageHtml,
|
|
@@ -4781,6 +5014,8 @@ async function createCheckoutSession(options) {
|
|
|
4781
5014
|
generateSizesAttribute,
|
|
4782
5015
|
generateSrcSet,
|
|
4783
5016
|
loadAllContent,
|
|
5017
|
+
loadBlockBySlug,
|
|
5018
|
+
loadBlocks,
|
|
4784
5019
|
loadContentBySlug,
|
|
4785
5020
|
loadPages,
|
|
4786
5021
|
loadPosts,
|
|
@@ -4788,5 +5023,6 @@ async function createCheckoutSession(options) {
|
|
|
4788
5023
|
loadProducts,
|
|
4789
5024
|
transformContent,
|
|
4790
5025
|
transformMediaItem,
|
|
4791
|
-
transformProduct
|
|
5026
|
+
transformProduct,
|
|
5027
|
+
variantForBucket
|
|
4792
5028
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -4068,6 +4068,180 @@ var CloudflareKVCacheAdapter = class {
|
|
|
4068
4068
|
}
|
|
4069
4069
|
};
|
|
4070
4070
|
|
|
4071
|
+
// src/cache/in-memory-adapter.ts
|
|
4072
|
+
var InMemoryCacheAdapter = class {
|
|
4073
|
+
store = /* @__PURE__ */ new Map();
|
|
4074
|
+
async get(key) {
|
|
4075
|
+
const entry = this.store.get(key);
|
|
4076
|
+
if (!entry) {
|
|
4077
|
+
return void 0;
|
|
4078
|
+
}
|
|
4079
|
+
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
|
|
4080
|
+
this.store.delete(key);
|
|
4081
|
+
return void 0;
|
|
4082
|
+
}
|
|
4083
|
+
return entry.value;
|
|
4084
|
+
}
|
|
4085
|
+
async set(key, value, options) {
|
|
4086
|
+
const expiresAt = options?.ttlSeconds && options.ttlSeconds > 0 ? Date.now() + options.ttlSeconds * 1e3 : void 0;
|
|
4087
|
+
this.store.set(key, { value, expiresAt });
|
|
4088
|
+
}
|
|
4089
|
+
async delete(key) {
|
|
4090
|
+
this.store.delete(key);
|
|
4091
|
+
}
|
|
4092
|
+
async deleteMany(keys) {
|
|
4093
|
+
keys.forEach((key) => this.store.delete(key));
|
|
4094
|
+
}
|
|
4095
|
+
async clear() {
|
|
4096
|
+
this.store.clear();
|
|
4097
|
+
}
|
|
4098
|
+
};
|
|
4099
|
+
|
|
4100
|
+
// src/ab/bucketing.ts
|
|
4101
|
+
var BUCKET_SPACE = 1e4;
|
|
4102
|
+
function bucketFor(visitorId, flagKey, version) {
|
|
4103
|
+
const input = `${visitorId}|${flagKey}|${version}`;
|
|
4104
|
+
const bytes = new TextEncoder().encode(input);
|
|
4105
|
+
let hash = 2166136261;
|
|
4106
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
4107
|
+
hash ^= bytes[i];
|
|
4108
|
+
hash = Math.imul(hash, 16777619);
|
|
4109
|
+
}
|
|
4110
|
+
return (hash >>> 0) % BUCKET_SPACE;
|
|
4111
|
+
}
|
|
4112
|
+
function variantForBucket(bucket, variants, trafficAllocationBp) {
|
|
4113
|
+
if (bucket >= trafficAllocationBp) return null;
|
|
4114
|
+
const sorted = [...variants].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
4115
|
+
const totalWeight = sorted.reduce((acc, v) => acc + v.weightBp, 0);
|
|
4116
|
+
if (totalWeight <= 0) return null;
|
|
4117
|
+
const scaled = Math.floor(bucket * totalWeight / trafficAllocationBp);
|
|
4118
|
+
let cursor = 0;
|
|
4119
|
+
for (const v of sorted) {
|
|
4120
|
+
cursor += v.weightBp;
|
|
4121
|
+
if (scaled < cursor) return v.key;
|
|
4122
|
+
}
|
|
4123
|
+
return sorted[sorted.length - 1].key;
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// src/ab/ab-client.ts
|
|
4127
|
+
var CONFIG_TTL_SECONDS = 3600;
|
|
4128
|
+
var VID_COOKIE = "perspect_vid";
|
|
4129
|
+
var VID_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2;
|
|
4130
|
+
var BASE_URL_DEFAULT = "https://api.perspect.com";
|
|
4131
|
+
function createPerspectAb(options) {
|
|
4132
|
+
const { apiKey, siteName, ctx } = options;
|
|
4133
|
+
const baseUrl = (options.baseUrl ?? BASE_URL_DEFAULT).replace(/\/$/, "");
|
|
4134
|
+
return {
|
|
4135
|
+
async forRequest(request) {
|
|
4136
|
+
const { visitorId, mintedNew } = readOrMintVid(request);
|
|
4137
|
+
const responseHeaders = new Headers();
|
|
4138
|
+
if (mintedNew) {
|
|
4139
|
+
responseHeaders.append(
|
|
4140
|
+
"Set-Cookie",
|
|
4141
|
+
`${VID_COOKIE}=${visitorId}; Path=/; Max-Age=${VID_COOKIE_MAX_AGE}; SameSite=Lax; Secure; HttpOnly`
|
|
4142
|
+
);
|
|
4143
|
+
}
|
|
4144
|
+
const config = await getConfig(options.cache, apiKey, siteName, baseUrl);
|
|
4145
|
+
return { ab: buildAbClient(apiKey, siteName, baseUrl, ctx, config, visitorId), responseHeaders };
|
|
4146
|
+
},
|
|
4147
|
+
async refreshConfig() {
|
|
4148
|
+
await fetchAndStoreConfig(options.cache, apiKey, siteName, baseUrl);
|
|
4149
|
+
}
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
function buildAbClient(apiKey, siteName, baseUrl, ctx, config, visitorId) {
|
|
4153
|
+
return {
|
|
4154
|
+
async getVariant(flagKey) {
|
|
4155
|
+
const flag = config.flags[flagKey];
|
|
4156
|
+
if (!flag || flag.status !== "running") {
|
|
4157
|
+
return { variant: flag?.defaultVariant ?? null, enrolled: false, config: null };
|
|
4158
|
+
}
|
|
4159
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
4160
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
4161
|
+
if (assigned === null) {
|
|
4162
|
+
return { variant: flag.defaultVariant, enrolled: false, config: null };
|
|
4163
|
+
}
|
|
4164
|
+
const variantCfg = flag.variants.find((v) => v.key === assigned)?.config ?? null;
|
|
4165
|
+
emit(apiKey, baseUrl, ctx, {
|
|
4166
|
+
site: siteName,
|
|
4167
|
+
kind: "exposure",
|
|
4168
|
+
flag: flagKey,
|
|
4169
|
+
version: flag.currentVersion,
|
|
4170
|
+
variant: assigned,
|
|
4171
|
+
visitorId,
|
|
4172
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, "exposure"),
|
|
4173
|
+
ts: Math.floor(Date.now() / 1e3)
|
|
4174
|
+
});
|
|
4175
|
+
return { variant: assigned, enrolled: true, config: variantCfg };
|
|
4176
|
+
},
|
|
4177
|
+
async track(event, properties) {
|
|
4178
|
+
for (const [flagKey, flag] of Object.entries(config.flags)) {
|
|
4179
|
+
if (flag.status !== "running") continue;
|
|
4180
|
+
const goals = flag.goalsByVersion[String(flag.currentVersion)] ?? [];
|
|
4181
|
+
if (!goals.includes(event)) continue;
|
|
4182
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
4183
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
4184
|
+
if (assigned === null) continue;
|
|
4185
|
+
emit(apiKey, baseUrl, ctx, {
|
|
4186
|
+
site: siteName,
|
|
4187
|
+
kind: "conversion",
|
|
4188
|
+
flag: flagKey,
|
|
4189
|
+
version: flag.currentVersion,
|
|
4190
|
+
variant: assigned,
|
|
4191
|
+
visitorId,
|
|
4192
|
+
event,
|
|
4193
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, event),
|
|
4194
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
4195
|
+
properties
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4201
|
+
function configCacheKey(siteName) {
|
|
4202
|
+
return `ab:config:${siteName}`;
|
|
4203
|
+
}
|
|
4204
|
+
async function getConfig(cache, apiKey, siteName, baseUrl) {
|
|
4205
|
+
if (cache) {
|
|
4206
|
+
const raw = await cache.get(configCacheKey(siteName));
|
|
4207
|
+
if (raw) return JSON.parse(raw);
|
|
4208
|
+
}
|
|
4209
|
+
return fetchAndStoreConfig(cache, apiKey, siteName, baseUrl);
|
|
4210
|
+
}
|
|
4211
|
+
async function fetchAndStoreConfig(cache, apiKey, siteName, baseUrl) {
|
|
4212
|
+
const res = await fetch(`${baseUrl}/_ab/config?site=${encodeURIComponent(siteName)}`, {
|
|
4213
|
+
headers: { "x-api-key": apiKey }
|
|
4214
|
+
});
|
|
4215
|
+
if (!res.ok) return { schemaVersion: 1, flags: {} };
|
|
4216
|
+
const config = await res.json();
|
|
4217
|
+
if (cache) {
|
|
4218
|
+
await cache.set(configCacheKey(siteName), JSON.stringify(config), { ttlSeconds: CONFIG_TTL_SECONDS });
|
|
4219
|
+
}
|
|
4220
|
+
return config;
|
|
4221
|
+
}
|
|
4222
|
+
function emit(apiKey, baseUrl, ctx, body) {
|
|
4223
|
+
const task = fetch(`${baseUrl}/_ab/event`, {
|
|
4224
|
+
method: "POST",
|
|
4225
|
+
headers: { "content-type": "application/json", "x-api-key": apiKey },
|
|
4226
|
+
body: JSON.stringify(body)
|
|
4227
|
+
}).then(() => void 0).catch(() => void 0);
|
|
4228
|
+
if (ctx) ctx.waitUntil(task);
|
|
4229
|
+
}
|
|
4230
|
+
function readOrMintVid(request) {
|
|
4231
|
+
const cookie = request.headers.get("cookie") ?? "";
|
|
4232
|
+
const match = cookie.match(/(?:^|; )perspect_vid=([^;]+)/);
|
|
4233
|
+
if (match) return { visitorId: decodeURIComponent(match[1]), mintedNew: false };
|
|
4234
|
+
return { visitorId: crypto.randomUUID().replace(/-/g, ""), mintedNew: true };
|
|
4235
|
+
}
|
|
4236
|
+
async function dedupFor(visitorId, flagKey, version, variant, event) {
|
|
4237
|
+
const input = `${visitorId}|${flagKey}|${version}|${variant}|${event}`;
|
|
4238
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
4239
|
+
const bytes = new Uint8Array(digest);
|
|
4240
|
+
let hex = "";
|
|
4241
|
+
for (let i = 0; i < 16; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
4242
|
+
return hex;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4071
4245
|
// src/utils/image-transform.ts
|
|
4072
4246
|
var DEFAULT_IMAGE_SIZES = {
|
|
4073
4247
|
thumbnail: {
|
|
@@ -4570,6 +4744,56 @@ async function loadContentBySlug(options) {
|
|
|
4570
4744
|
return null;
|
|
4571
4745
|
}
|
|
4572
4746
|
}
|
|
4747
|
+
async function loadBlocks(options) {
|
|
4748
|
+
const { client, siteName, logger = noopLogger, params } = options;
|
|
4749
|
+
if (!client) {
|
|
4750
|
+
log(logger, "warn", "[PerspectAPI] No client configured, cannot load blocks");
|
|
4751
|
+
return [];
|
|
4752
|
+
}
|
|
4753
|
+
try {
|
|
4754
|
+
log(logger, "info", `[PerspectAPI] Loading blocks for site "${siteName}"`);
|
|
4755
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
4756
|
+
const resolvedLimit = validateLimit(
|
|
4757
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
4758
|
+
"blocks query"
|
|
4759
|
+
);
|
|
4760
|
+
const response = await client.content.getContent(
|
|
4761
|
+
siteName,
|
|
4762
|
+
{
|
|
4763
|
+
...params,
|
|
4764
|
+
page_status: params?.page_status ?? "publish",
|
|
4765
|
+
page_type: "block",
|
|
4766
|
+
limit: resolvedLimit
|
|
4767
|
+
}
|
|
4768
|
+
);
|
|
4769
|
+
if (!response.data) {
|
|
4770
|
+
return [];
|
|
4771
|
+
}
|
|
4772
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
log(logger, "error", "[PerspectAPI] Error loading blocks", error);
|
|
4775
|
+
return [];
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
async function loadBlockBySlug(options) {
|
|
4779
|
+
const { client, siteName, slug, logger = noopLogger } = options;
|
|
4780
|
+
if (!client) {
|
|
4781
|
+
log(logger, "warn", "[PerspectAPI] No client configured, cannot load block");
|
|
4782
|
+
return null;
|
|
4783
|
+
}
|
|
4784
|
+
try {
|
|
4785
|
+
log(logger, "info", `[PerspectAPI] Loading block slug "${slug}" for site "${siteName}"`);
|
|
4786
|
+
const response = await client.content.getContentBySlug(siteName, slug);
|
|
4787
|
+
if (!response.data) {
|
|
4788
|
+
log(logger, "warn", `[PerspectAPI] Block not found for slug "${slug}"`);
|
|
4789
|
+
return null;
|
|
4790
|
+
}
|
|
4791
|
+
return transformContent(response.data, logger);
|
|
4792
|
+
} catch (error) {
|
|
4793
|
+
log(logger, "error", `[PerspectAPI] Error loading block slug "${slug}"`, error);
|
|
4794
|
+
return null;
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4573
4797
|
async function loadAllContent(options) {
|
|
4574
4798
|
const { logger = noopLogger } = options;
|
|
4575
4799
|
const [pages, posts] = await Promise.all([
|
|
@@ -4688,6 +4912,7 @@ export {
|
|
|
4688
4912
|
ContentClient,
|
|
4689
4913
|
DEFAULT_IMAGE_SIZES,
|
|
4690
4914
|
HttpClient,
|
|
4915
|
+
InMemoryCacheAdapter,
|
|
4691
4916
|
NewsletterClient,
|
|
4692
4917
|
NewsletterManagementClient,
|
|
4693
4918
|
NoopCacheAdapter,
|
|
@@ -4701,9 +4926,11 @@ export {
|
|
|
4701
4926
|
V1_DEPRECATION_NOTICE,
|
|
4702
4927
|
V1_SUNSET_DATE,
|
|
4703
4928
|
WebhooksClient,
|
|
4929
|
+
bucketFor,
|
|
4704
4930
|
buildImageUrl,
|
|
4705
4931
|
createApiError,
|
|
4706
4932
|
createCheckoutSession,
|
|
4933
|
+
createPerspectAb,
|
|
4707
4934
|
createPerspectApiClient,
|
|
4708
4935
|
createPerspectApiV2Client,
|
|
4709
4936
|
perspect_api_client_default as default,
|
|
@@ -4712,6 +4939,8 @@ export {
|
|
|
4712
4939
|
generateSizesAttribute,
|
|
4713
4940
|
generateSrcSet,
|
|
4714
4941
|
loadAllContent,
|
|
4942
|
+
loadBlockBySlug,
|
|
4943
|
+
loadBlocks,
|
|
4715
4944
|
loadContentBySlug,
|
|
4716
4945
|
loadPages,
|
|
4717
4946
|
loadPosts,
|
|
@@ -4719,5 +4948,6 @@ export {
|
|
|
4719
4948
|
loadProducts,
|
|
4720
4949
|
transformContent,
|
|
4721
4950
|
transformMediaItem,
|
|
4722
|
-
transformProduct
|
|
4951
|
+
transformProduct,
|
|
4952
|
+
variantForBucket
|
|
4723
4953
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { bucketFor, variantForBucket } from './bucketing';
|
|
2
|
+
import type { CacheAdapter } from '../cache/types';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Public types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export interface CreatePerspectAbOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
siteName: string;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Pluggable cache adapter — CloudflareKVCacheAdapter, InMemoryCacheAdapter, or NoopCacheAdapter. */
|
|
13
|
+
cache?: CacheAdapter;
|
|
14
|
+
/** ExecutionContext (CF Workers) or any object with waitUntil — enables background tasks. */
|
|
15
|
+
ctx?: { waitUntil(p: Promise<unknown>): void };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AbForRequestResult {
|
|
19
|
+
ab: AbClient;
|
|
20
|
+
/** Merge into the outgoing response — may carry a freshly minted perspect_vid cookie. */
|
|
21
|
+
responseHeaders: Headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AbVariantResult {
|
|
25
|
+
variant: string | null;
|
|
26
|
+
enrolled: boolean;
|
|
27
|
+
config: Record<string, unknown> | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AbClient {
|
|
31
|
+
getVariant(flagKey: string): Promise<AbVariantResult>;
|
|
32
|
+
track(event: string, properties?: Record<string, unknown>): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PerspectAb {
|
|
36
|
+
forRequest(request: Request): Promise<AbForRequestResult>;
|
|
37
|
+
refreshConfig(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Internal config types
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
interface AbVariantCfg {
|
|
45
|
+
key: string;
|
|
46
|
+
weightBp: number;
|
|
47
|
+
config: Record<string, unknown> | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface AbFlagCfg {
|
|
51
|
+
currentVersion: number;
|
|
52
|
+
status: 'draft' | 'running' | 'paused' | 'stopped';
|
|
53
|
+
variants: AbVariantCfg[];
|
|
54
|
+
trafficAllocationBp: number;
|
|
55
|
+
defaultVariant: string;
|
|
56
|
+
goalsByVersion: Record<string, string[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface AbConfigBlob {
|
|
60
|
+
schemaVersion: 1;
|
|
61
|
+
flags: Record<string, AbFlagCfg>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Constants
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const CONFIG_TTL_SECONDS = 3_600;
|
|
69
|
+
const VID_COOKIE = 'perspect_vid';
|
|
70
|
+
const VID_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2;
|
|
71
|
+
const BASE_URL_DEFAULT = 'https://api.perspect.com';
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Public entry point
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export function createPerspectAb(options: CreatePerspectAbOptions): PerspectAb {
|
|
78
|
+
const { apiKey, siteName, ctx } = options;
|
|
79
|
+
const baseUrl = (options.baseUrl ?? BASE_URL_DEFAULT).replace(/\/$/, '');
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
async forRequest(request: Request): Promise<AbForRequestResult> {
|
|
83
|
+
const { visitorId, mintedNew } = readOrMintVid(request);
|
|
84
|
+
const responseHeaders = new Headers();
|
|
85
|
+
if (mintedNew) {
|
|
86
|
+
responseHeaders.append(
|
|
87
|
+
'Set-Cookie',
|
|
88
|
+
`${VID_COOKIE}=${visitorId}; Path=/; Max-Age=${VID_COOKIE_MAX_AGE}; SameSite=Lax; Secure; HttpOnly`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const config = await getConfig(options.cache, apiKey, siteName, baseUrl);
|
|
92
|
+
return { ab: buildAbClient(apiKey, siteName, baseUrl, ctx, config, visitorId), responseHeaders };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async refreshConfig(): Promise<void> {
|
|
96
|
+
await fetchAndStoreConfig(options.cache, apiKey, siteName, baseUrl);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// AB client
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function buildAbClient(
|
|
106
|
+
apiKey: string,
|
|
107
|
+
siteName: string,
|
|
108
|
+
baseUrl: string,
|
|
109
|
+
ctx: { waitUntil(p: Promise<unknown>): void } | undefined,
|
|
110
|
+
config: AbConfigBlob,
|
|
111
|
+
visitorId: string,
|
|
112
|
+
): AbClient {
|
|
113
|
+
return {
|
|
114
|
+
async getVariant(flagKey: string): Promise<AbVariantResult> {
|
|
115
|
+
const flag = config.flags[flagKey];
|
|
116
|
+
if (!flag || flag.status !== 'running') {
|
|
117
|
+
return { variant: flag?.defaultVariant ?? null, enrolled: false, config: null };
|
|
118
|
+
}
|
|
119
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
120
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
121
|
+
if (assigned === null) {
|
|
122
|
+
return { variant: flag.defaultVariant, enrolled: false, config: null };
|
|
123
|
+
}
|
|
124
|
+
const variantCfg = flag.variants.find((v) => v.key === assigned)?.config ?? null;
|
|
125
|
+
emit(apiKey, baseUrl, ctx, {
|
|
126
|
+
site: siteName,
|
|
127
|
+
kind: 'exposure',
|
|
128
|
+
flag: flagKey,
|
|
129
|
+
version: flag.currentVersion,
|
|
130
|
+
variant: assigned,
|
|
131
|
+
visitorId,
|
|
132
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, 'exposure'),
|
|
133
|
+
ts: Math.floor(Date.now() / 1000),
|
|
134
|
+
});
|
|
135
|
+
return { variant: assigned, enrolled: true, config: variantCfg };
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async track(event: string, properties?: Record<string, unknown>): Promise<void> {
|
|
139
|
+
for (const [flagKey, flag] of Object.entries(config.flags)) {
|
|
140
|
+
if (flag.status !== 'running') continue;
|
|
141
|
+
const goals = flag.goalsByVersion[String(flag.currentVersion)] ?? [];
|
|
142
|
+
if (!goals.includes(event)) continue;
|
|
143
|
+
const bucket = bucketFor(visitorId, flagKey, flag.currentVersion);
|
|
144
|
+
const assigned = variantForBucket(bucket, flag.variants, flag.trafficAllocationBp);
|
|
145
|
+
if (assigned === null) continue;
|
|
146
|
+
emit(apiKey, baseUrl, ctx, {
|
|
147
|
+
site: siteName,
|
|
148
|
+
kind: 'conversion',
|
|
149
|
+
flag: flagKey,
|
|
150
|
+
version: flag.currentVersion,
|
|
151
|
+
variant: assigned,
|
|
152
|
+
visitorId,
|
|
153
|
+
event,
|
|
154
|
+
dedup: await dedupFor(visitorId, flagKey, flag.currentVersion, assigned, event),
|
|
155
|
+
ts: Math.floor(Date.now() / 1000),
|
|
156
|
+
properties,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Config cache (SWR via CacheAdapter)
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function configCacheKey(siteName: string): string {
|
|
168
|
+
return `ab:config:${siteName}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function getConfig(
|
|
172
|
+
cache: CacheAdapter | undefined,
|
|
173
|
+
apiKey: string,
|
|
174
|
+
siteName: string,
|
|
175
|
+
baseUrl: string,
|
|
176
|
+
): Promise<AbConfigBlob> {
|
|
177
|
+
if (cache) {
|
|
178
|
+
const raw = await cache.get(configCacheKey(siteName));
|
|
179
|
+
if (raw) return JSON.parse(raw) as AbConfigBlob;
|
|
180
|
+
}
|
|
181
|
+
return fetchAndStoreConfig(cache, apiKey, siteName, baseUrl);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function fetchAndStoreConfig(
|
|
185
|
+
cache: CacheAdapter | undefined,
|
|
186
|
+
apiKey: string,
|
|
187
|
+
siteName: string,
|
|
188
|
+
baseUrl: string,
|
|
189
|
+
): Promise<AbConfigBlob> {
|
|
190
|
+
const res = await fetch(`${baseUrl}/_ab/config?site=${encodeURIComponent(siteName)}`, {
|
|
191
|
+
headers: { 'x-api-key': apiKey },
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) return { schemaVersion: 1, flags: {} };
|
|
194
|
+
const config = (await res.json()) as AbConfigBlob;
|
|
195
|
+
if (cache) {
|
|
196
|
+
await cache.set(configCacheKey(siteName), JSON.stringify(config), { ttlSeconds: CONFIG_TTL_SECONDS });
|
|
197
|
+
}
|
|
198
|
+
return config;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Event emission
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
interface EventBody {
|
|
206
|
+
site: string;
|
|
207
|
+
kind: 'exposure' | 'conversion' | 'handoff';
|
|
208
|
+
flag: string;
|
|
209
|
+
version: number;
|
|
210
|
+
variant: string;
|
|
211
|
+
visitorId: string;
|
|
212
|
+
dedup: string;
|
|
213
|
+
ts: number;
|
|
214
|
+
event?: string;
|
|
215
|
+
properties?: Record<string, unknown>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function emit(
|
|
219
|
+
apiKey: string,
|
|
220
|
+
baseUrl: string,
|
|
221
|
+
ctx: { waitUntil(p: Promise<unknown>): void } | undefined,
|
|
222
|
+
body: EventBody,
|
|
223
|
+
): void {
|
|
224
|
+
const task = fetch(`${baseUrl}/_ab/event`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
|
|
227
|
+
body: JSON.stringify(body),
|
|
228
|
+
})
|
|
229
|
+
.then(() => undefined)
|
|
230
|
+
.catch(() => undefined);
|
|
231
|
+
if (ctx) ctx.waitUntil(task);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Visitor ID
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function readOrMintVid(request: Request): { visitorId: string; mintedNew: boolean } {
|
|
239
|
+
const cookie = request.headers.get('cookie') ?? '';
|
|
240
|
+
const match = cookie.match(/(?:^|; )perspect_vid=([^;]+)/);
|
|
241
|
+
if (match) return { visitorId: decodeURIComponent(match[1]), mintedNew: false };
|
|
242
|
+
return { visitorId: crypto.randomUUID().replace(/-/g, ''), mintedNew: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Dedup hash
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
async function dedupFor(
|
|
250
|
+
visitorId: string,
|
|
251
|
+
flagKey: string,
|
|
252
|
+
version: number,
|
|
253
|
+
variant: string,
|
|
254
|
+
event: string,
|
|
255
|
+
): Promise<string> {
|
|
256
|
+
const input = `${visitorId}|${flagKey}|${version}|${variant}|${event}`;
|
|
257
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
258
|
+
const bytes = new Uint8Array(digest);
|
|
259
|
+
let hex = '';
|
|
260
|
+
for (let i = 0; i < 16; i++) hex += bytes[i].toString(16).padStart(2, '0');
|
|
261
|
+
return hex;
|
|
262
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic bucketing for A/B assignments.
|
|
3
|
+
*
|
|
4
|
+
* Both the site SDK (local assignment) and the platform event endpoint
|
|
5
|
+
* (server-side validation) compute the same hash → variant mapping.
|
|
6
|
+
* Any change here must ship to both simultaneously, or assignments and
|
|
7
|
+
* validation will diverge.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BUCKET_SPACE = 10000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FNV-1a 32-bit over UTF-8 bytes, reduced mod 10000.
|
|
14
|
+
* Pure function, no crypto — determinism and portability beat pre-image
|
|
15
|
+
* resistance here. The attack surface on the mapping is the same for
|
|
16
|
+
* both sides; what matters is that site and platform agree bit-for-bit.
|
|
17
|
+
*/
|
|
18
|
+
export function bucketFor(
|
|
19
|
+
visitorId: string,
|
|
20
|
+
flagKey: string,
|
|
21
|
+
version: number,
|
|
22
|
+
): number {
|
|
23
|
+
const input = `${visitorId}|${flagKey}|${version}`;
|
|
24
|
+
const bytes = new TextEncoder().encode(input);
|
|
25
|
+
let hash = 0x811c9dc5;
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
27
|
+
hash ^= bytes[i];
|
|
28
|
+
hash = Math.imul(hash, 0x01000193);
|
|
29
|
+
}
|
|
30
|
+
return (hash >>> 0) % BUCKET_SPACE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a bucket to a variant using cumulative weightBp ranges.
|
|
35
|
+
* Variants are consumed in the order given — callers must sort variants
|
|
36
|
+
* consistently on both sides (we sort by key).
|
|
37
|
+
*
|
|
38
|
+
* Returns null iff bucket >= trafficAllocationBp (visitor not enrolled).
|
|
39
|
+
*/
|
|
40
|
+
export function variantForBucket(
|
|
41
|
+
bucket: number,
|
|
42
|
+
variants: { key: string; weightBp: number }[],
|
|
43
|
+
trafficAllocationBp: number,
|
|
44
|
+
): string | null {
|
|
45
|
+
if (bucket >= trafficAllocationBp) return null;
|
|
46
|
+
const sorted = [...variants].sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
47
|
+
const totalWeight = sorted.reduce((acc, v) => acc + v.weightBp, 0);
|
|
48
|
+
if (totalWeight <= 0) return null;
|
|
49
|
+
const scaled = Math.floor((bucket * totalWeight) / trafficAllocationBp);
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
for (const v of sorted) {
|
|
52
|
+
cursor += v.weightBp;
|
|
53
|
+
if (scaled < cursor) return v.key;
|
|
54
|
+
}
|
|
55
|
+
return sorted[sorted.length - 1].key;
|
|
56
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -62,8 +62,20 @@ export { HttpClient, createApiError } from './utils/http-client';
|
|
|
62
62
|
// Cache utilities
|
|
63
63
|
export { CacheManager } from './cache/cache-manager';
|
|
64
64
|
export { CloudflareKVCacheAdapter } from './cache/cloudflare-kv-adapter';
|
|
65
|
-
|
|
65
|
+
export { InMemoryCacheAdapter } from './cache/in-memory-adapter';
|
|
66
66
|
export { NoopCacheAdapter } from './cache/noop-adapter';
|
|
67
|
+
export type { CacheAdapter, CacheConfig } from './cache/types';
|
|
68
|
+
|
|
69
|
+
// A/B testing
|
|
70
|
+
export { createPerspectAb } from './ab/ab-client';
|
|
71
|
+
export { bucketFor, variantForBucket } from './ab/bucketing';
|
|
72
|
+
export type {
|
|
73
|
+
CreatePerspectAbOptions,
|
|
74
|
+
PerspectAb,
|
|
75
|
+
AbClient,
|
|
76
|
+
AbVariantResult,
|
|
77
|
+
AbForRequestResult,
|
|
78
|
+
} from './ab/ab-client';
|
|
67
79
|
|
|
68
80
|
// Image transformation utilities
|
|
69
81
|
export {
|
|
@@ -87,6 +99,8 @@ export {
|
|
|
87
99
|
loadProductBySlug,
|
|
88
100
|
loadPosts,
|
|
89
101
|
loadPages,
|
|
102
|
+
loadBlocks,
|
|
103
|
+
loadBlockBySlug,
|
|
90
104
|
loadContentBySlug,
|
|
91
105
|
loadAllContent,
|
|
92
106
|
createCheckoutSession,
|
|
@@ -100,6 +114,7 @@ export type {
|
|
|
100
114
|
LoadProductBySlugOptions,
|
|
101
115
|
LoadContentOptions,
|
|
102
116
|
LoadContentBySlugOptions,
|
|
117
|
+
LoadBlockBySlugOptions,
|
|
103
118
|
CheckoutSessionOptions,
|
|
104
119
|
LoaderLogger
|
|
105
120
|
} from './loaders';
|
package/src/loaders.ts
CHANGED
|
@@ -615,6 +615,83 @@ export async function loadContentBySlug(
|
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
export interface LoadBlockBySlugOptions extends LoaderOptions {
|
|
619
|
+
slug: string;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Load published blocks for a site.
|
|
624
|
+
*/
|
|
625
|
+
export async function loadBlocks(
|
|
626
|
+
options: LoadContentOptions
|
|
627
|
+
): Promise<BlogPost[]> {
|
|
628
|
+
const { client, siteName, logger = noopLogger, params } = options;
|
|
629
|
+
|
|
630
|
+
if (!client) {
|
|
631
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, cannot load blocks');
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
log(logger, 'info', `[PerspectAPI] Loading blocks for site "${siteName}"`);
|
|
637
|
+
const requestedLimit =
|
|
638
|
+
options.limit !== undefined ? options.limit : params?.limit;
|
|
639
|
+
const resolvedLimit = validateLimit(
|
|
640
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
641
|
+
'blocks query',
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const response: PaginatedResponse<Content> = await client.content.getContent(
|
|
645
|
+
siteName,
|
|
646
|
+
{
|
|
647
|
+
...params,
|
|
648
|
+
page_status: (params?.page_status ?? 'publish') as ContentStatus,
|
|
649
|
+
page_type: 'block' as ContentType,
|
|
650
|
+
limit: resolvedLimit
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
if (!response.data) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return response.data.map(content => transformContent(content, logger));
|
|
659
|
+
} catch (error) {
|
|
660
|
+
log(logger, 'error', '[PerspectAPI] Error loading blocks', error);
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Load a single block by slug.
|
|
667
|
+
*/
|
|
668
|
+
export async function loadBlockBySlug(
|
|
669
|
+
options: LoadBlockBySlugOptions
|
|
670
|
+
): Promise<BlogPost | null> {
|
|
671
|
+
const { client, siteName, slug, logger = noopLogger } = options;
|
|
672
|
+
|
|
673
|
+
if (!client) {
|
|
674
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, cannot load block');
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
log(logger, 'info', `[PerspectAPI] Loading block slug "${slug}" for site "${siteName}"`);
|
|
680
|
+
const response: ApiResponse<Content> =
|
|
681
|
+
await client.content.getContentBySlug(siteName, slug);
|
|
682
|
+
|
|
683
|
+
if (!response.data) {
|
|
684
|
+
log(logger, 'warn', `[PerspectAPI] Block not found for slug "${slug}"`);
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return transformContent(response.data, logger);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
log(logger, 'error', `[PerspectAPI] Error loading block slug "${slug}"`, error);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
618
695
|
/**
|
|
619
696
|
* Load all published content (pages + posts).
|
|
620
697
|
*/
|