init commit

This commit is contained in:
rxliuli
2025-11-04 05:03:50 +08:00
commit bce557cc2d
1396 changed files with 172991 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import type {
ClientIdentifier,
Host as NativeHost,
ProcessPlatform,
} from '@jet/environment';
import type {} from '@jet/engine'; // For ClientIdentifier.Unknown
export class Host implements NativeHost {
platform: ProcessPlatform = 'web';
get osBuild(): never {
throw makeWebDoesNotImplementException('osBuild');
}
get deviceModel(): string {
return 'web';
}
get devicePhysicalModel(): never {
throw makeWebDoesNotImplementException('devicePhysicalModel');
}
get deviceLocalizedModel() {
return '';
}
get deviceModelFamily(): never {
throw makeWebDoesNotImplementException('deviceModelFamily');
}
get clientIdentifier(): ClientIdentifier {
// We can't directly use the `ClientIdentifier.Unknown` enum member value
// because we cannot access "ambient const enums" with our TypeScript config.
// Enum handling is known to be tough in TypeScript and, for reasons like
// this, they are generally avoided.
// This returns a value defined on this enum by `@jet/engine`'s type definition
return 'unknown' as ClientIdentifier.Unknown;
}
get clientVersion(): never {
throw makeWebDoesNotImplementException('clientVersion');
}
isOSAtLeast(
_majorVersion: number,
_minorVersion: number,
_patchVersion: number,
): boolean {
return true;
}
}
export function makeWebDoesNotImplementException(property: keyof NativeHost) {
return new Error(
`\`Host\` property \`${property}\` is not implemented for the "web" platform`,
);
}

View File

@@ -0,0 +1,18 @@
import type { Random as IRandom } from '@jet/environment';
import { generateUuid } from '@amp/web-apps-utils';
export class Random implements IRandom {
nextBoolean(): boolean {
// See: https://stashweb.sd.apple.com/projects/AS/repos/jet-infrastructure/browse/Frameworks/JetEngine/JetEngine/JavaScript/Stack/Native%20APIs/JSRandomObject.swift?at=e90a88fa061f5cb6b9536d29a7ffd67e5db942db#41
return Math.random() < 0.5;
}
nextNumber(): number {
// See: https://stashweb.sd.apple.com/projects/AS/repos/jet-infrastructure/browse/Frameworks/JetEngine/JetEngine/JavaScript/Stack/Native%20APIs/JSRandomObject.swift?at=e90a88fa061f5cb6b9536d29a7ffd67e5db942db#45
return Math.random();
}
nextUUID(): string {
return generateUuid();
}
}

View File

@@ -0,0 +1,58 @@
import { getCookie } from '@amp/web-app-components/src/utils/cookie';
import type { LoggerFactory } from '@amp/web-apps-logger';
import { isSome } from '@amp/web-apps-utils';
import { deserializeServerData, stableStringify } from './server-data';
import { type PrefetchedIntent, isPrefetchedIntents } from './types';
export function getPrefetchedIntents(
loggerFactory: LoggerFactory,
options?: { evenIfSignedIn?: boolean; featureKitItfe?: string },
): Map<string, unknown> {
const logger = loggerFactory.loggerFor('getPrefetchedIntents');
const evenIfSignedIn = options?.evenIfSignedIn;
const itfe = options?.featureKitItfe;
const data = deserializeServerData();
if (!data || !isPrefetchedIntents(data)) {
return new Map();
}
// We avoid prefetched intents in two scenarios:
//
// Condition 1: User is signed in (and evenIfSignedIn is false)
// It's possible/likely that dispatching an intent when signed in behaves
// differently.
//
// Condition 2: ITFE is enabled in Feature Kit
// When ITFE is active, we discard prefetched intents so that media API
// calls are triggered in the browser, allowing Feature Kit to inject ITFE
// into those calls.
if ((!evenIfSignedIn && getCookie('media-user-token')) || itfe) {
logger.info(
'Discarding prefetched intents - signed in user or ITFE enabled',
);
return new Map();
}
logger.debug('received prefetched intents from the server:', data);
return new Map(
data
.map(
({
intent,
data,
}: PrefetchedIntent): [string, unknown] | null => {
try {
if (intent.$kind.includes('Library')) {
return null;
}
// NOTE: PrefetchedIntents.get depends on stableStringify
return [stableStringify(intent), data];
} catch (e) {
return null;
}
},
)
.filter(isSome),
);
}

