@stackframe/stack-shared 2.8.56 → 2.8.59
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/apps/apps-config.d.mts +6 -0
- package/dist/apps/apps-config.d.ts +6 -0
- package/dist/apps/apps-config.js +6 -0
- package/dist/apps/apps-config.js.map +1 -1
- package/dist/config/migrate-catalogs-to-product-lines.d.mts +12 -0
- package/dist/config/migrate-catalogs-to-product-lines.d.ts +12 -0
- package/dist/config/migrate-catalogs-to-product-lines.js +211 -0
- package/dist/config/migrate-catalogs-to-product-lines.js.map +1 -0
- package/dist/config/schema-fuzzer.test.js +20 -6
- package/dist/config/schema-fuzzer.test.js.map +1 -1
- package/dist/config/schema.d.mts +296 -224
- package/dist/config/schema.d.ts +296 -224
- package/dist/config/schema.js +46 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/esm/apps/apps-config.js +6 -0
- package/dist/esm/apps/apps-config.js.map +1 -1
- package/dist/esm/config/migrate-catalogs-to-product-lines.js +186 -0
- package/dist/esm/config/migrate-catalogs-to-product-lines.js.map +1 -0
- package/dist/esm/config/schema-fuzzer.test.js +20 -6
- package/dist/esm/config/schema-fuzzer.test.js.map +1 -1
- package/dist/esm/config/schema.js +47 -9
- package/dist/esm/config/schema.js.map +1 -1
- package/dist/esm/interface/admin-interface.js +49 -1
- package/dist/esm/interface/admin-interface.js.map +1 -1
- package/dist/esm/interface/client-interface.js +124 -25
- package/dist/esm/interface/client-interface.js.map +1 -1
- package/dist/esm/interface/crud/current-user.js +5 -2
- package/dist/esm/interface/crud/current-user.js.map +1 -1
- package/dist/esm/interface/crud/email-outbox.js +204 -0
- package/dist/esm/interface/crud/email-outbox.js.map +1 -0
- package/dist/esm/interface/crud/emails.js +0 -2
- package/dist/esm/interface/crud/emails.js.map +1 -1
- package/dist/esm/interface/crud/products.js +12 -1
- package/dist/esm/interface/crud/products.js.map +1 -1
- package/dist/esm/interface/crud/projects.js +3 -1
- package/dist/esm/interface/crud/projects.js.map +1 -1
- package/dist/esm/interface/crud/users.js +9 -2
- package/dist/esm/interface/crud/users.js.map +1 -1
- package/dist/esm/interface/server-interface.js +54 -0
- package/dist/esm/interface/server-interface.js.map +1 -1
- package/dist/esm/known-errors.js +69 -1
- package/dist/esm/known-errors.js.map +1 -1
- package/dist/esm/schema-fields.js +27 -3
- package/dist/esm/schema-fields.js.map +1 -1
- package/dist/esm/sessions.js +72 -8
- package/dist/esm/sessions.js.map +1 -1
- package/dist/esm/utils/env.js +13 -2
- package/dist/esm/utils/env.js.map +1 -1
- package/dist/esm/utils/esbuild.js +50 -21
- package/dist/esm/utils/esbuild.js.map +1 -1
- package/dist/esm/utils/globals.js +12 -0
- package/dist/esm/utils/globals.js.map +1 -1
- package/dist/esm/utils/paginated-lists.js +153 -23
- package/dist/esm/utils/paginated-lists.js.map +1 -1
- package/dist/esm/utils/paginated-lists.test.js +842 -0
- package/dist/esm/utils/paginated-lists.test.js.map +1 -0
- package/dist/esm/utils/proxies.js +28 -1
- package/dist/esm/utils/proxies.js.map +1 -1
- package/dist/esm/utils/react.js +7 -3
- package/dist/esm/utils/react.js.map +1 -1
- package/dist/esm/utils/results.js.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/interface/admin-interface.d.mts +26 -3
- package/dist/interface/admin-interface.d.ts +26 -3
- package/dist/interface/admin-interface.js +49 -1
- package/dist/interface/admin-interface.js.map +1 -1
- package/dist/interface/client-interface.d.mts +36 -0
- package/dist/interface/client-interface.d.ts +36 -0
- package/dist/interface/client-interface.js +124 -25
- package/dist/interface/client-interface.js.map +1 -1
- package/dist/interface/crud/current-user.d.mts +23 -6
- package/dist/interface/crud/current-user.d.ts +23 -6
- package/dist/interface/crud/current-user.js +5 -2
- package/dist/interface/crud/current-user.js.map +1 -1
- package/dist/interface/crud/email-outbox.d.mts +1075 -0
- package/dist/interface/crud/email-outbox.d.ts +1075 -0
- package/dist/interface/crud/email-outbox.js +241 -0
- package/dist/interface/crud/email-outbox.js.map +1 -0
- package/dist/interface/crud/emails.d.mts +0 -34
- package/dist/interface/crud/emails.d.ts +0 -34
- package/dist/interface/crud/emails.js +0 -2
- package/dist/interface/crud/emails.js.map +1 -1
- package/dist/interface/crud/products.d.mts +77 -0
- package/dist/interface/crud/products.d.ts +77 -0
- package/dist/interface/crud/products.js +12 -1
- package/dist/interface/crud/products.js.map +1 -1
- package/dist/interface/crud/project-api-keys.d.mts +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +1 -1
- package/dist/interface/crud/projects.d.mts +70 -66
- package/dist/interface/crud/projects.d.ts +70 -66
- package/dist/interface/crud/projects.js +3 -1
- package/dist/interface/crud/projects.js.map +1 -1
- package/dist/interface/crud/team-member-profiles.d.mts +28 -12
- package/dist/interface/crud/team-member-profiles.d.ts +28 -12
- package/dist/interface/crud/users.d.mts +38 -6
- package/dist/interface/crud/users.d.ts +38 -6
- package/dist/interface/crud/users.js +9 -2
- package/dist/interface/crud/users.js.map +1 -1
- package/dist/interface/server-interface.d.mts +52 -0
- package/dist/interface/server-interface.d.ts +52 -0
- package/dist/interface/server-interface.js +54 -0
- package/dist/interface/server-interface.js.map +1 -1
- package/dist/interface/webhooks.d.mts +18 -2
- package/dist/interface/webhooks.d.ts +18 -2
- package/dist/known-errors.d.mts +20 -1
- package/dist/known-errors.d.ts +20 -1
- package/dist/known-errors.js +69 -1
- package/dist/known-errors.js.map +1 -1
- package/dist/schema-fields.d.mts +38 -5
- package/dist/schema-fields.d.ts +38 -5
- package/dist/schema-fields.js +33 -3
- package/dist/schema-fields.js.map +1 -1
- package/dist/sessions.d.mts +35 -4
- package/dist/sessions.d.ts +35 -4
- package/dist/sessions.js +72 -8
- package/dist/sessions.js.map +1 -1
- package/dist/utils/env.d.mts +2 -1
- package/dist/utils/env.d.ts +2 -1
- package/dist/utils/env.js +13 -1
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/esbuild.js +49 -20
- package/dist/utils/esbuild.js.map +1 -1
- package/dist/utils/globals.d.mts +6 -1
- package/dist/utils/globals.d.ts +6 -1
- package/dist/utils/globals.js +13 -0
- package/dist/utils/globals.js.map +1 -1
- package/dist/utils/paginated-lists.d.mts +269 -12
- package/dist/utils/paginated-lists.d.ts +269 -12
- package/dist/utils/paginated-lists.js +153 -23
- package/dist/utils/paginated-lists.js.map +1 -1
- package/dist/utils/paginated-lists.test.d.mts +2 -0
- package/dist/utils/paginated-lists.test.d.ts +2 -0
- package/dist/utils/paginated-lists.test.js +844 -0
- package/dist/utils/paginated-lists.test.js.map +1 -0
- package/dist/utils/proxies.d.mts +8 -1
- package/dist/utils/proxies.d.ts +8 -1
- package/dist/utils/proxies.js +30 -2
- package/dist/utils/proxies.js.map +1 -1
- package/dist/utils/react.d.mts +1 -1
- package/dist/utils/react.d.ts +1 -1
- package/dist/utils/react.js +7 -3
- package/dist/utils/react.js.map +1 -1
- package/dist/utils/results.d.mts +5 -5
- package/dist/utils/results.d.ts +5 -5
- package/dist/utils/results.js.map +1 -1
- package/package.json +5 -4
- package/CHANGELOG.md +0 -1354
- package/dist/esm/interface/crud/config.js +0 -40
- package/dist/esm/interface/crud/config.js.map +0 -1
- package/dist/interface/crud/config.d.mts +0 -49
- package/dist/interface/crud/config.d.ts +0 -49
- package/dist/interface/crud/config.js +0 -79
- package/dist/interface/crud/config.js.map +0 -1
package/dist/esm/sessions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sessions.ts"],"sourcesContent":["import * as jose from 'jose';\nimport { InferType } from 'yup';\nimport { accessTokenPayloadSchema } from './schema-fields';\nimport { StackAssertionError, throwErr } from \"./utils/errors\";\nimport { Store } from \"./utils/stores\";\n\n\nexport type AccessTokenPayload = InferType<typeof accessTokenPayloadSchema>;\n\nfunction decodeAccessTokenIfValid(token: string): AccessTokenPayload | null {\n try {\n const payload = jose.decodeJwt(token);\n return accessTokenPayloadSchema.validateSync(payload);\n } catch (e) {\n return null;\n }\n}\n\nexport class AccessToken {\n static createIfValid(token: string): AccessToken | null {\n const payload = decodeAccessTokenIfValid(token);\n if (!payload) return null;\n return new AccessToken(token);\n }\n\n private constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n\n get payload() {\n return decodeAccessTokenIfValid(this.token) ?? throwErr(\"Invalid access token in payload (should've been validated in createIfValid)\", { token: this.token });\n }\n\n get expiresAt(): Date {\n const { exp } = this.payload;\n if (exp === undefined) return new Date(8640000000000000); // max date value\n return new Date(exp * 1000);\n }\n\n /**\n * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.\n */\n get expiresInMillis(): number {\n return Math.max(0, this.expiresAt.getTime() - Date.now());\n }\n\n isExpired(): boolean {\n return this.expiresInMillis <= 0;\n }\n}\n\nexport class RefreshToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Refresh token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n}\n\n/**\n * An InternalSession represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.\n *\n * A session never changes which user or session it belongs to, but the tokens in it may change over time.\n */\nexport class InternalSession {\n /**\n * Each session has a session key that depends on the tokens inside. If the session has a refresh token, the session key depends only on the refresh token. If the session does not have a refresh token, the session key depends only on the access token.\n *\n * Multiple Session objects may have the same session key, which implies that they represent the same session by the same user. Furthermore, a session's key never changes over the lifetime of a session object.\n *\n * This is useful for caching and indexing sessions.\n */\n public readonly sessionKey: string;\n\n /**\n * An access token that is not known to be invalid (ie. may be valid, but may have expired).\n */\n private _accessToken: Store<AccessToken | null>;\n private readonly _refreshToken: RefreshToken | null;\n\n /**\n * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).\n *\n * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is\n * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to\n * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed\n * in an access token, eg. in a server-side request handler).\n */\n private _knownToBeInvalid = new Store<boolean>(false);\n\n private _refreshPromise: Promise<AccessToken | null> | null = null;\n\n constructor(private readonly _options: {\n refreshAccessTokenCallback(refreshToken: RefreshToken): Promise<AccessToken | null>,\n refreshToken: string | null,\n accessToken?: string | null,\n }) {\n this._accessToken = new Store(_options.accessToken ? AccessToken.createIfValid(_options.accessToken) : null);\n this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;\n if (_options.accessToken === null && _options.refreshToken === null) {\n // this session is already invalid\n this._knownToBeInvalid.set(true);\n }\n this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });\n }\n\n static calculateSessionKey(ofTokens: { refreshToken: string | null, accessToken?: string | null }): string {\n if (ofTokens.refreshToken) {\n return `refresh-${ofTokens.refreshToken}`;\n } else if (ofTokens.accessToken) {\n return `access-${ofTokens.accessToken}`;\n } else {\n return \"not-logged-in\";\n }\n }\n\n isKnownToBeInvalid() {\n return this._knownToBeInvalid.get();\n }\n\n /**\n * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.\n */\n markInvalid() {\n this._accessToken.set(null);\n this._knownToBeInvalid.set(true);\n }\n\n onInvalidate(callback: () => void): { unsubscribe: () => void } {\n return this._knownToBeInvalid.onChange(() => callback());\n }\n\n /**\n * Returns the access token if it is found in the cache and not expired yet, or null otherwise. Never fetches new tokens.\n */\n getAccessTokenIfNotExpiredYet(minMillisUntilExpiration: number): AccessToken | null {\n if (minMillisUntilExpiration > 60_000) {\n throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);\n }\n\n const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();\n if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) return null;\n return accessToken;\n }\n\n /**\n * Returns the access token if it is found in the cache, fetching it otherwise.\n *\n * This is usually the function you want to call to get an access token. Either set `minMillisUntilExpiration` to a reasonable value, or catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).\n *\n * @returns null if the session is known to be invalid, cached tokens if they exist in the cache and the access token hasn't expired yet (the refresh token might still be invalid), or new tokens otherwise.\n */\n async getOrFetchLikelyValidTokens(minMillisUntilExpiration: number): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = this.getAccessTokenIfNotExpiredYet(minMillisUntilExpiration);\n if (!accessToken) {\n const newTokens = await this.fetchNewTokens();\n const expiresInMillis = newTokens?.accessToken.expiresInMillis;\n if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {\n throw new StackAssertionError(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short when they're generated (${expiresInMillis}ms)`);\n }\n return newTokens;\n }\n return { accessToken, refreshToken: this._refreshToken };\n }\n\n /**\n * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.\n *\n * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.\n *\n * In most cases, you should prefer `getOrFetchLikelyValidTokens`.\n *\n * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).\n */\n async fetchNewTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = await this._getNewlyFetchedAccessToken();\n return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;\n }\n\n markAccessTokenExpired(accessToken: AccessToken) {\n // TODO we don't need this anymore, since we now check the expiry by ourselves\n if (this._accessToken.get() === accessToken) {\n this._accessToken.set(null);\n }\n }\n\n /**\n * Note that a callback invocation with `null` does not mean the session has been invalidated; the access token may just have expired. Use `onInvalidate` to detect invalidation.\n */\n onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): { unsubscribe: () => void } {\n return this._accessToken.onChange(callback);\n }\n\n /**\n * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.\n */\n private _getPotentiallyInvalidAccessTokenIfAvailable(): AccessToken | null {\n if (!this._refreshToken) return null;\n if (this.isKnownToBeInvalid()) return null;\n\n const accessToken = this._accessToken.get();\n if (accessToken && !accessToken.isExpired()) return accessToken;\n\n return null;\n }\n\n /**\n * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.\n *\n * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.\n */\n private async _getNewlyFetchedAccessToken(): Promise<AccessToken | null> {\n if (!this._refreshToken) return null;\n if (this._knownToBeInvalid.get()) return null;\n\n if (!this._refreshPromise) {\n this._refreshAndSetRefreshPromise(this._refreshToken);\n }\n return await this._refreshPromise;\n }\n\n private _refreshAndSetRefreshPromise(refreshToken: RefreshToken) {\n let refreshPromise: Promise<AccessToken | null> = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {\n if (refreshPromise === this._refreshPromise) {\n this._refreshPromise = null;\n this._accessToken.set(accessToken);\n if (!accessToken) {\n this.markInvalid();\n }\n }\n return accessToken;\n });\n this._refreshPromise = refreshPromise;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/sessions.ts"],"sourcesContent":["import * as jose from 'jose';\nimport { InferType } from 'yup';\nimport { accessTokenPayloadSchema } from './schema-fields';\nimport { StackAssertionError, throwErr } from \"./utils/errors\";\nimport { runAsynchronously, wait } from './utils/promises';\nimport { Store } from \"./utils/stores\";\n\n\nexport type AccessTokenPayload = InferType<typeof accessTokenPayloadSchema>;\n\nfunction decodeAccessTokenIfValid(token: string): AccessTokenPayload | null {\n try {\n const payload = jose.decodeJwt(token);\n return accessTokenPayloadSchema.validateSync(payload);\n } catch (e) {\n return null;\n }\n}\n\nexport class AccessToken {\n static createIfValid(token: string): AccessToken | null {\n const payload = decodeAccessTokenIfValid(token);\n if (!payload) return null;\n return new AccessToken(token);\n }\n\n private constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n\n get payload() {\n return decodeAccessTokenIfValid(this.token) ?? throwErr(\"Invalid access token in payload (should've been validated in createIfValid)\", { token: this.token });\n }\n\n get expiresAt(): Date {\n const { exp } = this.payload;\n if (exp === undefined) return new Date(8640000000000000); // max date value\n return new Date(exp * 1000);\n }\n\n get issuedAt(): Date {\n const { iat } = this.payload;\n return new Date(iat * 1000);\n }\n\n /**\n * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.\n */\n get expiresInMillis(): number {\n return Math.max(0, this.expiresAt.getTime() - Date.now());\n }\n\n get issuedMillisAgo(): number {\n return Math.max(0, Date.now() - this.issuedAt.getTime());\n }\n\n isExpired(): boolean {\n return this.expiresInMillis <= 0;\n }\n}\n\nexport class RefreshToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Refresh token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n}\n\n/**\n * An InternalSession represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.\n *\n * A session never changes which user or session it belongs to, but the tokens in it may change over time.\n */\nexport class InternalSession {\n /**\n * Each session has a session key that depends on the tokens inside. If the session has a refresh token, the session key depends only on the refresh token. If the session does not have a refresh token, the session key depends only on the access token.\n *\n * Multiple Session objects may have the same session key, which implies that they represent the same session by the same user. Furthermore, a session's key never changes over the lifetime of a session object.\n *\n * This is useful for caching and indexing sessions.\n */\n public readonly sessionKey: string;\n\n /**\n * An access token that is not known to be invalid (ie. may be valid, but may have expired).\n */\n private _accessToken: Store<AccessToken | null>;\n private readonly _refreshToken: RefreshToken | null;\n\n /**\n * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).\n *\n * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is\n * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to\n * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed\n * in an access token, eg. in a server-side request handler).\n */\n private _knownToBeInvalid = new Store<boolean>(false);\n\n private _refreshPromise: Promise<AccessToken | null> | null = null;\n\n constructor(private readonly _options: {\n refreshAccessTokenCallback(refreshToken: RefreshToken): Promise<AccessToken | null>,\n refreshToken: string | null,\n accessToken?: string | null,\n }) {\n this._accessToken = new Store(_options.accessToken ? AccessToken.createIfValid(_options.accessToken) : null);\n this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;\n if (_options.accessToken === null && _options.refreshToken === null) {\n // this session is already invalid\n this._knownToBeInvalid.set(true);\n }\n this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });\n }\n\n static calculateSessionKey(ofTokens: { refreshToken: string | null, accessToken?: string | null }): string {\n if (ofTokens.refreshToken) {\n return `refresh-${ofTokens.refreshToken}`;\n } else if (ofTokens.accessToken) {\n return `access-${ofTokens.accessToken}`;\n } else {\n return \"not-logged-in\";\n }\n }\n\n isKnownToBeInvalid() {\n return this._knownToBeInvalid.get();\n }\n\n /**\n * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used. There is no\n * way out of this state, and the session object will never return valid tokens again.\n */\n markInvalid() {\n this._accessToken.set(null);\n this._knownToBeInvalid.set(true);\n }\n\n onInvalidate(callback: () => void): { unsubscribe: () => void } {\n return this._knownToBeInvalid.onChange(() => callback());\n }\n\n getRefreshToken(): RefreshToken | null {\n if (this.isKnownToBeInvalid()) return null;\n return this._refreshToken;\n }\n\n /**\n * Returns the access token if it is found in the cache and not expired yet, or null otherwise. Never fetches new tokens.\n */\n getAccessTokenIfNotExpiredYet(minMillisUntilExpiration: number, maxMillisSinceIssued: number | null): AccessToken | null {\n if (minMillisUntilExpiration > 45_000) {\n throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 45s`);\n }\n if (maxMillisSinceIssued !== null && maxMillisSinceIssued < 15_000) {\n throw new Error(`Required access token issuance ${maxMillisSinceIssued}ms is too short; assume that access token generation can take at least 15s`);\n }\n\n const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();\n if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) return null;\n if (maxMillisSinceIssued !== null && accessToken.issuedMillisAgo > maxMillisSinceIssued) return null;\n return accessToken;\n }\n\n /**\n * Returns the access token if it is found in the cache, fetching it otherwise.\n *\n * This is usually the function you want to call to get an access token. Either set `minMillisUntilExpiration` to a reasonable value, or catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).\n *\n * @returns null if the session is known to be invalid, cached tokens if they exist in the cache and the access token hasn't expired yet (the refresh token might still be invalid), or new tokens otherwise.\n */\n async getOrFetchLikelyValidTokens(minMillisUntilExpiration: number, maxMillisSinceIssued: number | null): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n // fast path to save a roundtrip to the server if the session is known to be invalid\n if (this.isKnownToBeInvalid()) return null;\n\n const accessToken = this.getAccessTokenIfNotExpiredYet(minMillisUntilExpiration, maxMillisSinceIssued);\n if (!accessToken) {\n const newTokens = await this.fetchNewTokens();\n const expiresInMillis = newTokens?.accessToken.expiresInMillis;\n const issuedMillisAgo = newTokens?.accessToken.issuedMillisAgo;\n if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {\n throw new StackAssertionError(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short when they're generated (${expiresInMillis}ms)`);\n }\n if (maxMillisSinceIssued !== null && issuedMillisAgo && issuedMillisAgo > maxMillisSinceIssued) {\n throw new StackAssertionError(`Required access token issuance ${maxMillisSinceIssued}ms is too short; access token issuance is too slow (${issuedMillisAgo}ms)`);\n }\n return newTokens;\n }\n return { accessToken, refreshToken: this.getRefreshToken() };\n }\n\n /**\n * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.\n *\n * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.\n *\n * In most cases, you should prefer `getOrFetchLikelyValidTokens`.\n *\n * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).\n */\n async fetchNewTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = await this._getNewlyFetchedAccessToken();\n return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;\n }\n\n /**\n * Manually mark the access token as expired, even if the date on its payload may still be valid.\n *\n * You don't usually have to call this function anymore, but you may want to call suggestAccessTokenExpired\n * to hint that the access token should be refreshed as its data may have changed, if possible.\n */\n markAccessTokenExpired(accessToken?: AccessToken) {\n if (!accessToken || this._accessToken.get()?.token === accessToken.token) {\n this._accessToken.set(null);\n }\n }\n\n /**\n * Strongly suggests that the access token should be refreshed as its data may have changed, although it's up to this\n * implementation to decide whether or when the access token will be refreshed.\n *\n * This is particularly useful when the data associated with the access token may have changed for example due to an\n * update to the user's profile.\n *\n * The current implementation marks the access token as expired if and only if a refresh token is available (regardless of\n * whether the refresh token is actually valid or not), although this is not a guarantee and subject to change.\n *\n * If you need a stronger guarantee of revoking an access token, use markAccessTokenExpired instead.\n */\n suggestAccessTokenExpired(): void {\n if (this._refreshToken) {\n this.markAccessTokenExpired();\n }\n }\n\n startRefreshingAccessToken(minMillisUntilExpiration: number, maxMillisSinceIssued: number | null): { unsubscribe: () => void } {\n let canceled = false;\n runAsynchronously(async () => {\n while (!canceled) {\n const tokens = await this.getOrFetchLikelyValidTokens(minMillisUntilExpiration, maxMillisSinceIssued);\n if (!tokens) return; // session is invalid, stop refreshing\n const nextRefreshIn = Math.min(\n tokens.accessToken.expiresInMillis - minMillisUntilExpiration,\n (maxMillisSinceIssued ?? Infinity) - tokens.accessToken.issuedMillisAgo,\n );\n await wait(Math.max(1, nextRefreshIn));\n }\n });\n return {\n unsubscribe: () => {\n canceled = true;\n },\n };\n }\n\n /**\n * Note that a callback invocation with `null` does not mean the session has been invalidated; the access token may just have expired. Use `onInvalidate` to detect invalidation.\n */\n onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): { unsubscribe: () => void } {\n return this._accessToken.onChange(callback);\n }\n\n /**\n * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.\n */\n private _getPotentiallyInvalidAccessTokenIfAvailable(): AccessToken | null {\n if (!this._refreshToken) return null;\n if (this.isKnownToBeInvalid()) return null;\n\n const accessToken = this._accessToken.get();\n if (accessToken && !accessToken.isExpired()) return accessToken;\n\n return null;\n }\n\n /**\n * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.\n *\n * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.\n */\n private async _getNewlyFetchedAccessToken(): Promise<AccessToken | null> {\n if (!this._refreshToken) return null;\n if (this._knownToBeInvalid.get()) return null;\n\n if (!this._refreshPromise) {\n this._refreshAndSetRefreshPromise(this._refreshToken);\n }\n return await this._refreshPromise;\n }\n\n private _refreshAndSetRefreshPromise(refreshToken: RefreshToken) {\n let refreshPromise: Promise<AccessToken | null> = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {\n if (refreshPromise === this._refreshPromise) {\n this._refreshPromise = null;\n this._accessToken.set(accessToken);\n if (!accessToken) {\n this.markInvalid();\n }\n }\n return accessToken;\n });\n this._refreshPromise = refreshPromise;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/esm/utils/env.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/utils/env.tsx
|
|
2
|
-
import { throwErr } from "./errors.js";
|
|
2
|
+
import { StackAssertionError, throwErr } from "./errors.js";
|
|
3
3
|
import { deindent } from "./strings.js";
|
|
4
4
|
function isBrowserLike() {
|
|
5
5
|
return typeof window !== "undefined" && typeof document !== "undefined" && typeof document.createElement !== "undefined";
|
|
@@ -34,7 +34,7 @@ function getEnvVariable(name, defaultValue) {
|
|
|
34
34
|
if (value) break;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (value
|
|
37
|
+
if (!value) {
|
|
38
38
|
if (defaultValue !== void 0) {
|
|
39
39
|
value = defaultValue;
|
|
40
40
|
} else {
|
|
@@ -43,6 +43,16 @@ function getEnvVariable(name, defaultValue) {
|
|
|
43
43
|
}
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
|
+
function getEnvBoolean(name) {
|
|
47
|
+
const value = getEnvVariable(name, "false");
|
|
48
|
+
if (value === "true") {
|
|
49
|
+
return true;
|
|
50
|
+
} else if (value === "false") {
|
|
51
|
+
return false;
|
|
52
|
+
} else {
|
|
53
|
+
throw new StackAssertionError(`Environment variable ${name} must be either "true" or "false": found ${JSON.stringify(value)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
46
56
|
function getNextRuntime() {
|
|
47
57
|
return process.env.NEXT_RUNTIME || throwErr("Missing environment variable: NEXT_RUNTIME");
|
|
48
58
|
}
|
|
@@ -50,6 +60,7 @@ function getNodeEnvironment() {
|
|
|
50
60
|
return getEnvVariable("NODE_ENV", "");
|
|
51
61
|
}
|
|
52
62
|
export {
|
|
63
|
+
getEnvBoolean,
|
|
53
64
|
getEnvVariable,
|
|
54
65
|
getNextRuntime,
|
|
55
66
|
getNodeEnvironment,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/env.tsx"],"sourcesContent":["import { throwErr } from \"./errors\";\nimport { deindent } from \"./strings\";\n\nexport function isBrowserLike() {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\" && typeof document.createElement !== \"undefined\";\n}\n\n// newName: oldName\nconst ENV_VAR_RENAME: Record<string, string[]> = {\n NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'],\n};\n\n/**\n * Returns the environment variable with the given name, returning the default (if given) or throwing an error (otherwise) if it's undefined or the empty string.\n */\nexport function getEnvVariable(name: string, defaultValue?: string | undefined): string {\n if (isBrowserLike()) {\n throw new Error(deindent`\n Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client.\n \n Use process.env.XYZ directly instead.\n `);\n }\n if (name === \"NEXT_RUNTIME\") {\n throw new Error(deindent`\n Can't use getEnvVariable to access the NEXT_RUNTIME environment variable because it's compiled into the client bundle.\n \n Use getNextRuntime() instead.\n `);\n }\n\n // throw error if the old name is used as the retrieve key\n for (const [newName, oldNames] of Object.entries(ENV_VAR_RENAME)) {\n if (oldNames.includes(name)) {\n throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);\n }\n }\n\n let value = process.env[name];\n\n // check the key under the old name if the new name is not found\n if (!value && ENV_VAR_RENAME[name] as any) {\n for (const oldName of ENV_VAR_RENAME[name]) {\n value = process.env[oldName];\n if (value) break;\n }\n }\n\n if (value
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/env.tsx"],"sourcesContent":["import { StackAssertionError, throwErr } from \"./errors\";\nimport { deindent } from \"./strings\";\n\nexport function isBrowserLike() {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\" && typeof document.createElement !== \"undefined\";\n}\n\n// newName: oldName\nconst ENV_VAR_RENAME: Record<string, string[]> = {\n NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'],\n};\n\n/**\n * Returns the environment variable with the given name, returning the default (if given) or throwing an error (otherwise) if it's undefined or the empty string.\n */\nexport function getEnvVariable(name: string, defaultValue?: string | undefined): string {\n if (isBrowserLike()) {\n throw new Error(deindent`\n Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client.\n \n Use process.env.XYZ directly instead.\n `);\n }\n if (name === \"NEXT_RUNTIME\") {\n throw new Error(deindent`\n Can't use getEnvVariable to access the NEXT_RUNTIME environment variable because it's compiled into the client bundle.\n \n Use getNextRuntime() instead.\n `);\n }\n\n // throw error if the old name is used as the retrieve key\n for (const [newName, oldNames] of Object.entries(ENV_VAR_RENAME)) {\n if (oldNames.includes(name)) {\n throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);\n }\n }\n\n let value = process.env[name];\n\n // check the key under the old name if the new name is not found\n if (!value && ENV_VAR_RENAME[name] as any) {\n for (const oldName of ENV_VAR_RENAME[name]) {\n value = process.env[oldName];\n if (value) break;\n }\n }\n\n if (!value) {\n if (defaultValue !== undefined) {\n value = defaultValue;\n } else {\n throwErr(`Missing environment variable: ${name}`);\n }\n }\n\n return value;\n}\n\nexport function getEnvBoolean(name: string): boolean {\n const value = getEnvVariable(name, \"false\");\n if (value === \"true\") {\n return true;\n } else if (value === \"false\") {\n return false;\n } else {\n throw new StackAssertionError(`Environment variable ${name} must be either \"true\" or \"false\": found ${JSON.stringify(value)}`);\n }\n}\n\nexport function getNextRuntime() {\n // This variable is compiled into the client bundle, so we can't use getEnvVariable here.\n return process.env.NEXT_RUNTIME || throwErr(\"Missing environment variable: NEXT_RUNTIME\");\n}\n\nexport function getNodeEnvironment() {\n return getEnvVariable(\"NODE_ENV\", \"\");\n}\n"],"mappings":";AAAA,SAAS,qBAAqB,gBAAgB;AAC9C,SAAS,gBAAgB;AAElB,SAAS,gBAAgB;AAC9B,SAAO,OAAO,WAAW,eAAe,OAAO,aAAa,eAAe,OAAO,SAAS,kBAAkB;AAC/G;AAGA,IAAM,iBAA2C;AAAA,EAC/C,2BAA2B,CAAC,kBAAkB,uBAAuB;AACvE;AAKO,SAAS,eAAe,MAAc,cAA2C;AACtF,MAAI,cAAc,GAAG;AACnB,UAAM,IAAI,MAAM;AAAA;AAAA;AAAA;AAAA,KAIf;AAAA,EACH;AACA,MAAI,SAAS,gBAAgB;AAC3B,UAAM,IAAI,MAAM;AAAA;AAAA;AAAA;AAAA,KAIf;AAAA,EACH;AAGA,aAAW,CAAC,SAAS,QAAQ,KAAK,OAAO,QAAQ,cAAc,GAAG;AAChE,QAAI,SAAS,SAAS,IAAI,GAAG;AAC3B,eAAS,wBAAwB,IAAI,wBAAwB,OAAO,yDAAyD;AAAA,IAC/H;AAAA,EACF;AAEA,MAAI,QAAQ,QAAQ,IAAI,IAAI;AAG5B,MAAI,CAAC,SAAS,eAAe,IAAI,GAAU;AACzC,eAAW,WAAW,eAAe,IAAI,GAAG;AAC1C,cAAQ,QAAQ,IAAI,OAAO;AAC3B,UAAI,MAAO;AAAA,IACb;AAAA,EACF;AAEA,MAAI,CAAC,OAAO;AACV,QAAI,iBAAiB,QAAW;AAC9B,cAAQ;AAAA,IACV,OAAO;AACL,eAAS,iCAAiC,IAAI,EAAE;AAAA,IAClD;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,cAAc,MAAuB;AACnD,QAAM,QAAQ,eAAe,MAAM,OAAO;AAC1C,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA,EACT,WAAW,UAAU,SAAS;AAC5B,WAAO;AAAA,EACT,OAAO;AACL,UAAM,IAAI,oBAAoB,wBAAwB,IAAI,4CAA4C,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EAC/H;AACF;AAEO,SAAS,iBAAiB;AAE/B,SAAO,QAAQ,IAAI,gBAAgB,SAAS,4CAA4C;AAC1F;AAEO,SAAS,qBAAqB;AACnC,SAAO,eAAe,YAAY,EAAE;AACtC;","names":[]}
|
|
@@ -2,36 +2,65 @@
|
|
|
2
2
|
import * as esbuild from "esbuild-wasm/lib/browser.js";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { isBrowserLike } from "./env.js";
|
|
5
|
-
import { StackAssertionError, throwErr } from "./errors.js";
|
|
5
|
+
import { captureError, StackAssertionError, throwErr } from "./errors.js";
|
|
6
|
+
import { ignoreUnhandledRejection, runAsynchronously } from "./promises.js";
|
|
6
7
|
import { Result } from "./results.js";
|
|
7
8
|
import { traceSpan, withTraceSpan } from "./telemetry.js";
|
|
8
|
-
|
|
9
|
-
var esbuildInitializePromise = null;
|
|
9
|
+
import { createGlobalAsync } from "./globals.js";
|
|
10
10
|
globalThis.self ??= globalThis;
|
|
11
|
+
if (process.env.NODE_ENV === "development" && typeof process !== "undefined" && typeof process.exit === "function") {
|
|
12
|
+
runAsynchronously(async () => {
|
|
13
|
+
try {
|
|
14
|
+
await initializeEsbuild();
|
|
15
|
+
} catch (e) {
|
|
16
|
+
captureError("initialize-esbuild-in-dev", e);
|
|
17
|
+
globalThis.process?.exit?.(1);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
var esbuildInitializePromise = null;
|
|
11
22
|
function initializeEsbuild() {
|
|
12
|
-
|
|
23
|
+
const esbuildWasmUrl = `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`;
|
|
24
|
+
if (esbuildInitializePromise == null) {
|
|
13
25
|
esbuildInitializePromise = withTraceSpan("initializeEsbuild", async () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
try {
|
|
27
|
+
let initOptions;
|
|
28
|
+
if (isBrowserLike()) {
|
|
29
|
+
initOptions = {
|
|
30
|
+
wasmURL: esbuildWasmUrl
|
|
31
|
+
};
|
|
32
|
+
} else {
|
|
33
|
+
const esbuildWasmModule = await createGlobalAsync("esbuildWasmModule", async () => {
|
|
34
|
+
const esbuildWasmResponse = await fetch(esbuildWasmUrl);
|
|
35
|
+
if (!esbuildWasmResponse.ok) {
|
|
36
|
+
throw new StackAssertionError(`Failed to fetch esbuild.wasm: ${esbuildWasmResponse.status} ${esbuildWasmResponse.statusText}: ${await esbuildWasmResponse.text()}`);
|
|
37
|
+
}
|
|
38
|
+
const esbuildWasm = await esbuildWasmResponse.arrayBuffer();
|
|
39
|
+
const esbuildWasmArray = new Uint8Array(esbuildWasm);
|
|
40
|
+
if (esbuildWasmArray[0] !== 0 || esbuildWasmArray[1] !== 97 || esbuildWasmArray[2] !== 115 || esbuildWasmArray[3] !== 109) {
|
|
41
|
+
throw new StackAssertionError(`Invalid esbuild.wasm file: ${new TextDecoder().decode(esbuildWasmArray)}`);
|
|
42
|
+
}
|
|
43
|
+
return new WebAssembly.Module(esbuildWasm);
|
|
44
|
+
});
|
|
45
|
+
initOptions = {
|
|
46
|
+
wasmModule: esbuildWasmModule,
|
|
47
|
+
worker: false
|
|
48
|
+
};
|
|
22
49
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
try {
|
|
51
|
+
await esbuild.initialize(initOptions);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (e instanceof Error && e.message === 'Cannot call "initialize" more than once') {
|
|
54
|
+
} else {
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
27
57
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
worker: false
|
|
32
|
-
});
|
|
58
|
+
} catch (e) {
|
|
59
|
+
esbuildInitializePromise = null;
|
|
60
|
+
throw new StackAssertionError("Failed to initialize ESBuild", { cause: e });
|
|
33
61
|
}
|
|
34
62
|
})();
|
|
63
|
+
ignoreUnhandledRejection(esbuildInitializePromise);
|
|
35
64
|
}
|
|
36
65
|
return esbuildInitializePromise;
|
|
37
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/esbuild.tsx"],"sourcesContent":["import * as esbuild from 'esbuild-wasm/lib/browser.js';\nimport { join } from 'path';\nimport { isBrowserLike } from './env';\nimport { StackAssertionError, throwErr } from \"./errors\";\nimport { Result } from \"./results\";\nimport { traceSpan, withTraceSpan } from './telemetry';\n\nconst esbuildWasmUrl = `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`;\n\nlet esbuildInitializePromise: Promise<void> | null = null;\n// esbuild requires self property to be set, and it is not set by default in nodejs\n(globalThis.self as any) ??= globalThis as any;\n\nexport function initializeEsbuild(): Promise<void> {\n if (!esbuildInitializePromise) {\n esbuildInitializePromise = withTraceSpan('initializeEsbuild', async () => {\n if (isBrowserLike()) {\n await esbuild.initialize({\n wasmURL: esbuildWasmUrl,\n });\n } else {\n const esbuildWasmResponse = await fetch(esbuildWasmUrl);\n if (!esbuildWasmResponse.ok) {\n throw new StackAssertionError(`Failed to fetch esbuild.wasm: ${esbuildWasmResponse.status} ${esbuildWasmResponse.statusText}: ${await esbuildWasmResponse.text()}`);\n }\n const esbuildWasm = await esbuildWasmResponse.arrayBuffer();\n const esbuildWasmArray = new Uint8Array(esbuildWasm);\n if (esbuildWasmArray[0] !== 0x00 || esbuildWasmArray[1] !== 0x61 || esbuildWasmArray[2] !== 0x73 || esbuildWasmArray[3] !== 0x6d) {\n throw new StackAssertionError(`Invalid esbuild.wasm file: ${new TextDecoder().decode(esbuildWasmArray)}`);\n }\n const esbuildWasmModule = new WebAssembly.Module(esbuildWasm);\n await esbuild.initialize({\n wasmModule: esbuildWasmModule,\n worker: false,\n });\n }\n })();\n }\n\n return esbuildInitializePromise;\n}\n\nexport async function bundleJavaScript(sourceFiles: Record<string, string> & { '/entry.js': string }, options: {\n format?: 'iife' | 'esm' | 'cjs',\n externalPackages?: Record<string, string>,\n keepAsImports?: string[],\n sourcemap?: false | 'inline',\n allowHttpImports?: boolean,\n} = {}): Promise<Result<string, string>> {\n await initializeEsbuild();\n\n const sourceFilesMap = new Map(Object.entries(sourceFiles));\n const externalPackagesMap = new Map(Object.entries(options.externalPackages ?? {}));\n const keepAsImports = options.keepAsImports ?? [];\n\n const httpImportCache = new Map<string, { contents: string, loader: esbuild.Loader, resolveDir: string }>();\n\n const extToLoader: Map<string, esbuild.Loader> = new Map([\n ['tsx', 'tsx'],\n ['ts', 'ts'],\n ['js', 'js'],\n ['jsx', 'jsx'],\n ['json', 'json'],\n ['css', 'css'],\n ]);\n let result;\n try {\n result = await traceSpan('bundleJavaScript', async () => await esbuild.build({\n entryPoints: ['/entry.js'],\n bundle: true,\n write: false,\n format: options.format ?? 'iife',\n platform: 'browser',\n target: 'es2015',\n jsx: 'automatic',\n sourcemap: options.sourcemap ?? 'inline',\n external: keepAsImports,\n plugins: [\n ...options.allowHttpImports ? [{\n name: \"esm-sh-only\",\n setup(build: esbuild.PluginBuild) {\n // Handle absolute URLs and relative imports from esm.sh modules.\n build.onResolve({ filter: /.*/ }, (args) => {\n // Only touch absolute http(s) specifiers or children of our own namespace\n const isHttp = args.path.startsWith(\"http://\") || args.path.startsWith(\"https://\");\n const fromEsmNs = args.namespace === \"esm-sh\";\n\n if (!isHttp && !fromEsmNs) return null; // Let other plugins handle bare/relative/local\n\n // Resolve relative URLs inside esm.sh-fetched modules\n const url = new URL(args.path, fromEsmNs ? args.importer : undefined);\n\n if (url.protocol !== \"https:\" || url.host !== \"esm.sh\") {\n throw new Error(`Blocked non-esm.sh URL import: ${url.href}`);\n }\n\n return { path: url.href, namespace: \"esm-sh\" };\n });\n\n build.onLoad({ filter: /.*/, namespace: \"esm-sh\" }, async (args) => {\n if (httpImportCache.has(args.path)) return httpImportCache.get(args.path)!;\n\n const res = await fetch(args.path, { redirect: \"follow\" });\n if (!res.ok) throw new Error(`Fetch ${res.status} ${res.statusText} for ${args.path}`);\n const finalUrl = new URL(res.url);\n // Defensive: follow shouldn’t leave esm.sh, but re-check.\n if (finalUrl.host !== \"esm.sh\") {\n throw new Error(`Redirect escaped esm.sh: ${finalUrl.href}`);\n }\n\n const ct = (res.headers.get(\"content-type\") || \"\").toLowerCase();\n let loader: esbuild.Loader =\n ct.includes(\"css\") ? \"css\" :\n ct.includes(\"json\") ? \"json\" :\n ct.includes(\"typescript\") ? \"ts\" :\n ct.includes(\"jsx\") ? \"jsx\" :\n ct.includes(\"tsx\") ? \"tsx\" :\n \"js\";\n\n // Fallback by extension (esm.sh sometimes omits CT)\n const p = finalUrl.pathname;\n if (p.endsWith(\".css\")) loader = \"css\";\n else if (p.endsWith(\".json\")) loader = \"json\";\n else if (p.endsWith(\".ts\")) loader = \"ts\";\n else if (p.endsWith(\".tsx\")) loader = \"tsx\";\n else if (p.endsWith(\".jsx\")) loader = \"jsx\";\n\n const contents = await res.text();\n const result = {\n contents,\n loader,\n // Ensures relative imports inside that module resolve against the file’s URL\n resolveDir: new URL(\".\", finalUrl.href).toString(),\n watchFiles: [finalUrl.href],\n };\n httpImportCache.set(args.path, result);\n return result;\n });\n },\n } as esbuild.Plugin] : [],\n {\n name: 'replace-packages-with-globals',\n setup(build) {\n build.onResolve({ filter: /.*/ }, args => {\n // Skip packages that should remain external (not be shimmed)\n if (keepAsImports.includes(args.path)) {\n return undefined;\n }\n if (externalPackagesMap.has(args.path)) {\n return { path: args.path, namespace: 'package-shim' };\n }\n return undefined;\n });\n\n build.onLoad({ filter: /.*/, namespace: 'package-shim' }, (args) => {\n const contents = externalPackagesMap.get(args.path);\n if (contents == null) throw new StackAssertionError(`esbuild requested file ${args.path} that is not in the virtual file system`);\n\n return { contents, loader: 'ts' };\n });\n },\n },\n {\n name: 'virtual-fs',\n setup(build) {\n build.onResolve({ filter: /.*/ }, args => {\n const absolutePath = join(\"/\", args.path);\n if (sourceFilesMap.has(absolutePath)) {\n return { path: absolutePath, namespace: 'virtual' };\n }\n return undefined;\n });\n\n /* 2️⃣ Load the module from the map */\n build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => {\n const contents = sourceFilesMap.get(args.path);\n if (contents == null) throw new StackAssertionError(`esbuild requested file ${args.path} that is not in the virtual file system`);\n\n const ext = args.path.split('.').pop() ?? '';\n const loader = extToLoader.get(ext) ?? throwErr(`esbuild requested file ${args.path} with unknown extension ${ext}`);\n\n return { contents, loader };\n });\n },\n },\n ],\n }));\n } catch (e) {\n if (e instanceof Error && e.message.startsWith(\"Build failed with \")) {\n return Result.error(e.message);\n }\n throw e;\n }\n\n if (result.errors.length > 0) {\n return Result.error(result.errors.map(e => e.text).join('\\n'));\n }\n\n if (result.outputFiles.length > 0) {\n return Result.ok(result.outputFiles[0].text);\n }\n return throwErr(\"No output generated??\");\n}\n"],"mappings":"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","names":["build","result"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/esbuild.tsx"],"sourcesContent":["import * as esbuild from 'esbuild-wasm/lib/browser.js';\nimport { join } from 'path';\nimport { isBrowserLike } from './env';\nimport { captureError, StackAssertionError, throwErr } from \"./errors\";\nimport { ignoreUnhandledRejection, runAsynchronously } from './promises';\nimport { Result } from \"./results\";\nimport { traceSpan, withTraceSpan } from './telemetry';\nimport { createGlobalAsync } from './globals';\n\n\n// esbuild requires self property to be set, and it is not set by default in nodejs\n(globalThis.self as any) ??= globalThis as any;\n\n\nif (process.env.NODE_ENV === 'development' && typeof process !== \"undefined\" && typeof process.exit === \"function\") {\n // On development Node.js servers, initialize ESBuild as soon as the module is imported so we have to wait less on the first request\n runAsynchronously(async () => {\n try {\n await initializeEsbuild();\n } catch (e) {\n captureError(\"initialize-esbuild-in-dev\", e);\n (globalThis as any).process?.exit?.(1);\n }\n });\n}\n\nlet esbuildInitializePromise: Promise<void> | null = null;\n\nexport function initializeEsbuild(): Promise<void> {\n const esbuildWasmUrl = `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`;\n if (esbuildInitializePromise == null) {\n esbuildInitializePromise = withTraceSpan('initializeEsbuild', async () => {\n try {\n let initOptions;\n if (isBrowserLike()) {\n initOptions = {\n wasmURL: esbuildWasmUrl,\n };\n } else {\n const esbuildWasmModule = await createGlobalAsync('esbuildWasmModule', async () => {\n const esbuildWasmResponse = await fetch(esbuildWasmUrl);\n if (!esbuildWasmResponse.ok) {\n throw new StackAssertionError(`Failed to fetch esbuild.wasm: ${esbuildWasmResponse.status} ${esbuildWasmResponse.statusText}: ${await esbuildWasmResponse.text()}`);\n }\n const esbuildWasm = await esbuildWasmResponse.arrayBuffer();\n const esbuildWasmArray = new Uint8Array(esbuildWasm);\n if (esbuildWasmArray[0] !== 0x00 || esbuildWasmArray[1] !== 0x61 || esbuildWasmArray[2] !== 0x73 || esbuildWasmArray[3] !== 0x6d) {\n throw new StackAssertionError(`Invalid esbuild.wasm file: ${new TextDecoder().decode(esbuildWasmArray)}`);\n }\n return new WebAssembly.Module(esbuildWasm);\n });\n initOptions = {\n wasmModule: esbuildWasmModule,\n worker: false,\n };\n }\n try {\n await esbuild.initialize(initOptions);\n } catch (e) {\n if (e instanceof Error && e.message === 'Cannot call \"initialize\" more than once') {\n // this happens especially in local development, just ignore\n } else {\n throw e;\n }\n }\n } catch (e) {\n esbuildInitializePromise = null;\n throw new StackAssertionError(\"Failed to initialize ESBuild\", { cause: e });\n }\n })();\n ignoreUnhandledRejection(esbuildInitializePromise);\n }\n\n return esbuildInitializePromise;\n}\n\nexport async function bundleJavaScript(sourceFiles: Record<string, string> & { '/entry.js': string }, options: {\n format?: 'iife' | 'esm' | 'cjs',\n externalPackages?: Record<string, string>,\n keepAsImports?: string[],\n sourcemap?: false | 'inline',\n allowHttpImports?: boolean,\n} = {}): Promise<Result<string, string>> {\n await initializeEsbuild();\n\n const sourceFilesMap = new Map(Object.entries(sourceFiles));\n const externalPackagesMap = new Map(Object.entries(options.externalPackages ?? {}));\n const keepAsImports = options.keepAsImports ?? [];\n\n const httpImportCache = new Map<string, { contents: string, loader: esbuild.Loader, resolveDir: string }>();\n\n const extToLoader: Map<string, esbuild.Loader> = new Map([\n ['tsx', 'tsx'],\n ['ts', 'ts'],\n ['js', 'js'],\n ['jsx', 'jsx'],\n ['json', 'json'],\n ['css', 'css'],\n ]);\n let result;\n try {\n result = await traceSpan('bundleJavaScript', async () => await esbuild.build({\n entryPoints: ['/entry.js'],\n bundle: true,\n write: false,\n format: options.format ?? 'iife',\n platform: 'browser',\n target: 'es2015',\n jsx: 'automatic',\n sourcemap: options.sourcemap ?? 'inline',\n external: keepAsImports,\n plugins: [\n ...options.allowHttpImports ? [{\n name: \"esm-sh-only\",\n setup(build: esbuild.PluginBuild) {\n // Handle absolute URLs and relative imports from esm.sh modules.\n build.onResolve({ filter: /.*/ }, (args) => {\n // Only touch absolute http(s) specifiers or children of our own namespace\n const isHttp = args.path.startsWith(\"http://\") || args.path.startsWith(\"https://\");\n const fromEsmNs = args.namespace === \"esm-sh\";\n\n if (!isHttp && !fromEsmNs) return null; // Let other plugins handle bare/relative/local\n\n // Resolve relative URLs inside esm.sh-fetched modules\n const url = new URL(args.path, fromEsmNs ? args.importer : undefined);\n\n if (url.protocol !== \"https:\" || url.host !== \"esm.sh\") {\n throw new Error(`Blocked non-esm.sh URL import: ${url.href}`);\n }\n\n return { path: url.href, namespace: \"esm-sh\" };\n });\n\n build.onLoad({ filter: /.*/, namespace: \"esm-sh\" }, async (args) => {\n if (httpImportCache.has(args.path)) return httpImportCache.get(args.path)!;\n\n const res = await fetch(args.path, { redirect: \"follow\" });\n if (!res.ok) throw new Error(`Fetch ${res.status} ${res.statusText} for ${args.path}`);\n const finalUrl = new URL(res.url);\n // Defensive: follow shouldn’t leave esm.sh, but re-check.\n if (finalUrl.host !== \"esm.sh\") {\n throw new Error(`Redirect escaped esm.sh: ${finalUrl.href}`);\n }\n\n const ct = (res.headers.get(\"content-type\") || \"\").toLowerCase();\n let loader: esbuild.Loader =\n ct.includes(\"css\") ? \"css\" :\n ct.includes(\"json\") ? \"json\" :\n ct.includes(\"typescript\") ? \"ts\" :\n ct.includes(\"jsx\") ? \"jsx\" :\n ct.includes(\"tsx\") ? \"tsx\" :\n \"js\";\n\n // Fallback by extension (esm.sh sometimes omits CT)\n const p = finalUrl.pathname;\n if (p.endsWith(\".css\")) loader = \"css\";\n else if (p.endsWith(\".json\")) loader = \"json\";\n else if (p.endsWith(\".ts\")) loader = \"ts\";\n else if (p.endsWith(\".tsx\")) loader = \"tsx\";\n else if (p.endsWith(\".jsx\")) loader = \"jsx\";\n\n const contents = await res.text();\n const result = {\n contents,\n loader,\n // Ensures relative imports inside that module resolve against the file’s URL\n resolveDir: new URL(\".\", finalUrl.href).toString(),\n watchFiles: [finalUrl.href],\n };\n httpImportCache.set(args.path, result);\n return result;\n });\n },\n } as esbuild.Plugin] : [],\n {\n name: 'replace-packages-with-globals',\n setup(build) {\n build.onResolve({ filter: /.*/ }, args => {\n // Skip packages that should remain external (not be shimmed)\n if (keepAsImports.includes(args.path)) {\n return undefined;\n }\n if (externalPackagesMap.has(args.path)) {\n return { path: args.path, namespace: 'package-shim' };\n }\n return undefined;\n });\n\n build.onLoad({ filter: /.*/, namespace: 'package-shim' }, (args) => {\n const contents = externalPackagesMap.get(args.path);\n if (contents == null) throw new StackAssertionError(`esbuild requested file ${args.path} that is not in the virtual file system`);\n\n return { contents, loader: 'ts' };\n });\n },\n },\n {\n name: 'virtual-fs',\n setup(build) {\n build.onResolve({ filter: /.*/ }, args => {\n const absolutePath = join(\"/\", args.path);\n if (sourceFilesMap.has(absolutePath)) {\n return { path: absolutePath, namespace: 'virtual' };\n }\n return undefined;\n });\n\n /* 2️⃣ Load the module from the map */\n build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => {\n const contents = sourceFilesMap.get(args.path);\n if (contents == null) throw new StackAssertionError(`esbuild requested file ${args.path} that is not in the virtual file system`);\n\n const ext = args.path.split('.').pop() ?? '';\n const loader = extToLoader.get(ext) ?? throwErr(`esbuild requested file ${args.path} with unknown extension ${ext}`);\n\n return { contents, loader };\n });\n },\n },\n ],\n }));\n } catch (e) {\n if (e instanceof Error && e.message.startsWith(\"Build failed with \")) {\n return Result.error(e.message);\n }\n throw e;\n }\n\n if (result.errors.length > 0) {\n return Result.error(result.errors.map(e => e.text).join('\\n'));\n }\n\n if (result.outputFiles.length > 0) {\n return Result.ok(result.outputFiles[0].text);\n }\n return throwErr(\"No output generated??\");\n}\n"],"mappings":"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","names":["build","result"]}
|
|
@@ -11,6 +11,17 @@ function createGlobal(key, init) {
|
|
|
11
11
|
}
|
|
12
12
|
return globalVar[stackGlobalsSymbol][key];
|
|
13
13
|
}
|
|
14
|
+
function createGlobalAsync(key, init) {
|
|
15
|
+
let promise = null;
|
|
16
|
+
if (!globalVar[stackGlobalsSymbol][key]) {
|
|
17
|
+
promise = init().catch((e) => {
|
|
18
|
+
delete globalVar[stackGlobalsSymbol][key];
|
|
19
|
+
throw e;
|
|
20
|
+
});
|
|
21
|
+
globalVar[stackGlobalsSymbol][key] = promise;
|
|
22
|
+
}
|
|
23
|
+
return promise ?? globalVar[stackGlobalsSymbol][key];
|
|
24
|
+
}
|
|
14
25
|
function getGlobal(key) {
|
|
15
26
|
return globalVar[stackGlobalsSymbol][key];
|
|
16
27
|
}
|
|
@@ -19,6 +30,7 @@ function setGlobal(key, value) {
|
|
|
19
30
|
}
|
|
20
31
|
export {
|
|
21
32
|
createGlobal,
|
|
33
|
+
createGlobalAsync,
|
|
22
34
|
getGlobal,
|
|
23
35
|
globalVar,
|
|
24
36
|
setGlobal
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/globals.tsx"],"sourcesContent":["const globalVar: any =\n typeof globalThis !== 'undefined' ? globalThis :\n typeof global !== 'undefined' ? global :\n typeof window !== 'undefined' ? window :\n typeof self !== 'undefined' ? self :\n {};\nexport {\n globalVar\n};\n\nif (typeof globalThis === 'undefined') {\n (globalVar as any).globalThis = globalVar;\n}\n\nconst stackGlobalsSymbol = Symbol.for('__stack-globals');\nglobalVar[stackGlobalsSymbol] ??= {};\n\nexport function createGlobal<T>(key: string, init: () => T) {\n if (!globalVar[stackGlobalsSymbol][key]) {\n globalVar[stackGlobalsSymbol][key] = init();\n }\n return globalVar[stackGlobalsSymbol][key] as T;\n}\n\nexport function getGlobal(key: string): any {\n return globalVar[stackGlobalsSymbol][key];\n}\n\nexport function setGlobal(key: string, value: any) {\n globalVar[stackGlobalsSymbol][key] = value;\n}\n"],"mappings":";AAAA,IAAM,YACJ,OAAO,eAAe,cAAc,aAClC,OAAO,WAAW,cAAc,SAC9B,OAAO,WAAW,cAAc,SAC9B,OAAO,SAAS,cAAc,OAC5B,CAAC;AAKX,IAAI,OAAO,eAAe,aAAa;AACrC,EAAC,UAAkB,aAAa;AAClC;AAEA,IAAM,qBAAqB,OAAO,IAAI,iBAAiB;AACvD,UAAU,kBAAkB,MAAM,CAAC;AAE5B,SAAS,aAAgB,KAAa,MAAe;AAC1D,MAAI,CAAC,UAAU,kBAAkB,EAAE,GAAG,GAAG;AACvC,cAAU,kBAAkB,EAAE,GAAG,IAAI,KAAK;AAAA,EAC5C;AACA,SAAO,UAAU,kBAAkB,EAAE,GAAG;AAC1C;AAEO,SAAS,UAAU,KAAkB;AAC1C,SAAO,UAAU,kBAAkB,EAAE,GAAG;AAC1C;AAEO,SAAS,UAAU,KAAa,OAAY;AACjD,YAAU,kBAAkB,EAAE,GAAG,IAAI;AACvC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/globals.tsx"],"sourcesContent":["const globalVar: any =\n typeof globalThis !== 'undefined' ? globalThis :\n typeof global !== 'undefined' ? global :\n typeof window !== 'undefined' ? window :\n typeof self !== 'undefined' ? self :\n {};\nexport {\n globalVar\n};\n\nif (typeof globalThis === 'undefined') {\n (globalVar as any).globalThis = globalVar;\n}\n\nconst stackGlobalsSymbol = Symbol.for('__stack-globals');\nglobalVar[stackGlobalsSymbol] ??= {};\n\nexport function createGlobal<T>(key: string, init: () => T) {\n if (!globalVar[stackGlobalsSymbol][key]) {\n globalVar[stackGlobalsSymbol][key] = init();\n }\n return globalVar[stackGlobalsSymbol][key] as T;\n}\n\n/**\n * Like createGlobal, but if the asynchronous initialization fails, the global will be reset and recomputed on the next\n * invocation.\n */\nexport function createGlobalAsync<T>(key: string, init: () => Promise<T>): Promise<T> {\n let promise: Promise<T> | null = null;\n if (!globalVar[stackGlobalsSymbol][key]) {\n promise = init().catch((e) => {\n delete globalVar[stackGlobalsSymbol][key];\n throw e;\n });\n globalVar[stackGlobalsSymbol][key] = promise;\n }\n return promise ?? globalVar[stackGlobalsSymbol][key] as Promise<T>;\n}\n\nexport function getGlobal(key: string): any {\n return globalVar[stackGlobalsSymbol][key];\n}\n\nexport function setGlobal(key: string, value: any) {\n globalVar[stackGlobalsSymbol][key] = value;\n}\n"],"mappings":";AAAA,IAAM,YACJ,OAAO,eAAe,cAAc,aAClC,OAAO,WAAW,cAAc,SAC9B,OAAO,WAAW,cAAc,SAC9B,OAAO,SAAS,cAAc,OAC5B,CAAC;AAKX,IAAI,OAAO,eAAe,aAAa;AACrC,EAAC,UAAkB,aAAa;AAClC;AAEA,IAAM,qBAAqB,OAAO,IAAI,iBAAiB;AACvD,UAAU,kBAAkB,MAAM,CAAC;AAE5B,SAAS,aAAgB,KAAa,MAAe;AAC1D,MAAI,CAAC,UAAU,kBAAkB,EAAE,GAAG,GAAG;AACvC,cAAU,kBAAkB,EAAE,GAAG,IAAI,KAAK;AAAA,EAC5C;AACA,SAAO,UAAU,kBAAkB,EAAE,GAAG;AAC1C;AAMO,SAAS,kBAAqB,KAAa,MAAoC;AACpF,MAAI,UAA6B;AACjC,MAAI,CAAC,UAAU,kBAAkB,EAAE,GAAG,GAAG;AACvC,cAAU,KAAK,EAAE,MAAM,CAAC,MAAM;AAC5B,aAAO,UAAU,kBAAkB,EAAE,GAAG;AACxC,YAAM;AAAA,IACR,CAAC;AACD,cAAU,kBAAkB,EAAE,GAAG,IAAI;AAAA,EACvC;AACA,SAAO,WAAW,UAAU,kBAAkB,EAAE,GAAG;AACrD;AAEO,SAAS,UAAU,KAAkB;AAC1C,SAAO,UAAU,kBAAkB,EAAE,GAAG;AAC1C;AAEO,SAAS,UAAU,KAAa,OAAY;AACjD,YAAU,kBAAkB,EAAE,GAAG,IAAI;AACvC;","names":[]}
|
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
// src/utils/paginated-lists.tsx
|
|
2
|
-
import { range } from "./arrays.js";
|
|
3
2
|
import { StackAssertionError } from "./errors.js";
|
|
4
3
|
var PaginatedList = class _PaginatedList {
|
|
5
4
|
// Implementations
|
|
5
|
+
/** Returns the cursor pointing to the start of the list (before any items). */
|
|
6
6
|
getFirstCursor() {
|
|
7
7
|
return this._getFirstCursor();
|
|
8
8
|
}
|
|
9
|
+
/** Returns the cursor pointing to the end of the list (after all items). */
|
|
9
10
|
getLastCursor() {
|
|
10
11
|
return this._getLastCursor();
|
|
11
12
|
}
|
|
13
|
+
/** Compares two items according to the given orderBy. Returns negative if a < b, 0 if equal, positive if a > b. */
|
|
12
14
|
compare(orderBy, a, b) {
|
|
13
15
|
return this._compare(orderBy, a, b);
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Fetches items moving forward ('next') or backward ('prev') from the given cursor.
|
|
19
|
+
*
|
|
20
|
+
* Respects `limitPrecision`: 'exact' guarantees the exact limit, 'at-least'/'at-most'/'approximate'
|
|
21
|
+
* allow flexibility for performance. Returns items, boundary flags, and a new cursor.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Get 5 items after the start
|
|
26
|
+
* const result = await list.nextOrPrev('next', { cursor: list.getFirstCursor(), limit: 5, filter: {}, orderBy: 'asc', limitPrecision: 'exact' });
|
|
27
|
+
* // result.items.length === 5 (or less if list has fewer items)
|
|
28
|
+
* // result.isFirst === true (started at first cursor)
|
|
29
|
+
* // result.isLast === true if we got all remaining items
|
|
30
|
+
*
|
|
31
|
+
* // Continue from where we left off
|
|
32
|
+
* const more = await list.nextOrPrev('next', { cursor: result.cursor, limit: 5, ... });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
15
35
|
async nextOrPrev(type, options) {
|
|
16
36
|
let result = [];
|
|
17
37
|
let includesFirst = false;
|
|
18
38
|
let includesLast = false;
|
|
19
39
|
let cursor = options.cursor;
|
|
20
40
|
let limitRemaining = options.limit;
|
|
21
|
-
while (limitRemaining > 0
|
|
41
|
+
while (limitRemaining > 0 && (type !== "next" || !includesLast) && (type !== "prev" || !includesFirst)) {
|
|
22
42
|
const iterationRes = await this._nextOrPrev(type, {
|
|
23
43
|
cursor,
|
|
24
44
|
limit: options.limit,
|
|
@@ -46,21 +66,23 @@ var PaginatedList = class _PaginatedList {
|
|
|
46
66
|
if (type === "next") {
|
|
47
67
|
result = result.slice(0, options.limit);
|
|
48
68
|
includesLast = false;
|
|
49
|
-
if (options.limit > 0) cursor = result[result.length - 1].
|
|
69
|
+
if (options.limit > 0) cursor = result[result.length - 1].nextCursor;
|
|
50
70
|
} else {
|
|
51
71
|
result = result.slice(result.length - options.limit);
|
|
52
72
|
includesFirst = false;
|
|
53
|
-
if (options.limit > 0) cursor = result[0].
|
|
73
|
+
if (options.limit > 0) cursor = result[0].prevCursor;
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
return { items: result, isFirst: includesFirst, isLast: includesLast, cursor };
|
|
57
77
|
}
|
|
78
|
+
/** Fetches items after the given cursor (forward pagination). */
|
|
58
79
|
async next({ after, ...rest }) {
|
|
59
80
|
return await this.nextOrPrev("next", {
|
|
60
81
|
...rest,
|
|
61
82
|
cursor: after
|
|
62
83
|
});
|
|
63
84
|
}
|
|
85
|
+
/** Fetches items before the given cursor (backward pagination). */
|
|
64
86
|
async prev({ before, ...rest }) {
|
|
65
87
|
return await this.nextOrPrev("prev", {
|
|
66
88
|
...rest,
|
|
@@ -68,6 +90,27 @@ var PaginatedList = class _PaginatedList {
|
|
|
68
90
|
});
|
|
69
91
|
}
|
|
70
92
|
// Utility methods below
|
|
93
|
+
/**
|
|
94
|
+
* Transforms this list by mapping each item to zero or more new items.
|
|
95
|
+
*
|
|
96
|
+
* Note that the sort order must be preserved after the operation; the flat-mapped list will not be sorted automatically.
|
|
97
|
+
*
|
|
98
|
+
* @param itemMapper - Maps each item (with its cursor) to an array of new items
|
|
99
|
+
* @param compare - Comparison function for the new item type
|
|
100
|
+
* @param newCursorFromOldCursor/oldCursorFromNewCursor - Cursor conversion functions
|
|
101
|
+
* @param estimateItemsToFetch - Estimates how many source items to fetch for a given limit
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* // Expand orders into line items (1 order -> N line items)
|
|
106
|
+
* const lineItems = ordersList.flatMap({
|
|
107
|
+
* itemMapper: ({ item: order }) => order.lineItems.map((li, i) => ({ item: li, prevCursor: `${order.id}-${i}`, nextCursor: `${order.id}-${i + 1}` })),
|
|
108
|
+
* compare: (_, a, b) => a.createdAt - b.createdAt,
|
|
109
|
+
* estimateItemsToFetch: ({ limit }) => Math.ceil(limit / 3), // avg 3 items per order
|
|
110
|
+
* // ... cursor converters
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
71
114
|
flatMap(options) {
|
|
72
115
|
const that = this;
|
|
73
116
|
class FlatMapPaginatedList extends _PaginatedList {
|
|
@@ -104,10 +147,27 @@ var PaginatedList = class _PaginatedList {
|
|
|
104
147
|
}
|
|
105
148
|
return new FlatMapPaginatedList();
|
|
106
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Transforms each item in the list. Requires a reverse mapper for comparison delegation.
|
|
152
|
+
*
|
|
153
|
+
* @param itemMapper - Transforms each item
|
|
154
|
+
* @param oldItemFromNewItem - Reverse-maps new items back to old items (for comparison)
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* // Convert User objects to UserDTO
|
|
159
|
+
* const userDtos = usersList.map({
|
|
160
|
+
* itemMapper: (user) => ({ id: user.id, displayName: user.name }),
|
|
161
|
+
* oldItemFromNewItem: (dto) => fullUsers.get(dto.id)!, // for comparison
|
|
162
|
+
* oldFilterFromNewFilter: (f) => f,
|
|
163
|
+
* oldOrderByFromNewOrderBy: (o) => o,
|
|
164
|
+
* });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
107
167
|
map(options) {
|
|
108
168
|
return this.flatMap({
|
|
109
169
|
itemMapper: (itemEntry, filter, orderBy) => {
|
|
110
|
-
return [{ item: options.itemMapper(itemEntry.item),
|
|
170
|
+
return [{ item: options.itemMapper(itemEntry.item), prevCursor: itemEntry.prevCursor, nextCursor: itemEntry.nextCursor }];
|
|
111
171
|
},
|
|
112
172
|
compare: (orderBy, a, b) => this.compare(options.oldOrderByFromNewOrderBy(orderBy), options.oldItemFromNewItem(a), options.oldItemFromNewItem(b)),
|
|
113
173
|
newCursorFromOldCursor: (cursor) => cursor,
|
|
@@ -117,6 +177,22 @@ var PaginatedList = class _PaginatedList {
|
|
|
117
177
|
estimateItemsToFetch: (options2) => options2.limit
|
|
118
178
|
});
|
|
119
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Filters items in the list. Requires an estimate function since filtering may reduce output.
|
|
182
|
+
*
|
|
183
|
+
* @param filter - Predicate to include/exclude items
|
|
184
|
+
* @param estimateItemsToFetch - Estimates how many source items to fetch (accounts for filter selectivity)
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* // Filter to only active users
|
|
189
|
+
* const activeUsers = usersList.filter({
|
|
190
|
+
* filter: (user, filterOpts) => user.isActive && user.role === filterOpts.role,
|
|
191
|
+
* oldFilterFromNewFilter: (f) => ({}), // original list has no filter
|
|
192
|
+
* estimateItemsToFetch: ({ limit }) => limit * 2, // expect ~50% active
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
120
196
|
filter(options) {
|
|
121
197
|
return this.flatMap({
|
|
122
198
|
itemMapper: (itemEntry, filter, orderBy) => options.filter(itemEntry.item, filter) ? [itemEntry] : [],
|
|
@@ -128,6 +204,20 @@ var PaginatedList = class _PaginatedList {
|
|
|
128
204
|
estimateItemsToFetch: (o) => options.estimateItemsToFetch(o)
|
|
129
205
|
});
|
|
130
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Adds an additional filter constraint while preserving the original filter type.
|
|
209
|
+
* Shorthand for `filter()` that intersects Filter with AddedFilter.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* // Add a "verified" filter on top of existing filters
|
|
214
|
+
* const verifiedUsers = usersList.addFilter({
|
|
215
|
+
* filter: (user, f) => user.emailVerified,
|
|
216
|
+
* estimateItemsToFetch: ({ limit }) => limit * 2, // ~50% are verified
|
|
217
|
+
* });
|
|
218
|
+
* // verifiedUsers filter type is Filter
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
131
221
|
addFilter(options) {
|
|
132
222
|
return this.filter({
|
|
133
223
|
filter: (item, filter) => options.filter(item, filter),
|
|
@@ -135,6 +225,20 @@ var PaginatedList = class _PaginatedList {
|
|
|
135
225
|
estimateItemsToFetch: (o) => options.estimateItemsToFetch(o)
|
|
136
226
|
});
|
|
137
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Merges multiple paginated lists into one, interleaving items by sort order.
|
|
230
|
+
* All lists must use the same compare function.
|
|
231
|
+
*
|
|
232
|
+
* The merged cursor is a JSON-encoded array of individual list cursors.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* // Merge users from multiple sources into a unified feed
|
|
237
|
+
* const allUsers = PaginatedList.merge(internalUsers, externalUsers, partnerUsers);
|
|
238
|
+
* const page = await allUsers.next({ after: allUsers.getFirstCursor(), limit: 20, ... });
|
|
239
|
+
* // page.items contains interleaved items from all sources, sorted by orderBy
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
138
242
|
static merge(...lists) {
|
|
139
243
|
class MergePaginatedList extends _PaginatedList {
|
|
140
244
|
_getFirstCursor() {
|
|
@@ -146,7 +250,7 @@ var PaginatedList = class _PaginatedList {
|
|
|
146
250
|
_compare(orderBy, a, b) {
|
|
147
251
|
const listsResults = lists.map((list) => list.compare(orderBy, a, b));
|
|
148
252
|
if (!listsResults.every((result) => result === listsResults[0])) {
|
|
149
|
-
throw new StackAssertionError("Lists have different compare results; make sure that they use the same compare function", { lists, listsResults });
|
|
253
|
+
throw new StackAssertionError("Lists have different compare results; make sure that they use the same compare function", { lists, listsResults, orderBy, a, b });
|
|
150
254
|
}
|
|
151
255
|
return listsResults[0];
|
|
152
256
|
}
|
|
@@ -163,20 +267,40 @@ var PaginatedList = class _PaginatedList {
|
|
|
163
267
|
}));
|
|
164
268
|
const combinedItems = fetchedLists.flatMap((list, i) => list.items.map((itemEntry) => ({ itemEntry, listIndex: i })));
|
|
165
269
|
const sortedItems = [...combinedItems].sort((a, b) => this._compare(orderBy, a.itemEntry.item, b.itemEntry.item));
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
270
|
+
const sortedItemsWithMergedCursors = [];
|
|
271
|
+
const curCursors = [...cursors];
|
|
272
|
+
for (const item of type === "next" ? sortedItems : sortedItems.reverse()) {
|
|
273
|
+
const lastCursors = [...curCursors];
|
|
274
|
+
curCursors[item.listIndex] = type === "next" ? item.itemEntry.nextCursor : item.itemEntry.prevCursor;
|
|
275
|
+
sortedItemsWithMergedCursors.push({
|
|
276
|
+
item: item.itemEntry.item,
|
|
277
|
+
prevCursor: type === "next" ? JSON.stringify(lastCursors) : JSON.stringify(curCursors),
|
|
278
|
+
nextCursor: type === "next" ? JSON.stringify(curCursors) : JSON.stringify(lastCursors)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (type === "prev") {
|
|
282
|
+
sortedItemsWithMergedCursors.reverse();
|
|
283
|
+
}
|
|
170
284
|
return {
|
|
171
|
-
items:
|
|
172
|
-
isFirst:
|
|
173
|
-
isLast:
|
|
174
|
-
cursor: JSON.stringify(
|
|
285
|
+
items: sortedItemsWithMergedCursors,
|
|
286
|
+
isFirst: fetchedLists.every((list) => list.isFirst),
|
|
287
|
+
isLast: fetchedLists.every((list) => list.isLast),
|
|
288
|
+
cursor: JSON.stringify(curCursors)
|
|
175
289
|
};
|
|
176
290
|
}
|
|
177
291
|
}
|
|
178
292
|
return new MergePaginatedList();
|
|
179
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Returns an empty paginated list that always returns no items.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```ts
|
|
299
|
+
* const empty = PaginatedList.empty();
|
|
300
|
+
* const result = await empty.next({ after: empty.getFirstCursor(), limit: 10, ... });
|
|
301
|
+
* // result = { items: [], isFirst: true, isLast: true, cursor: "first" }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
180
304
|
static empty() {
|
|
181
305
|
class EmptyPaginatedList extends _PaginatedList {
|
|
182
306
|
_getFirstCursor() {
|
|
@@ -201,10 +325,10 @@ var ArrayPaginatedList = class extends PaginatedList {
|
|
|
201
325
|
this.array = array;
|
|
202
326
|
}
|
|
203
327
|
_getFirstCursor() {
|
|
204
|
-
return "0";
|
|
328
|
+
return "before-0";
|
|
205
329
|
}
|
|
206
330
|
_getLastCursor() {
|
|
207
|
-
return
|
|
331
|
+
return `before-${this.array.length}`;
|
|
208
332
|
}
|
|
209
333
|
_compare(orderBy, a, b) {
|
|
210
334
|
return orderBy(a, b);
|
|
@@ -212,14 +336,20 @@ var ArrayPaginatedList = class extends PaginatedList {
|
|
|
212
336
|
async _nextOrPrev(type, options) {
|
|
213
337
|
const filteredArray = this.array.filter(options.filter);
|
|
214
338
|
const sortedArray = [...filteredArray].sort((a, b) => this._compare(options.orderBy, a, b));
|
|
215
|
-
const itemEntriesArray = sortedArray.map((item, index) => ({
|
|
216
|
-
|
|
217
|
-
|
|
339
|
+
const itemEntriesArray = sortedArray.map((item, index) => ({
|
|
340
|
+
item,
|
|
341
|
+
prevCursor: `before-${index}`,
|
|
342
|
+
nextCursor: `before-${index + 1}`
|
|
343
|
+
}));
|
|
344
|
+
const oldCursor = Number(options.cursor.replace("before-", ""));
|
|
345
|
+
const clampedOldCursor = Math.max(0, Math.min(sortedArray.length, oldCursor));
|
|
346
|
+
const newCursor = Math.max(0, Math.min(sortedArray.length, clampedOldCursor + (type === "next" ? 1 : -1) * options.limit));
|
|
347
|
+
const slicedItemEntriesArray = itemEntriesArray.slice(Math.min(clampedOldCursor, newCursor), Math.max(clampedOldCursor, newCursor));
|
|
218
348
|
return {
|
|
219
|
-
items:
|
|
220
|
-
isFirst:
|
|
221
|
-
isLast:
|
|
222
|
-
cursor:
|
|
349
|
+
items: slicedItemEntriesArray,
|
|
350
|
+
isFirst: clampedOldCursor === 0 || newCursor === 0,
|
|
351
|
+
isLast: clampedOldCursor === sortedArray.length || newCursor === sortedArray.length,
|
|
352
|
+
cursor: `before-${newCursor}`
|
|
223
353
|
};
|
|
224
354
|
}
|
|
225
355
|
};
|