View File

@@ -0,0 +1,118 @@
import type { LoggerFactory } from '@amp/web-apps-logger';
import type { Intent, IntentReturnType } from '@jet/environment/dispatching';
import { serializeServerData, stableStringify } from './server-data';
import type { PrefetchedIntent } from './types';
import { getPrefetchedIntents } from './get-prefetched-intents';
export type { PrefetchedIntent } from './types';
export function serializePrefetchedIntents(
loggerFactory: LoggerFactory,
prefetchedIntents: PrefetchedIntent[],
): string {
const serialized = serializeServerData(
prefetchedIntents.map(removeSeoData),
);
if (serialized.length === 0) {
const logger = loggerFactory.loggerFor('serializePrefetchedIntents');
logger.warn('failed to serialize prefetched intents');
}
return serialized;
}
// SEO data is never needed for the first clientside render since the server
// already adds SEO tags. The seoData convention is ubiquitous across the apps.
// See: rdar://144581413 (Etag constantly changes on pages with songs due to seoData.ogSongs)
function removeSeoData(intent: PrefetchedIntent): PrefetchedIntent {
const { data } = intent;
// We very intentionally return the original intent to prevent
// needlessly allocating new objects.
if (data === null || typeof data !== 'object' || !('seoData' in data)) {
return intent;
}
const { seoData } = data;
if (seoData === null || typeof seoData !== 'object') {
return intent;
}
let partialSeoData:
| { pageTitle?: unknown; titleHeader?: unknown }
| undefined = undefined;
if ('pageTitle' in seoData || 'titleHeader' in seoData) {
partialSeoData = {};
if ('pageTitle' in seoData) {
partialSeoData['pageTitle'] = seoData.pageTitle;
}
if ('titleHeader' in seoData) {
partialSeoData['titleHeader'] = seoData.titleHeader;
}
}
// Only if we're actually going to do the removal do we spread
return {
...intent,
data: {
...data,
// Page title is desirable to keep as it is occasionally consulted
// outside of MetaTags.svelte
seoData: partialSeoData,
},
};
}
export class PrefetchedIntents {
static empty(): PrefetchedIntents {
return new PrefetchedIntents(new Map());
}
static fromDom(
loggerFactory: LoggerFactory,
options?: { evenIfSignedIn?: boolean; featureKitItfe?: string },
): PrefetchedIntents {
return new PrefetchedIntents(
getPrefetchedIntents(loggerFactory, options),
);
}
private intents: Map<string, unknown>;
private constructor(intents: Map<string, unknown>) {
this.intents = intents;
}
get<I extends Intent<unknown>>(intent: I): IntentReturnType<I> | undefined {
if (this.intents.size === 0) {
return;
}
let subject: string | void;
try {
subject = stableStringify(intent);
} catch (e) {
// It's possible the intents don't stringify. If that's that case,
// then we won't find it in this.intents, since the keys of that
// are successfully stringified intents. We could try something
// sophisticated here, but it's probably not worth it as most
// intents will serialize.
return;
}
const data = this.intents.get(subject);
// Remove the prefetched data so that it can only be used once
this.intents.delete(subject);
// NOTE: There really isn't a good way to be safe with types here. We
// don't have a type guard for arbitrary IntentReturnType<I>. We just
// have to trust that the serialized data is of the correct type. This
// isn't unreasonable since we control serialization.
return data as unknown as IntentReturnType<I> | undefined;
}
}

View File

@@ -0,0 +1,109 @@
import { isPOJO } from '@amp/web-apps-utils';
// NOTE: be careful with imports here. This file is imported by browser code,
// so we expect tree shaking to only keep these functions.
const SERVER_DATA_ID = 'serialized-server-data';
// Take care with < (which has special meaning inside script tags)
// See: https://github.com/sveltejs/kit/blob/ff9a27b4/packages/kit/src/runtime/server/page/serialize_data.js#L4-L28
const replacements = {
'<': '\\u003C',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
};
const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
/**
* Serializes a POJO into a HTML <script> tag that can be read clientside by
* `deserializeServerData`.
*
* Use this to share data between serverside and clientside. Include the
* returned HTML in the response to a client to allow it to read this data.
*
* @param data data to serialize
* @returns serialized data (or empty string if serialization fails)
*/
export function serializeServerData(data: object): string {
try {
const sanitizedData = JSON.stringify(data).replace(
pattern,
(match) => replacements[match as keyof typeof replacements],
);
return `<script type="application/json" id="${SERVER_DATA_ID}">${sanitizedData}</script>`;
} catch (e) {
// Don't let recursive data (or other non-serializable things) throw.
// We'd rather just let the serialize no-op to avoid breaking consumers.
return '';
}
}
/**
* Deserializes data serialized on the server by `serializeServerData`.
*
* @returns deserialized data (or undefined if it doesn't exist/errors)
*/
export function deserializeServerData(): ReturnType<JSON['parse']> | undefined {
const script = document.getElementById(SERVER_DATA_ID);
if (!script) {
return;
}
script.parentNode?.removeChild(script);
try {
return JSON.parse(script.textContent || '');
} catch (e) {
// If the content is malformed, we want to avoid throwing. This
// situation should be impossible since we control the serialization
// above.
return;
}
}
/**
* JSON stringify a POJO value in a stable manner. Specifically, this means that
* objects which are structurally equal serialize to the same string.
*
* This is useful when comparing objects serialized by a server against objects
* build in browser. With plain JSON.stringify(), property order matters and is
* not guaranteed to be the same. In other words these two objects would
* JSON.stringify() differently:
*
* { a: 1, b: 2 }
* { b: 2, a: 1 }
*
* But these are structurally equal--they have the same keys and values.
*
* The expected use case for this function is generating keys for a Map for
* objects from a server that will be compared against objects from the browser.
* This function should be used on objects returned from `deserializeServerData`
* before they are used in such contexts.
*
* See: https://stackoverflow.com/a/43049877
*/
export function stableStringify(data: unknown): string {
if (Array.isArray(data)) {
const items = data.map(stableStringify).join(',');
return `[${items}]`;
}
// Sort object keys before serializing
if (isPOJO(data)) {
const keys = [...Object.keys(data)];
keys.sort();
const properties = keys
// undefined values should not get included in stringification
.filter((key) => typeof data[key] !== 'undefined')
.map(
(key) => `${JSON.stringify(key)}:${stableStringify(data[key])}`,
)
.join(',');
return `{${properties}}`;
}
return JSON.stringify(data);
}

View File

@@ -0,0 +1,27 @@
import type { Intent } from '@jet/environment/dispatching';
export interface PrefetchedIntent {
intent: Intent<unknown>;
data: unknown;
}
export function isPrefetchedIntents(v: unknown): v is PrefetchedIntent[] {
return Array.isArray(v) && v.every(isPrefetchedIntent);
}
function isPrefetchedIntent(v: unknown): v is PrefetchedIntent {
return hasIntentAndData(v) && isIntent(v.intent);
}
function hasIntentAndData(v: unknown): v is HasIntentAndData {
return v !== null && typeof v === 'object' && 'intent' in v && 'data' in v;
}
interface HasIntentAndData {
intent: unknown;
data: unknown;
}
function isIntent(v: unknown): v is Intent<unknown> {
return v !== null && typeof v === 'object' && '$kind' in v;
}