@stackframe/stack-shared 2.8.48 → 2.8.51

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/config/schema.d.mts +8 -52
  3. package/dist/config/schema.d.ts +8 -52
  4. package/dist/config/schema.js +10 -0
  5. package/dist/config/schema.js.map +1 -1
  6. package/dist/esm/config/schema.js +10 -0
  7. package/dist/esm/config/schema.js.map +1 -1
  8. package/dist/esm/interface/admin-interface.js +10 -0
  9. package/dist/esm/interface/admin-interface.js.map +1 -1
  10. package/dist/esm/interface/client-interface.js +11 -7
  11. package/dist/esm/interface/client-interface.js.map +1 -1
  12. package/dist/esm/interface/crud/projects.js +2 -1
  13. package/dist/esm/interface/crud/projects.js.map +1 -1
  14. package/dist/esm/interface/server-interface.js +11 -0
  15. package/dist/esm/interface/server-interface.js.map +1 -1
  16. package/dist/esm/known-errors.js +1 -1
  17. package/dist/esm/known-errors.js.map +1 -1
  18. package/dist/esm/utils/caches.js +31 -16
  19. package/dist/esm/utils/caches.js.map +1 -1
  20. package/dist/esm/utils/ips.js.map +1 -1
  21. package/dist/esm/utils/react.js +49 -2
  22. package/dist/esm/utils/react.js.map +1 -1
  23. package/dist/esm/utils/stores.js +1 -0
  24. package/dist/esm/utils/stores.js.map +1 -1
  25. package/dist/esm/utils/urls.js.map +1 -1
  26. package/dist/interface/admin-interface.d.mts +6 -0
  27. package/dist/interface/admin-interface.d.ts +6 -0
  28. package/dist/interface/admin-interface.js +10 -0
  29. package/dist/interface/admin-interface.js.map +1 -1
  30. package/dist/interface/client-interface.d.mts +2 -1
  31. package/dist/interface/client-interface.d.ts +2 -1
  32. package/dist/interface/client-interface.js +11 -7
  33. package/dist/interface/client-interface.js.map +1 -1
  34. package/dist/interface/crud/projects.d.mts +10 -0
  35. package/dist/interface/crud/projects.d.ts +10 -0
  36. package/dist/interface/crud/projects.js +2 -1
  37. package/dist/interface/crud/projects.js.map +1 -1
  38. package/dist/interface/server-interface.d.mts +4 -0
  39. package/dist/interface/server-interface.d.ts +4 -0
  40. package/dist/interface/server-interface.js +11 -0
  41. package/dist/interface/server-interface.js.map +1 -1
  42. package/dist/known-errors.d.mts +1 -1
  43. package/dist/known-errors.d.ts +1 -1
  44. package/dist/known-errors.js +1 -1
  45. package/dist/known-errors.js.map +1 -1
  46. package/dist/sessions.d.mts +1 -1
  47. package/dist/sessions.d.ts +1 -1
  48. package/dist/utils/caches.d.mts +10 -9
  49. package/dist/utils/caches.d.ts +10 -9
  50. package/dist/utils/caches.js +31 -16
  51. package/dist/utils/caches.js.map +1 -1
  52. package/dist/utils/ips.d.mts +1 -1
  53. package/dist/utils/ips.d.ts +1 -1
  54. package/dist/utils/ips.js.map +1 -1
  55. package/dist/utils/react.d.mts +3 -1
  56. package/dist/utils/react.d.ts +3 -1
  57. package/dist/utils/react.js +50 -1
  58. package/dist/utils/react.js.map +1 -1
  59. package/dist/utils/stores.js +1 -0
  60. package/dist/utils/stores.js.map +1 -1
  61. package/dist/utils/urls.js.map +1 -1
  62. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  // src/utils/caches.tsx
2
+ import { isBrowserLike } from "./env.js";
2
3
  import { DependenciesMap } from "./maps.js";
3
4
  import { filterUndefined } from "./objects.js";
4
5
  import { pending, rateLimited, resolved, runAsynchronously, wait } from "./promises.js";
@@ -58,6 +59,15 @@ var AsyncCache = class {
58
59
  }
59
60
  await Promise.all(promises);
60
61
  }
62
+ async invalidateWhere(predicate) {
63
+ const promises = [];
64
+ for (const [dependencies, cache] of this._map) {
65
+ if (predicate(dependencies)) {
66
+ promises.push(cache.invalidate().catch(() => void 0));
67
+ }
68
+ }
69
+ await Promise.all(promises);
70
+ }
61
71
  };
62
72
  var AsyncValueCache = class {
63
73
  constructor(fetcher, _options = {}) {
@@ -93,9 +103,12 @@ var AsyncValueCache = class {
93
103
  this._store.set(value);
94
104
  }
95
105
  _setAsync(value) {
106
+ if (this._subscriptionsCount === 0 && !isBrowserLike()) {
107
+ this._invalidateCacheSoon();
108
+ }
96
109
  const promise = pending(value);
97
110
  this._pendingPromise = promise;
98
- return pending(this._store.setAsync(promise));
111
+ return pending(this._store.setAsync(promise).then(() => void 0));
99
112
  }
100
113
  _refetch(cacheStrategy) {
101
114
  if (cacheStrategy === "read-write" && this._pendingPromise) {
@@ -114,25 +127,33 @@ var AsyncValueCache = class {
114
127
  return this._setAsync(value);
115
128
  }
116
129
  /**
117
- * Refetches the value from the fetcher, and updates the cache with it.
130
+ * If anyone is listening to the cache, refreshes the value, and sets it without invalidating the cache.
118
131
  */
119
132
  async refresh() {
120
- return await this.getOrWait("write-only");
133
+ if (this._subscriptionsCount > 0) {
134
+ await this.getOrWait("write-only");
135
+ }
121
136
  }
122
137
  /**
123
- * Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone was listening to it,
124
- * it will refresh immediately.
138
+ * Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone is listening to the cache, it will refresh immediately.
125
139
  */
126
- invalidate() {
140
+ async invalidate() {
127
141
  this._store.setUnavailable();
128
142
  this._pendingPromise = void 0;
129
- if (this._subscriptionsCount > 0) {
130
- runAsynchronously(this.refresh());
131
- }
143
+ await this.refresh();
132
144
  }
133
145
  isDirty() {
134
146
  return this._pendingPromise === void 0;
135
147
  }
148
+ _invalidateCacheSoon() {
149
+ const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
150
+ runAsynchronously(async () => {
151
+ await wait(5e3);
152
+ if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
153
+ await this.invalidate();
154
+ }
155
+ });
156
+ }
136
157
  onStateChange(callback) {
137
158
  const storeObj = this._store.onChange(callback);
138
159
  runAsynchronously(this.getOrWait("read-write"));
@@ -149,13 +170,7 @@ var AsyncValueCache = class {
149
170
  hasUnsubscribed = true;
150
171
  storeObj.unsubscribe();
151
172
  if (--this._subscriptionsCount === 0) {
152
- const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
153
- runAsynchronously(async () => {
154
- await wait(5e3);
155
- if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
156
- this.invalidate();
157
- }
158
- });
173
+ this._invalidateCacheSoon();
159
174
  for (const unsubscribe of this._unsubscribers) {
160
175
  unsubscribe();
161
176
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/caches.tsx"],"sourcesContent":["import { DependenciesMap } from \"./maps\";\nimport { filterUndefined } from \"./objects\";\nimport { RateLimitOptions, ReactPromise, pending, rateLimited, resolved, runAsynchronously, wait } from \"./promises\";\nimport { AsyncStore } from \"./stores\";\n\n/**\n * Can be used to cache the result of a function call, for example for the `use` hook in React.\n */\nexport function cacheFunction<F extends Function>(f: F): F {\n const dependenciesMap = new DependenciesMap<any, any>();\n\n return ((...args: any) => {\n if (dependenciesMap.has(args)) {\n return dependenciesMap.get(args);\n }\n\n const value = f(...args);\n dependenciesMap.set(args, value);\n return value;\n }) as any as F;\n}\nundefined?.test(\"cacheFunction\", ({ expect }) => {\n // Test with a simple function\n let callCount = 0;\n const add = (a: number, b: number) => {\n callCount++;\n return a + b;\n };\n\n const cachedAdd = cacheFunction(add);\n\n // First call should execute the function\n expect(cachedAdd(1, 2)).toBe(3);\n expect(callCount).toBe(1);\n\n // Second call with same args should use cached result\n expect(cachedAdd(1, 2)).toBe(3);\n expect(callCount).toBe(1);\n\n // Call with different args should execute the function again\n expect(cachedAdd(2, 3)).toBe(5);\n expect(callCount).toBe(2);\n\n // Test with a function that returns objects\n let objectCallCount = 0;\n const createObject = (id: number) => {\n objectCallCount++;\n return { id };\n };\n\n const cachedCreateObject = cacheFunction(createObject);\n\n // First call should execute the function\n const obj1 = cachedCreateObject(1);\n expect(obj1).toEqual({ id: 1 });\n expect(objectCallCount).toBe(1);\n\n // Second call with same args should use cached result\n const obj2 = cachedCreateObject(1);\n expect(obj2).toBe(obj1); // Same reference\n expect(objectCallCount).toBe(1);\n});\n\n\ntype CacheStrategy = \"write-only\" | \"read-write\" | \"never\";\n\nexport class AsyncCache<D extends any[], T> {\n private readonly _map = new DependenciesMap<D, AsyncValueCache<T>>();\n\n constructor(\n private readonly _fetcher: (dependencies: D) => Promise<T>,\n private readonly _options: {\n onSubscribe?: (key: D, refresh: () => void) => (() => void),\n rateLimiter?: Omit<RateLimitOptions, \"batchCalls\">,\n } = {},\n ) {\n // nothing here yet\n }\n\n private _createKeyed<FunctionName extends keyof AsyncValueCache<T>>(\n functionName: FunctionName,\n ): (key: D, ...args: Parameters<AsyncValueCache<T>[FunctionName]>) => ReturnType<AsyncValueCache<T>[FunctionName]> {\n return (key: D, ...args) => {\n const valueCache = this.getValueCache(key);\n return (valueCache[functionName] as any).apply(valueCache, args);\n };\n }\n\n getValueCache(dependencies: D): AsyncValueCache<T> {\n let cache = this._map.get(dependencies);\n if (!cache) {\n cache = new AsyncValueCache(\n async () => await this._fetcher(dependencies),\n {\n ...this._options,\n onSubscribe: this._options.onSubscribe ? (cb) => this._options.onSubscribe!(dependencies, cb) : undefined,\n },\n );\n this._map.set(dependencies, cache);\n }\n return cache;\n }\n\n async refreshWhere(predicate: (dependencies: D) => boolean) {\n const promises: Promise<T>[] = [];\n for (const [dependencies, cache] of this._map) {\n if (predicate(dependencies)) {\n promises.push(cache.refresh());\n }\n }\n await Promise.all(promises);\n }\n\n readonly isCacheAvailable = this._createKeyed(\"isCacheAvailable\");\n readonly getIfCached = this._createKeyed(\"getIfCached\");\n readonly getOrWait = this._createKeyed(\"getOrWait\");\n readonly forceSetCachedValue = this._createKeyed(\"forceSetCachedValue\");\n readonly forceSetCachedValueAsync = this._createKeyed(\"forceSetCachedValueAsync\");\n readonly refresh = this._createKeyed(\"refresh\");\n readonly invalidate = this._createKeyed(\"invalidate\");\n readonly onStateChange = this._createKeyed(\"onStateChange\");\n readonly isDirty = this._createKeyed(\"isDirty\");\n}\n\nclass AsyncValueCache<T> {\n private _store: AsyncStore<T>;\n private _pendingPromise: ReactPromise<T> | undefined;\n private _fetcher: () => Promise<T>;\n private readonly _rateLimitOptions: Omit<RateLimitOptions, \"batchCalls\">;\n private _subscriptionsCount = 0;\n private _unsubscribers: (() => void)[] = [];\n private _mostRecentRefreshPromiseIndex = 0;\n\n constructor(\n fetcher: () => Promise<T>,\n private readonly _options: {\n onSubscribe?: (refresh: () => void) => (() => void),\n rateLimiter?: Omit<RateLimitOptions, \"batchCalls\">,\n } = {},\n ) {\n this._store = new AsyncStore();\n this._rateLimitOptions = {\n concurrency: 1,\n throttleMs: 300,\n ...filterUndefined(_options.rateLimiter ?? {}),\n };\n\n\n this._fetcher = rateLimited(fetcher, {\n ...this._rateLimitOptions,\n batchCalls: true,\n });\n }\n\n isCacheAvailable(): boolean {\n return this._store.isAvailable();\n }\n\n getIfCached() {\n return this._store.get();\n }\n\n getOrWait(cacheStrategy: CacheStrategy): ReactPromise<T> {\n const cached = this.getIfCached();\n if (cacheStrategy === \"read-write\" && cached.status === \"ok\") {\n return resolved(cached.data);\n }\n\n return this._refetch(cacheStrategy);\n }\n\n private _set(value: T): void {\n this._store.set(value);\n }\n\n private _setAsync(value: Promise<T>): ReactPromise<boolean> {\n const promise = pending(value);\n this._pendingPromise = promise;\n return pending(this._store.setAsync(promise));\n }\n\n private _refetch(cacheStrategy: CacheStrategy): ReactPromise<T> {\n if (cacheStrategy === \"read-write\" && this._pendingPromise) {\n return this._pendingPromise;\n }\n const promise = pending(this._fetcher());\n if (cacheStrategy === \"never\") {\n return promise;\n }\n return pending(this._setAsync(promise).then(() => promise));\n }\n\n forceSetCachedValue(value: T): void {\n this._set(value);\n }\n\n forceSetCachedValueAsync(value: Promise<T>): ReactPromise<boolean> {\n return this._setAsync(value);\n }\n\n /**\n * Refetches the value from the fetcher, and updates the cache with it.\n */\n async refresh(): Promise<T> {\n return await this.getOrWait(\"write-only\");\n }\n\n /**\n * Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone was listening to it,\n * it will refresh immediately.\n */\n invalidate(): void {\n this._store.setUnavailable();\n this._pendingPromise = undefined;\n if (this._subscriptionsCount > 0) {\n runAsynchronously(this.refresh());\n }\n }\n\n isDirty(): boolean {\n return this._pendingPromise === undefined;\n }\n\n onStateChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const storeObj = this._store.onChange(callback);\n\n runAsynchronously(this.getOrWait(\"read-write\"));\n\n if (this._subscriptionsCount++ === 0 && this._options.onSubscribe) {\n const unsubscribe = this._options.onSubscribe(() => {\n runAsynchronously(this.refresh());\n });\n this._unsubscribers.push(unsubscribe);\n }\n\n let hasUnsubscribed = false;\n return {\n unsubscribe: () => {\n if (hasUnsubscribed) return;\n hasUnsubscribed = true;\n storeObj.unsubscribe();\n if (--this._subscriptionsCount === 0) {\n const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;\n runAsynchronously(async () => {\n // wait a few seconds; if anything changes during that time, we don't want to refresh\n // else we do unnecessary requests if we unsubscribe and then subscribe again immediately\n await wait(5000);\n if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {\n this.invalidate();\n }\n });\n\n for (const unsubscribe of this._unsubscribers) {\n unsubscribe();\n }\n }\n },\n };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/utils/caches.tsx"],"sourcesContent":["import { isBrowserLike } from \"./env\";\nimport { DependenciesMap } from \"./maps\";\nimport { filterUndefined } from \"./objects\";\nimport { RateLimitOptions, ReactPromise, pending, rateLimited, resolved, runAsynchronously, wait } from \"./promises\";\nimport { AsyncStore } from \"./stores\";\n\n/**\n * Can be used to cache the result of a function call, for example for the `use` hook in React.\n */\nexport function cacheFunction<F extends Function>(f: F): F {\n const dependenciesMap = new DependenciesMap<any, any>();\n\n return ((...args: any) => {\n if (dependenciesMap.has(args)) {\n return dependenciesMap.get(args);\n }\n\n const value = f(...args);\n dependenciesMap.set(args, value);\n return value;\n }) as any as F;\n}\nundefined?.test(\"cacheFunction\", ({ expect }) => {\n // Test with a simple function\n let callCount = 0;\n const add = (a: number, b: number) => {\n callCount++;\n return a + b;\n };\n\n const cachedAdd = cacheFunction(add);\n\n // First call should execute the function\n expect(cachedAdd(1, 2)).toBe(3);\n expect(callCount).toBe(1);\n\n // Second call with same args should use cached result\n expect(cachedAdd(1, 2)).toBe(3);\n expect(callCount).toBe(1);\n\n // Call with different args should execute the function again\n expect(cachedAdd(2, 3)).toBe(5);\n expect(callCount).toBe(2);\n\n // Test with a function that returns objects\n let objectCallCount = 0;\n const createObject = (id: number) => {\n objectCallCount++;\n return { id };\n };\n\n const cachedCreateObject = cacheFunction(createObject);\n\n // First call should execute the function\n const obj1 = cachedCreateObject(1);\n expect(obj1).toEqual({ id: 1 });\n expect(objectCallCount).toBe(1);\n\n // Second call with same args should use cached result\n const obj2 = cachedCreateObject(1);\n expect(obj2).toBe(obj1); // Same reference\n expect(objectCallCount).toBe(1);\n});\n\n\ntype CacheStrategy = \"write-only\" | \"read-write\" | \"never\";\n\nexport class AsyncCache<D extends any[], T> {\n private readonly _map = new DependenciesMap<D, AsyncValueCache<T>>();\n\n constructor(\n private readonly _fetcher: (dependencies: D) => Promise<T>,\n private readonly _options: {\n onSubscribe?: (key: D, refresh: () => void) => (() => void),\n rateLimiter?: Omit<RateLimitOptions, \"batchCalls\">,\n } = {},\n ) {\n // nothing here yet\n }\n\n private _createKeyed<FunctionName extends keyof AsyncValueCache<T>>(\n functionName: FunctionName,\n ): (key: D, ...args: Parameters<AsyncValueCache<T>[FunctionName]>) => ReturnType<AsyncValueCache<T>[FunctionName]> {\n return (key: D, ...args) => {\n const valueCache = this.getValueCache(key);\n return (valueCache[functionName] as any).apply(valueCache, args);\n };\n }\n\n getValueCache(dependencies: D): AsyncValueCache<T> {\n let cache = this._map.get(dependencies);\n if (!cache) {\n cache = new AsyncValueCache(\n async () => await this._fetcher(dependencies),\n {\n ...this._options,\n onSubscribe: this._options.onSubscribe ? (cb) => this._options.onSubscribe!(dependencies, cb) : undefined,\n },\n );\n this._map.set(dependencies, cache);\n }\n return cache;\n }\n\n async refreshWhere(predicate: (dependencies: D) => boolean) {\n const promises: Promise<void>[] = [];\n for (const [dependencies, cache] of this._map) {\n if (predicate(dependencies)) {\n promises.push(cache.refresh());\n }\n }\n await Promise.all(promises);\n }\n\n async invalidateWhere(predicate: (dependencies: D) => boolean) {\n const promises: Promise<void>[] = [];\n for (const [dependencies, cache] of this._map) {\n if (predicate(dependencies)) {\n promises.push(cache.invalidate().catch(() => undefined));\n }\n }\n await Promise.all(promises);\n }\n\n readonly isCacheAvailable = this._createKeyed(\"isCacheAvailable\");\n readonly getIfCached = this._createKeyed(\"getIfCached\");\n readonly getOrWait = this._createKeyed(\"getOrWait\");\n readonly forceSetCachedValue = this._createKeyed(\"forceSetCachedValue\");\n readonly forceSetCachedValueAsync = this._createKeyed(\"forceSetCachedValueAsync\");\n readonly refresh = this._createKeyed(\"refresh\");\n readonly invalidate = this._createKeyed(\"invalidate\");\n readonly onStateChange = this._createKeyed(\"onStateChange\");\n readonly isDirty = this._createKeyed(\"isDirty\");\n}\n\nclass AsyncValueCache<T> {\n private _store: AsyncStore<T>;\n private _pendingPromise: ReactPromise<T> | undefined;\n private _fetcher: () => Promise<T>;\n private readonly _rateLimitOptions: Omit<RateLimitOptions, \"batchCalls\">;\n private _subscriptionsCount = 0;\n private _unsubscribers: (() => void)[] = [];\n private _mostRecentRefreshPromiseIndex = 0;\n\n constructor(\n fetcher: () => Promise<T>,\n private readonly _options: {\n onSubscribe?: (refresh: () => void) => (() => void),\n rateLimiter?: Omit<RateLimitOptions, \"batchCalls\">,\n } = {},\n ) {\n this._store = new AsyncStore();\n this._rateLimitOptions = {\n concurrency: 1,\n throttleMs: 300,\n ...filterUndefined(_options.rateLimiter ?? {}),\n };\n\n\n this._fetcher = rateLimited(fetcher, {\n ...this._rateLimitOptions,\n batchCalls: true,\n });\n }\n\n isCacheAvailable(): boolean {\n return this._store.isAvailable();\n }\n\n getIfCached() {\n return this._store.get();\n }\n\n getOrWait(cacheStrategy: CacheStrategy): ReactPromise<T> {\n const cached = this.getIfCached();\n if (cacheStrategy === \"read-write\" && cached.status === \"ok\") {\n return resolved(cached.data);\n }\n\n return this._refetch(cacheStrategy);\n }\n\n private _set(value: T): void {\n this._store.set(value);\n }\n\n private _setAsync(value: Promise<T>): ReactPromise<void> {\n if (this._subscriptionsCount === 0 && !isBrowserLike()) {\n // if we're in a server-like environment, we'd rather cache less aggressively to avoid memory leaks.\n // hence, if no one is listening to this cache, let's invalidate it\n this._invalidateCacheSoon();\n }\n const promise = pending(value);\n this._pendingPromise = promise;\n return pending(this._store.setAsync(promise).then(() => undefined));\n }\n\n private _refetch(cacheStrategy: CacheStrategy): ReactPromise<T> {\n if (cacheStrategy === \"read-write\" && this._pendingPromise) {\n return this._pendingPromise;\n }\n const promise = pending(this._fetcher());\n if (cacheStrategy === \"never\") {\n return promise;\n }\n return pending(this._setAsync(promise).then(() => promise));\n }\n\n forceSetCachedValue(value: T): void {\n this._set(value);\n }\n\n forceSetCachedValueAsync(value: Promise<T>): ReactPromise<void> {\n return this._setAsync(value);\n }\n\n /**\n * If anyone is listening to the cache, refreshes the value, and sets it without invalidating the cache.\n */\n async refresh(): Promise<void> {\n // note that we do the extra check here to save a request if no one is listening to the cache anyway\n if (this._subscriptionsCount > 0) {\n await this.getOrWait(\"write-only\");\n }\n }\n\n /**\n * Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone is listening to the cache, it will refresh immediately.\n */\n async invalidate(): Promise<void> {\n this._store.setUnavailable();\n this._pendingPromise = undefined;\n await this.refresh();\n }\n\n isDirty(): boolean {\n return this._pendingPromise === undefined;\n }\n\n _invalidateCacheSoon(): void {\n // wait a few seconds; we want to keep the cache up during this time\n // else we do unnecessary requests if we unsubscribe and then subscribe again immediately\n const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;\n runAsynchronously(async () => {\n await wait(5000);\n if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {\n await this.invalidate();\n }\n });\n }\n\n onStateChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const storeObj = this._store.onChange(callback);\n\n runAsynchronously(this.getOrWait(\"read-write\"));\n\n if (this._subscriptionsCount++ === 0 && this._options.onSubscribe) {\n const unsubscribe = this._options.onSubscribe(() => {\n runAsynchronously(this.refresh());\n });\n this._unsubscribers.push(unsubscribe);\n }\n\n let hasUnsubscribed = false;\n return {\n unsubscribe: () => {\n if (hasUnsubscribed) return;\n hasUnsubscribed = true;\n storeObj.unsubscribe();\n if (--this._subscriptionsCount === 0) {\n this._invalidateCacheSoon();\n\n for (const unsubscribe of this._unsubscribers) {\n unsubscribe();\n }\n }\n },\n };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/ips.tsx"],"sourcesContent":["import ipRegex from \"ip-regex\";\n\nexport type Ipv4Address = `${number}.${number}.${number}.${number}`;\nexport type Ipv6Address = string;\n\nexport function isIpAddress(ip: string): ip is Ipv4Address | Ipv6Address {\n return ipRegex({ exact: true }).test(ip);\n}\nundefined?.test(\"isIpAddress\", ({ expect }) => {\n // Test valid IPv4 addresses\n expect(isIpAddress(\"192.168.1.1\")).toBe(true);\n expect(isIpAddress(\"127.0.0.1\")).toBe(true);\n expect(isIpAddress(\"0.0.0.0\")).toBe(true);\n expect(isIpAddress(\"255.255.255.255\")).toBe(true);\n\n // Test valid IPv6 addresses\n expect(isIpAddress(\"::1\")).toBe(true);\n expect(isIpAddress(\"2001:db8::\")).toBe(true);\n expect(isIpAddress(\"2001:db8:85a3:8d3:1319:8a2e:370:7348\")).toBe(true);\n\n // Test invalid IP addresses\n expect(isIpAddress(\"\")).toBe(false);\n expect(isIpAddress(\"not an ip\")).toBe(false);\n expect(isIpAddress(\"256.256.256.256\")).toBe(false);\n expect(isIpAddress(\"192.168.1\")).toBe(false);\n expect(isIpAddress(\"192.168.1.1.1\")).toBe(false);\n expect(isIpAddress(\"2001:db8::xyz\")).toBe(false);\n});\n\nexport function assertIpAddress(ip: string): asserts ip is Ipv4Address | Ipv6Address {\n if (!isIpAddress(ip)) {\n throw new Error(`Invalid IP address: ${ip}`);\n }\n}\nundefined?.test(\"assertIpAddress\", ({ expect }) => {\n // Test with valid IPv4 address\n expect(() => assertIpAddress(\"192.168.1.1\")).not.toThrow();\n\n // Test with valid IPv6 address\n expect(() => assertIpAddress(\"::1\")).not.toThrow();\n\n // Test with invalid IP addresses\n expect(() => assertIpAddress(\"\")).toThrow(\"Invalid IP address: \");\n expect(() => assertIpAddress(\"not an ip\")).toThrow(\"Invalid IP address: not an ip\");\n expect(() => assertIpAddress(\"256.256.256.256\")).toThrow(\"Invalid IP address: 256.256.256.256\");\n expect(() => assertIpAddress(\"192.168.1\")).toThrow(\"Invalid IP address: 192.168.1\");\n});\n"],"mappings":";AAAA,OAAO,aAAa;AAKb,SAAS,YAAY,IAA6C;AACvE,SAAO,QAAQ,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,EAAE;AACzC;AAsBO,SAAS,gBAAgB,IAAqD;AACnF,MAAI,CAAC,YAAY,EAAE,GAAG;AACpB,UAAM,IAAI,MAAM,uBAAuB,EAAE,EAAE;AAAA,EAC7C;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/utils/ips.tsx"],"sourcesContent":["import ipRegex from \"ip-regex\";\n\nexport type Ipv4Address = `${number}.${number}.${number}.${number}`;\nexport type Ipv6Address = string;\n\nexport function isIpAddress(ip: string) {\n return ipRegex({ exact: true }).test(ip);\n}\nundefined?.test(\"isIpAddress\", ({ expect }) => {\n // Test valid IPv4 addresses\n expect(isIpAddress(\"192.168.1.1\")).toBe(true);\n expect(isIpAddress(\"127.0.0.1\")).toBe(true);\n expect(isIpAddress(\"0.0.0.0\")).toBe(true);\n expect(isIpAddress(\"255.255.255.255\")).toBe(true);\n\n // Test valid IPv6 addresses\n expect(isIpAddress(\"::1\")).toBe(true);\n expect(isIpAddress(\"2001:db8::\")).toBe(true);\n expect(isIpAddress(\"2001:db8:85a3:8d3:1319:8a2e:370:7348\")).toBe(true);\n\n // Test invalid IP addresses\n expect(isIpAddress(\"\")).toBe(false);\n expect(isIpAddress(\"not an ip\")).toBe(false);\n expect(isIpAddress(\"256.256.256.256\")).toBe(false);\n expect(isIpAddress(\"192.168.1\")).toBe(false);\n expect(isIpAddress(\"192.168.1.1.1\")).toBe(false);\n expect(isIpAddress(\"2001:db8::xyz\")).toBe(false);\n});\n\nexport function assertIpAddress(ip: string): asserts ip is Ipv4Address | Ipv6Address {\n if (!isIpAddress(ip)) {\n throw new Error(`Invalid IP address: ${ip}`);\n }\n}\nundefined?.test(\"assertIpAddress\", ({ expect }) => {\n // Test with valid IPv4 address\n expect(() => assertIpAddress(\"192.168.1.1\")).not.toThrow();\n\n // Test with valid IPv6 address\n expect(() => assertIpAddress(\"::1\")).not.toThrow();\n\n // Test with invalid IP addresses\n expect(() => assertIpAddress(\"\")).toThrow(\"Invalid IP address: \");\n expect(() => assertIpAddress(\"not an ip\")).toThrow(\"Invalid IP address: not an ip\");\n expect(() => assertIpAddress(\"256.256.256.256\")).toThrow(\"Invalid IP address: 256.256.256.256\");\n expect(() => assertIpAddress(\"192.168.1\")).toThrow(\"Invalid IP address: 192.168.1\");\n});\n"],"mappings":";AAAA,OAAO,aAAa;AAKb,SAAS,YAAY,IAAY;AACtC,SAAO,QAAQ,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,EAAE;AACzC;AAsBO,SAAS,gBAAgB,IAAqD;AACnF,MAAI,CAAC,YAAY,EAAE,GAAG;AACpB,UAAM,IAAI,MAAM,uBAAuB,EAAE,EAAE;AAAA,EAC7C;AACF;","names":[]}
@@ -1,13 +1,41 @@
1
1
  // src/utils/react.tsx
2
2
  import React from "react";
3
3
  import { isBrowserLike } from "./env.js";
4
- import { neverResolve } from "./promises.js";
4
+ import { neverResolve, runAsynchronously } from "./promises.js";
5
5
  import { deindent } from "./strings.js";
6
6
  function componentWrapper(displayName, render) {
7
7
  const Component = forwardRefIfNeeded(render);
8
8
  Component.displayName = displayName;
9
9
  return Component;
10
10
  }
11
+ var react18PromiseCache = /* @__PURE__ */ new WeakMap();
12
+ function use(promise) {
13
+ if ("use" in React) {
14
+ return React.use(promise);
15
+ } else {
16
+ if (react18PromiseCache.has(promise)) {
17
+ const result = react18PromiseCache.get(promise);
18
+ if (result.status === "pending") {
19
+ throw promise;
20
+ } else if (result.status === "ok") {
21
+ return result.data;
22
+ } else {
23
+ throw result.error;
24
+ }
25
+ } else {
26
+ react18PromiseCache.set(promise, { "status": "pending", progress: void 0 });
27
+ runAsynchronously(async () => {
28
+ try {
29
+ const res = await promise;
30
+ react18PromiseCache.set(promise, { "status": "ok", data: res });
31
+ } catch (e) {
32
+ react18PromiseCache.set(promise, { "status": "error", error: e });
33
+ }
34
+ });
35
+ throw promise;
36
+ }
37
+ }
38
+ }
11
39
  function forwardRefIfNeeded(render) {
12
40
  const version = React.version;
13
41
  const major = parseInt(version.split(".")[0]);
@@ -33,7 +61,7 @@ function getNodeText(node) {
33
61
  throw new Error(`Unknown node type: ${typeof node}`);
34
62
  }
35
63
  function suspend() {
36
- React.use(neverResolve());
64
+ use(neverResolve());
37
65
  throw new Error("Somehow a Promise that never resolves was resolved?");
38
66
  }
39
67
  function mapRef(ref, mapper) {
@@ -80,6 +108,23 @@ function mapRefState(refState, mapper, reverseMapper) {
80
108
  }
81
109
  };
82
110
  }
111
+ function useQueryState(key, defaultValue) {
112
+ const getValue = () => new URLSearchParams(window.location.search).get(key) ?? defaultValue ?? "";
113
+ const [value, setValue] = React.useState(getValue);
114
+ React.useEffect(() => {
115
+ const onPopState = () => setValue(getValue());
116
+ window.addEventListener("popstate", onPopState);
117
+ return () => window.removeEventListener("popstate", onPopState);
118
+ }, []);
119
+ const update = (next) => {
120
+ const params = new URLSearchParams(window.location.search);
121
+ params.set(key, next);
122
+ const newUrl = `${window.location.pathname}?${params.toString()}`;
123
+ window.history.pushState(null, "", newUrl);
124
+ setValue(next);
125
+ };
126
+ return [value, update];
127
+ }
83
128
  function shouldRethrowRenderingError(error) {
84
129
  return !!error && typeof error === "object" && "digest" in error && error.digest === "BAILOUT_TO_CLIENT_SIDE_RENDERING";
85
130
  }
@@ -134,6 +179,8 @@ export {
134
179
  shouldRethrowRenderingError,
135
180
  suspend,
136
181
  suspendIfSsr,
182
+ use,
183
+ useQueryState,
137
184
  useRefState
138
185
  };
139
186
  //# sourceMappingURL=react.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/react.tsx"],"sourcesContent":["import React, { SetStateAction } from \"react\";\nimport { isBrowserLike } from \"./env\";\nimport { neverResolve } from \"./promises\";\nimport { deindent } from \"./strings\";\n\nexport function componentWrapper<\n C extends React.ComponentType<any> | keyof React.JSX.IntrinsicElements,\n ExtraProps extends {} = {}\n>(displayName: string, render: React.ForwardRefRenderFunction<RefFromComponent<C>, React.ComponentPropsWithRef<C> & ExtraProps>) {\n const Component = forwardRefIfNeeded(render);\n Component.displayName = displayName;\n return Component;\n}\ntype RefFromComponent<C extends React.ComponentType<any> | keyof React.JSX.IntrinsicElements> = NonNullable<RefFromComponentDistCond<React.ComponentPropsWithRef<C>[\"ref\"]>>;\ntype RefFromComponentDistCond<A> = A extends React.RefObject<infer T> ? T : never; // distributive conditional type; see https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\n\nexport function forwardRefIfNeeded<T, P = {}>(render: React.ForwardRefRenderFunction<T, P>): React.FC<P & { ref?: React.Ref<T> }> {\n // TODO: when we drop support for react 18, remove this\n\n const version = React.version;\n const major = parseInt(version.split(\".\")[0]);\n if (major < 19) {\n return React.forwardRef<T, P>(render as any) as any;\n } else {\n return ((props: P) => render(props, (props as any).ref)) as any;\n }\n}\n\nexport function getNodeText(node: React.ReactNode): string {\n if ([\"number\", \"string\"].includes(typeof node)) {\n return `${node}`;\n }\n if (!node) {\n return \"\";\n }\n if (Array.isArray(node)) {\n return node.map(getNodeText).join(\"\");\n }\n if (typeof node === \"object\" && \"props\" in node) {\n return getNodeText(node.props.children);\n }\n throw new Error(`Unknown node type: ${typeof node}`);\n}\nundefined?.test(\"getNodeText\", ({ expect }) => {\n // Test with string\n expect(getNodeText(\"hello\")).toBe(\"hello\");\n\n // Test with number\n expect(getNodeText(42)).toBe(\"42\");\n\n // Test with null/undefined\n expect(getNodeText(null)).toBe(\"\");\n expect(getNodeText(undefined)).toBe(\"\");\n\n // Test with array\n expect(getNodeText([\"hello\", \" \", \"world\"])).toBe(\"hello world\");\n expect(getNodeText([1, 2, 3])).toBe(\"123\");\n\n // Test with mixed array\n expect(getNodeText([\"hello\", 42, null])).toBe(\"hello42\");\n\n // Test with React element (mocked)\n const mockElement = {\n props: {\n children: \"child text\"\n }\n } as React.ReactElement;\n expect(getNodeText(mockElement)).toBe(\"child text\");\n\n // Test with nested React elements\n const nestedElement = {\n props: {\n children: {\n props: {\n children: \"nested text\"\n }\n } as React.ReactElement\n }\n } as React.ReactElement;\n expect(getNodeText(nestedElement)).toBe(\"nested text\");\n\n // Test with array of React elements\n const arrayOfElements = [\n { props: { children: \"first\" } } as React.ReactElement,\n { props: { children: \"second\" } } as React.ReactElement\n ];\n expect(getNodeText(arrayOfElements)).toBe(\"firstsecond\");\n});\n\n/**\n * Suspends the currently rendered component indefinitely. Will not unsuspend unless the component rerenders.\n *\n * You can use this to translate older query- or AsyncResult-based code to new the Suspense system, for example: `if (query.isLoading) suspend();`\n */\nexport function suspend(): never {\n React.use(neverResolve());\n throw new Error(\"Somehow a Promise that never resolves was resolved?\");\n}\n\nexport function mapRef<T, R>(ref: ReadonlyRef<T>, mapper: (value: T) => R): ReadonlyRef<R> {\n let last: [T, R] | null = null;\n return {\n get current() {\n const input = ref.current;\n if (last === null || input !== last[0]) {\n last = [input, mapper(input)];\n }\n return last[1];\n },\n };\n}\n\nexport type ReadonlyRef<T> = {\n readonly current: T,\n};\n\nexport type RefState<T> = ReadonlyRef<T> & {\n set: (updater: SetStateAction<T>) => void,\n};\n\n/**\n * Like useState, but its value is immediately available on refState.current after being set.\n *\n * Like useRef, but setting the value will cause a rerender.\n *\n * Note that useRefState returns a new object every time a rerender happens due to a value change, which is intentional\n * as it allows you to specify it in a dependency array like this:\n *\n * ```tsx\n * useEffect(() => {\n * // do something with refState.current\n * }, [refState]); // instead of refState.current\n * ```\n *\n * If you don't want this, you can wrap the result in a useMemo call.\n */\nexport function useRefState<T>(initialValue: T): RefState<T> {\n const [, setState] = React.useState(initialValue);\n const ref = React.useRef(initialValue);\n const setValue = React.useCallback((updater: SetStateAction<T>) => {\n const value: T = typeof updater === \"function\" ? (updater as any)(ref.current) : updater;\n ref.current = value;\n setState(value);\n }, []);\n const res = React.useMemo(() => ({\n get current() {\n return ref.current;\n },\n set: setValue,\n }), [setValue]);\n return res;\n}\n\nexport function mapRefState<T, R>(refState: RefState<T>, mapper: (value: T) => R, reverseMapper: (oldT: T, newR: R) => T): RefState<R> {\n let last: [T, R] | null = null;\n return {\n get current() {\n const input = refState.current;\n if (last === null || input !== last[0]) {\n last = [input, mapper(input)];\n }\n return last[1];\n },\n set(updater: SetStateAction<R>) {\n const value: R = typeof updater === \"function\" ? (updater as any)(this.current) : updater;\n refState.set(reverseMapper(refState.current, value));\n },\n };\n}\n\nexport function shouldRethrowRenderingError(error: unknown): boolean {\n return !!error && typeof error === \"object\" && \"digest\" in error && error.digest === \"BAILOUT_TO_CLIENT_SIDE_RENDERING\";\n}\n\nexport class NoSuspenseBoundaryError extends Error {\n digest: string;\n reason: string;\n\n constructor(options: { caller?: string }) {\n super(deindent`\n Suspense boundary not found! Read the error message below carefully on how to fix it.\n\n ${options.caller ?? \"This code path\"} attempted to display a loading indicator, but didn't find a Suspense boundary above it. Please read the error message below carefully.\n \n The fix depends on which of the 4 scenarios caused it:\n \n 1. [Next.js] You are missing a loading.tsx file in your app directory. Fix it by adding a loading.tsx file in your app directory.\n\n 2. [React] You are missing a <Suspense> boundary in your component. Fix it by wrapping your component (or the entire app) in a <Suspense> component.\n\n 3. [Next.js] The component is rendered in the root (outermost) layout.tsx or template.tsx file. Next.js does not wrap those files in a Suspense boundary, even if there is a loading.tsx file in the same folder. To fix it, wrap your layout inside a route group like this:\n\n - app\n - - layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${options.caller ?? \"this code path\"}\n - - loading.tsx // required for suspense\n - - (main)\n - - - layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${options.caller ?? \"this code path\"}\n - - - route.tsx // your actual main page\n - - - the rest of your app\n\n For more information on this approach, see Next's documentation on route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups\n \n 4. You caught this error with try-catch or a custom error boundary. Fix this by rethrowing the error or not catching it in the first place.\n\n See: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n\n More information on SSR and Suspense boundaries: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content\n `);\n\n this.name = \"NoSuspenseBoundaryError\";\n this.reason = options.caller ?? \"suspendIfSsr()\";\n\n // set the digest so nextjs doesn't log the error\n // https://github.com/vercel/next.js/blob/d01d6d9c35a8c2725b3d74c1402ab76d4779a6cf/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts#L14\n this.digest = \"BAILOUT_TO_CLIENT_SIDE_RENDERING\";\n }\n}\nundefined?.test(\"NoSuspenseBoundaryError\", ({ expect }) => {\n // Test with default options\n const defaultError = new NoSuspenseBoundaryError({});\n expect(defaultError.name).toBe(\"NoSuspenseBoundaryError\");\n expect(defaultError.reason).toBe(\"suspendIfSsr()\");\n expect(defaultError.digest).toBe(\"BAILOUT_TO_CLIENT_SIDE_RENDERING\");\n expect(defaultError.message).toContain(\"This code path attempted to display a loading indicator\");\n\n // Test with custom caller\n const customError = new NoSuspenseBoundaryError({ caller: \"CustomComponent\" });\n expect(customError.name).toBe(\"NoSuspenseBoundaryError\");\n expect(customError.reason).toBe(\"CustomComponent\");\n expect(customError.digest).toBe(\"BAILOUT_TO_CLIENT_SIDE_RENDERING\");\n expect(customError.message).toContain(\"CustomComponent attempted to display a loading indicator\");\n\n // Verify error message contains all the necessary information\n expect(customError.message).toContain(\"loading.tsx\");\n expect(customError.message).toContain(\"route groups\");\n expect(customError.message).toContain(\"https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\");\n});\n\n\n/**\n * Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.\n */\nexport function suspendIfSsr(caller?: string) {\n if (!isBrowserLike()) {\n throw new NoSuspenseBoundaryError({ caller });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/utils/react.tsx"],"sourcesContent":["import React, { SetStateAction } from \"react\";\nimport { isBrowserLike } from \"./env\";\nimport { neverResolve, runAsynchronously } from \"./promises\";\nimport { AsyncResult } from \"./results\";\nimport { deindent } from \"./strings\";\n\nexport function componentWrapper<\n C extends React.ComponentType<any> | keyof React.JSX.IntrinsicElements,\n ExtraProps extends {} = {}\n>(displayName: string, render: React.ForwardRefRenderFunction<RefFromComponent<C>, React.ComponentPropsWithRef<C> & ExtraProps>) {\n const Component = forwardRefIfNeeded(render);\n Component.displayName = displayName;\n return Component;\n}\ntype RefFromComponent<C extends React.ComponentType<any> | keyof React.JSX.IntrinsicElements> = NonNullable<RefFromComponentDistCond<React.ComponentPropsWithRef<C>[\"ref\"]>>;\ntype RefFromComponentDistCond<A> = A extends React.RefObject<infer T> ? T : never; // distributive conditional type; see https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\n\nconst react18PromiseCache = new WeakMap<Promise<unknown>, AsyncResult<unknown, unknown>>();\nexport function use<T>(promise: Promise<T>): T {\n if (\"use\" in React) {\n return React.use(promise);\n } else {\n if (react18PromiseCache.has(promise)) {\n const result = react18PromiseCache.get(promise)!;\n if (result.status === \"pending\") {\n throw promise;\n } else if (result.status === \"ok\") {\n return result.data as T;\n } else {\n throw result.error;\n }\n } else {\n react18PromiseCache.set(promise, { \"status\": \"pending\", progress: undefined });\n runAsynchronously(async () => {\n try {\n const res = await promise;\n react18PromiseCache.set(promise, { \"status\": \"ok\", data: res });\n } catch (e) {\n react18PromiseCache.set(promise, { \"status\": \"error\", error: e });\n }\n });\n throw promise;\n }\n }\n}\n\nexport function forwardRefIfNeeded<T, P = {}>(render: React.ForwardRefRenderFunction<T, P>): React.FC<P & { ref?: React.Ref<T> }> {\n // TODO: when we drop support for react 18, remove this\n\n const version = React.version;\n const major = parseInt(version.split(\".\")[0]);\n if (major < 19) {\n return React.forwardRef<T, P>(render as any) as any;\n } else {\n return ((props: P) => render(props, (props as any).ref)) as any;\n }\n}\n\nexport function getNodeText(node: React.ReactNode): string {\n if ([\"number\", \"string\"].includes(typeof node)) {\n return `${node}`;\n }\n if (!node) {\n return \"\";\n }\n if (Array.isArray(node)) {\n return node.map(getNodeText).join(\"\");\n }\n if (typeof node === \"object\" && \"props\" in node) {\n return getNodeText(node.props.children);\n }\n throw new Error(`Unknown node type: ${typeof node}`);\n}\nundefined?.test(\"getNodeText\", ({ expect }) => {\n // Test with string\n expect(getNodeText(\"hello\")).toBe(\"hello\");\n\n // Test with number\n expect(getNodeText(42)).toBe(\"42\");\n\n // Test with null/undefined\n expect(getNodeText(null)).toBe(\"\");\n expect(getNodeText(undefined)).toBe(\"\");\n\n // Test with array\n expect(getNodeText([\"hello\", \" \", \"world\"])).toBe(\"hello world\");\n expect(getNodeText([1, 2, 3])).toBe(\"123\");\n\n // Test with mixed array\n expect(getNodeText([\"hello\", 42, null])).toBe(\"hello42\");\n\n // Test with React element (mocked)\n const mockElement = {\n props: {\n children: \"child text\"\n }\n } as React.ReactElement;\n expect(getNodeText(mockElement)).toBe(\"child text\");\n\n // Test with nested React elements\n const nestedElement = {\n props: {\n children: {\n props: {\n children: \"nested text\"\n }\n } as React.ReactElement\n }\n } as React.ReactElement;\n expect(getNodeText(nestedElement)).toBe(\"nested text\");\n\n // Test with array of React elements\n const arrayOfElements = [\n { props: { children: \"first\" } } as React.ReactElement,\n { props: { children: \"second\" } } as React.ReactElement\n ];\n expect(getNodeText(arrayOfElements)).toBe(\"firstsecond\");\n});\n\n/**\n * Suspends the currently rendered component indefinitely. Will not unsuspend unless the component rerenders.\n *\n * You can use this to translate older query- or AsyncResult-based code to new the Suspense system, for example: `if (query.isLoading) suspend();`\n */\nexport function suspend(): never {\n use(neverResolve());\n throw new Error(\"Somehow a Promise that never resolves was resolved?\");\n}\n\nexport function mapRef<T, R>(ref: ReadonlyRef<T>, mapper: (value: T) => R): ReadonlyRef<R> {\n let last: [T, R] | null = null;\n return {\n get current() {\n const input = ref.current;\n if (last === null || input !== last[0]) {\n last = [input, mapper(input)];\n }\n return last[1];\n },\n };\n}\n\nexport type ReadonlyRef<T> = {\n readonly current: T,\n};\n\nexport type RefState<T> = ReadonlyRef<T> & {\n set: (updater: SetStateAction<T>) => void,\n};\n\n/**\n * Like useState, but its value is immediately available on refState.current after being set.\n *\n * Like useRef, but setting the value will cause a rerender.\n *\n * Note that useRefState returns a new object every time a rerender happens due to a value change, which is intentional\n * as it allows you to specify it in a dependency array like this:\n *\n * ```tsx\n * useEffect(() => {\n * // do something with refState.current\n * }, [refState]); // instead of refState.current\n * ```\n *\n * If you don't want this, you can wrap the result in a useMemo call.\n */\nexport function useRefState<T>(initialValue: T): RefState<T> {\n const [, setState] = React.useState(initialValue);\n const ref = React.useRef(initialValue);\n const setValue = React.useCallback((updater: SetStateAction<T>) => {\n const value: T = typeof updater === \"function\" ? (updater as any)(ref.current) : updater;\n ref.current = value;\n setState(value);\n }, []);\n const res = React.useMemo(() => ({\n get current() {\n return ref.current;\n },\n set: setValue,\n }), [setValue]);\n return res;\n}\n\nexport function mapRefState<T, R>(refState: RefState<T>, mapper: (value: T) => R, reverseMapper: (oldT: T, newR: R) => T): RefState<R> {\n let last: [T, R] | null = null;\n return {\n get current() {\n const input = refState.current;\n if (last === null || input !== last[0]) {\n last = [input, mapper(input)];\n }\n return last[1];\n },\n set(updater: SetStateAction<R>) {\n const value: R = typeof updater === \"function\" ? (updater as any)(this.current) : updater;\n refState.set(reverseMapper(refState.current, value));\n },\n };\n}\n\nexport function useQueryState(key: string, defaultValue?: string) {\n const getValue = () => new URLSearchParams(window.location.search).get(key) ?? defaultValue ?? \"\";\n\n const [value, setValue] = React.useState(getValue);\n\n React.useEffect(() => {\n const onPopState = () => setValue(getValue());\n window.addEventListener(\"popstate\", onPopState);\n return () => window.removeEventListener(\"popstate\", onPopState);\n }, []);\n\n const update = (next: string) => {\n const params = new URLSearchParams(window.location.search);\n params.set(key, next);\n const newUrl = `${window.location.pathname}?${params.toString()}`;\n window.history.pushState(null, \"\", newUrl);\n setValue(next);\n };\n\n return [value, update] as const;\n}\n\nexport function shouldRethrowRenderingError(error: unknown): boolean {\n return !!error && typeof error === \"object\" && \"digest\" in error && error.digest === \"BAILOUT_TO_CLIENT_SIDE_RENDERING\";\n}\n\nexport class NoSuspenseBoundaryError extends Error {\n digest: string;\n reason: string;\n\n constructor(options: { caller?: string }) {\n super(deindent`\n Suspense boundary not found! Read the error message below carefully on how to fix it.\n\n ${options.caller ?? \"This code path\"} attempted to display a loading indicator, but didn't find a Suspense boundary above it. Please read the error message below carefully.\n \n The fix depends on which of the 4 scenarios caused it:\n \n 1. [Next.js] You are missing a loading.tsx file in your app directory. Fix it by adding a loading.tsx file in your app directory.\n\n 2. [React] You are missing a <Suspense> boundary in your component. Fix it by wrapping your component (or the entire app) in a <Suspense> component.\n\n 3. [Next.js] The component is rendered in the root (outermost) layout.tsx or template.tsx file. Next.js does not wrap those files in a Suspense boundary, even if there is a loading.tsx file in the same folder. To fix it, wrap your layout inside a route group like this:\n\n - app\n - - layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${options.caller ?? \"this code path\"}\n - - loading.tsx // required for suspense\n - - (main)\n - - - layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${options.caller ?? \"this code path\"}\n - - - route.tsx // your actual main page\n - - - the rest of your app\n\n For more information on this approach, see Next's documentation on route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups\n \n 4. You caught this error with try-catch or a custom error boundary. Fix this by rethrowing the error or not catching it in the first place.\n\n See: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n\n More information on SSR and Suspense boundaries: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content\n `);\n\n this.name = \"NoSuspenseBoundaryError\";\n this.reason = options.caller ?? \"suspendIfSsr()\";\n\n // set the digest so nextjs doesn't log the error\n // https://github.com/vercel/next.js/blob/d01d6d9c35a8c2725b3d74c1402ab76d4779a6cf/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts#L14\n this.digest = \"BAILOUT_TO_CLIENT_SIDE_RENDERING\";\n }\n}\nundefined?.test(\"NoSuspenseBoundaryError\", ({ expect }) => {\n // Test with default options\n const defaultError = new NoSuspenseBoundaryError({});\n expect(defaultError.name).toBe(\"NoSuspenseBoundaryError\");\n expect(defaultError.reason).toBe(\"suspendIfSsr()\");\n expect(defaultError.digest).toBe(\"BAILOUT_TO_CLIENT_SIDE_RENDERING\");\n expect(defaultError.message).toContain(\"This code path attempted to display a loading indicator\");\n\n // Test with custom caller\n const customError = new NoSuspenseBoundaryError({ caller: \"CustomComponent\" });\n expect(customError.name).toBe(\"NoSuspenseBoundaryError\");\n expect(customError.reason).toBe(\"CustomComponent\");\n expect(customError.digest).toBe(\"BAILOUT_TO_CLIENT_SIDE_RENDERING\");\n expect(customError.message).toContain(\"CustomComponent attempted to display a loading indicator\");\n\n // Verify error message contains all the necessary information\n expect(customError.message).toContain(\"loading.tsx\");\n expect(customError.message).toContain(\"route groups\");\n expect(customError.message).toContain(\"https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\");\n});\n\n\n/**\n * Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.\n */\nexport function suspendIfSsr(caller?: string) {\n if (!isBrowserLike()) {\n throw new NoSuspenseBoundaryError({ caller });\n }\n}\n"],"mappings":"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","names":[]}
@@ -142,6 +142,7 @@ var AsyncStore = class _AsyncStore {
142
142
  }
143
143
  setUnavailable() {
144
144
  this._lastSuccessfulUpdate = ++this._updateCounter;
145
+ this._mostRecentOkValue = void 0;
145
146
  this._isAvailable = false;
146
147
  this._isRejected = false;
147
148
  this._rejectionError = void 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/stores.tsx"],"sourcesContent":["import { ReadWriteLock } from \"./locks\";\nimport { ReactPromise, pending, rejected, resolved } from \"./promises\";\nimport { AsyncResult, Result } from \"./results\";\nimport { generateUuid } from \"./uuids\";\n\nexport type ReadonlyStore<T> = {\n get(): T,\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n};\n\nexport type AsyncStoreStateChangeCallback<T> = (args: { state: AsyncResult<T>, oldState: AsyncResult<T>, lastOkValue: T | undefined }) => void;\n\nexport type ReadonlyAsyncStore<T> = {\n isAvailable(): boolean,\n get(): AsyncResult<T, unknown, void>,\n getOrWait(): ReactPromise<T>,\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void },\n onceStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void },\n};\n\nexport class Store<T> implements ReadonlyStore<T> {\n private readonly _callbacks: Map<string, ((value: T, oldValue: T | undefined) => void)> = new Map();\n\n constructor(\n private _value: T\n ) {}\n\n get(): T {\n return this._value;\n }\n\n set(value: T): void {\n const oldValue = this._value;\n this._value = value;\n this._callbacks.forEach((callback) => callback(value, oldValue));\n }\n\n update(updater: (value: T) => T): T {\n const value = updater(this._value);\n this.set(value);\n return value;\n }\n\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const uuid = generateUuid();\n this._callbacks.set(uuid, callback);\n return {\n unsubscribe: () => {\n this._callbacks.delete(uuid);\n },\n };\n }\n\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const { unsubscribe } = this.onChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n}\n\nexport const storeLock = new ReadWriteLock();\n\n\nexport class AsyncStore<T> implements ReadonlyAsyncStore<T> {\n private _isAvailable: boolean;\n private _mostRecentOkValue: T | undefined = undefined;\n\n private _isRejected = false;\n private _rejectionError: unknown;\n private readonly _waitingRejectFunctions = new Map<string, ((error: unknown) => void)>();\n\n private readonly _callbacks: Map<string, AsyncStoreStateChangeCallback<T>> = new Map();\n\n private _updateCounter = 0;\n private _lastSuccessfulUpdate = -1;\n\n constructor(...args: [] | [T]) {\n if (args.length === 0) {\n this._isAvailable = false;\n } else {\n this._isAvailable = true;\n this._mostRecentOkValue = args[0];\n }\n }\n\n isAvailable(): boolean {\n return this._isAvailable;\n }\n\n isRejected(): boolean {\n return this._isRejected;\n }\n\n get() {\n if (this.isRejected()) {\n return AsyncResult.error(this._rejectionError);\n } else if (this.isAvailable()) {\n return AsyncResult.ok(this._mostRecentOkValue as T);\n } else {\n return AsyncResult.pending();\n }\n }\n\n getOrWait(): ReactPromise<T> {\n const uuid = generateUuid();\n if (this.isRejected()) {\n return rejected(this._rejectionError);\n } else if (this.isAvailable()) {\n return resolved(this._mostRecentOkValue as T);\n }\n const promise = new Promise<T>((resolve, reject) => {\n this.onceChange((value) => {\n resolve(value);\n });\n this._waitingRejectFunctions.set(uuid, reject);\n });\n const withFinally = promise.finally(() => {\n this._waitingRejectFunctions.delete(uuid);\n });\n return pending(withFinally);\n }\n\n _setIfLatest(result: Result<T>, curCounter: number) {\n const oldState = this.get();\n const oldValue = this._mostRecentOkValue;\n if (curCounter > this._lastSuccessfulUpdate) {\n switch (result.status) {\n case \"ok\": {\n if (!this._isAvailable || this._isRejected || this._mostRecentOkValue !== result.data) {\n this._lastSuccessfulUpdate = curCounter;\n this._isAvailable = true;\n this._isRejected = false;\n this._mostRecentOkValue = result.data;\n this._rejectionError = undefined;\n this._callbacks.forEach((callback) => callback({\n state: this.get(),\n oldState,\n lastOkValue: oldValue,\n }));\n return true;\n }\n return false;\n }\n case \"error\": {\n this._lastSuccessfulUpdate = curCounter;\n this._isAvailable = false;\n this._isRejected = true;\n this._rejectionError = result.error;\n this._waitingRejectFunctions.forEach((reject) => reject(result.error));\n this._callbacks.forEach((callback) => callback({\n state: this.get(),\n oldState,\n lastOkValue: oldValue,\n }));\n return true;\n }\n }\n }\n return false;\n }\n\n set(value: T): void {\n this._setIfLatest(Result.ok(value), ++this._updateCounter);\n }\n\n update(updater: (value: T | undefined) => T): T {\n const value = updater(this._mostRecentOkValue);\n this.set(value);\n return value;\n }\n\n async setAsync(promise: Promise<T>): Promise<boolean> {\n return await storeLock.withReadLock(async () => {\n const curCounter = ++this._updateCounter;\n const result = await Result.fromPromise(promise);\n return this._setIfLatest(result, curCounter);\n });\n }\n\n setUnavailable(): void {\n this._lastSuccessfulUpdate = ++this._updateCounter;\n this._isAvailable = false;\n this._isRejected = false;\n this._rejectionError = undefined;\n }\n\n setRejected(error: unknown): void {\n this._setIfLatest(Result.error(error), ++this._updateCounter);\n }\n\n map<U>(mapper: (value: T) => U): AsyncStore<U> {\n const store = new AsyncStore<U>();\n this.onChange((value) => {\n store.set(mapper(value));\n });\n return store;\n }\n\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n return this.onStateChange(({ state, lastOkValue }) => {\n if (state.status === \"ok\") {\n callback(state.data, lastOkValue);\n }\n });\n }\n\n onStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void } {\n const uuid = generateUuid();\n this._callbacks.set(uuid, callback);\n return {\n unsubscribe: () => {\n this._callbacks.delete(uuid);\n },\n };\n }\n\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const { unsubscribe } = this.onChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n\n onceStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void } {\n const { unsubscribe } = this.onStateChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/utils/stores.tsx"],"sourcesContent":["import { ReadWriteLock } from \"./locks\";\nimport { ReactPromise, pending, rejected, resolved } from \"./promises\";\nimport { AsyncResult, Result } from \"./results\";\nimport { generateUuid } from \"./uuids\";\n\nexport type ReadonlyStore<T> = {\n get(): T,\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n};\n\nexport type AsyncStoreStateChangeCallback<T> = (args: { state: AsyncResult<T>, oldState: AsyncResult<T>, lastOkValue: T | undefined }) => void;\n\nexport type ReadonlyAsyncStore<T> = {\n isAvailable(): boolean,\n get(): AsyncResult<T, unknown, void>,\n getOrWait(): ReactPromise<T>,\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void },\n onStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void },\n onceStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void },\n};\n\nexport class Store<T> implements ReadonlyStore<T> {\n private readonly _callbacks: Map<string, ((value: T, oldValue: T | undefined) => void)> = new Map();\n\n constructor(\n private _value: T\n ) {}\n\n get(): T {\n return this._value;\n }\n\n set(value: T): void {\n const oldValue = this._value;\n this._value = value;\n this._callbacks.forEach((callback) => callback(value, oldValue));\n }\n\n update(updater: (value: T) => T): T {\n const value = updater(this._value);\n this.set(value);\n return value;\n }\n\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const uuid = generateUuid();\n this._callbacks.set(uuid, callback);\n return {\n unsubscribe: () => {\n this._callbacks.delete(uuid);\n },\n };\n }\n\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const { unsubscribe } = this.onChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n}\n\nexport const storeLock = new ReadWriteLock();\n\n\nexport class AsyncStore<T> implements ReadonlyAsyncStore<T> {\n private _isAvailable: boolean;\n private _mostRecentOkValue: T | undefined = undefined;\n\n private _isRejected = false;\n private _rejectionError: unknown;\n private readonly _waitingRejectFunctions = new Map<string, ((error: unknown) => void)>();\n\n private readonly _callbacks: Map<string, AsyncStoreStateChangeCallback<T>> = new Map();\n\n private _updateCounter = 0;\n private _lastSuccessfulUpdate = -1;\n\n constructor(...args: [] | [T]) {\n if (args.length === 0) {\n this._isAvailable = false;\n } else {\n this._isAvailable = true;\n this._mostRecentOkValue = args[0];\n }\n }\n\n isAvailable(): boolean {\n return this._isAvailable;\n }\n\n isRejected(): boolean {\n return this._isRejected;\n }\n\n get() {\n if (this.isRejected()) {\n return AsyncResult.error(this._rejectionError);\n } else if (this.isAvailable()) {\n return AsyncResult.ok(this._mostRecentOkValue as T);\n } else {\n return AsyncResult.pending();\n }\n }\n\n getOrWait(): ReactPromise<T> {\n const uuid = generateUuid();\n if (this.isRejected()) {\n return rejected(this._rejectionError);\n } else if (this.isAvailable()) {\n return resolved(this._mostRecentOkValue as T);\n }\n const promise = new Promise<T>((resolve, reject) => {\n this.onceChange((value) => {\n resolve(value);\n });\n this._waitingRejectFunctions.set(uuid, reject);\n });\n const withFinally = promise.finally(() => {\n this._waitingRejectFunctions.delete(uuid);\n });\n return pending(withFinally);\n }\n\n _setIfLatest(result: Result<T>, curCounter: number) {\n const oldState = this.get();\n const oldValue = this._mostRecentOkValue;\n if (curCounter > this._lastSuccessfulUpdate) {\n switch (result.status) {\n case \"ok\": {\n if (!this._isAvailable || this._isRejected || this._mostRecentOkValue !== result.data) {\n this._lastSuccessfulUpdate = curCounter;\n this._isAvailable = true;\n this._isRejected = false;\n this._mostRecentOkValue = result.data;\n this._rejectionError = undefined;\n this._callbacks.forEach((callback) => callback({\n state: this.get(),\n oldState,\n lastOkValue: oldValue,\n }));\n return true;\n }\n return false;\n }\n case \"error\": {\n this._lastSuccessfulUpdate = curCounter;\n this._isAvailable = false;\n this._isRejected = true;\n this._rejectionError = result.error;\n this._waitingRejectFunctions.forEach((reject) => reject(result.error));\n this._callbacks.forEach((callback) => callback({\n state: this.get(),\n oldState,\n lastOkValue: oldValue,\n }));\n return true;\n }\n }\n }\n return false;\n }\n\n set(value: T): void {\n this._setIfLatest(Result.ok(value), ++this._updateCounter);\n }\n\n update(updater: (value: T | undefined) => T): T {\n const value = updater(this._mostRecentOkValue);\n this.set(value);\n return value;\n }\n\n async setAsync(promise: Promise<T>): Promise<boolean> {\n return await storeLock.withReadLock(async () => {\n const curCounter = ++this._updateCounter;\n const result = await Result.fromPromise(promise);\n return this._setIfLatest(result, curCounter);\n });\n }\n\n setUnavailable(): void {\n this._lastSuccessfulUpdate = ++this._updateCounter;\n this._mostRecentOkValue = undefined;\n this._isAvailable = false;\n this._isRejected = false;\n this._rejectionError = undefined;\n }\n\n setRejected(error: unknown): void {\n this._setIfLatest(Result.error(error), ++this._updateCounter);\n }\n\n map<U>(mapper: (value: T) => U): AsyncStore<U> {\n const store = new AsyncStore<U>();\n this.onChange((value) => {\n store.set(mapper(value));\n });\n return store;\n }\n\n onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n return this.onStateChange(({ state, lastOkValue }) => {\n if (state.status === \"ok\") {\n callback(state.data, lastOkValue);\n }\n });\n }\n\n onStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void } {\n const uuid = generateUuid();\n this._callbacks.set(uuid, callback);\n return {\n unsubscribe: () => {\n this._callbacks.delete(uuid);\n },\n };\n }\n\n onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {\n const { unsubscribe } = this.onChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n\n onceStateChange(callback: AsyncStoreStateChangeCallback<T>): { unsubscribe: () => void } {\n const { unsubscribe } = this.onStateChange((...args) => {\n unsubscribe();\n callback(...args);\n });\n return { unsubscribe };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/urls.tsx"],"sourcesContent":["import { generateSecureRandomString } from \"./crypto\";\nimport { templateIdentity } from \"./strings\";\n\nexport function createUrlIfValid(...args: ConstructorParameters<typeof URL>) {\n try {\n return new URL(...args);\n } catch (e) {\n return null;\n }\n}\nundefined?.test(\"createUrlIfValid\", ({ expect }) => {\n // Test with valid URLs\n expect(createUrlIfValid(\"https://example.com\")).toBeInstanceOf(URL);\n expect(createUrlIfValid(\"https://example.com/path?query=value#hash\")).toBeInstanceOf(URL);\n expect(createUrlIfValid(\"/path\", \"https://example.com\")).toBeInstanceOf(URL);\n\n // Test with invalid URLs\n expect(createUrlIfValid(\"\")).toBeNull();\n expect(createUrlIfValid(\"not a url\")).toBeNull();\n expect(createUrlIfValid(\"http://\")).toBeNull();\n});\n\nexport function isValidUrl(url: string) {\n return !!createUrlIfValid(url);\n}\nundefined?.test(\"isValidUrl\", ({ expect }) => {\n // Test with valid URLs\n expect(isValidUrl(\"https://example.com\")).toBe(true);\n expect(isValidUrl(\"http://localhost:3000\")).toBe(true);\n expect(isValidUrl(\"ftp://example.com\")).toBe(true);\n\n // Test with invalid URLs\n expect(isValidUrl(\"\")).toBe(false);\n expect(isValidUrl(\"not a url\")).toBe(false);\n expect(isValidUrl(\"http://\")).toBe(false);\n});\n\nexport function isValidHostname(hostname: string) {\n // Basic validation\n if (!hostname || hostname.startsWith('.') || hostname.endsWith('.') || hostname.includes('..')) {\n return false;\n }\n\n const url = createUrlIfValid(`https://${hostname}`);\n if (!url) return false;\n return url.hostname === hostname;\n}\nundefined?.test(\"isValidHostname\", ({ expect }) => {\n // Test with valid hostnames\n expect(isValidHostname(\"example.com\")).toBe(true);\n expect(isValidHostname(\"localhost\")).toBe(true);\n expect(isValidHostname(\"sub.domain.example.com\")).toBe(true);\n expect(isValidHostname(\"127.0.0.1\")).toBe(true);\n\n // Test with invalid hostnames\n expect(isValidHostname(\"\")).toBe(false);\n expect(isValidHostname(\"example.com/path\")).toBe(false);\n expect(isValidHostname(\"https://example.com\")).toBe(false);\n expect(isValidHostname(\"example com\")).toBe(false);\n});\n\nexport function isValidHostnameWithWildcards(hostname: string) {\n // Empty hostnames are invalid\n if (!hostname) return false;\n\n // Check if it contains wildcards\n const hasWildcard = hostname.includes('*');\n\n if (!hasWildcard) {\n // If no wildcards, validate as a normal hostname\n return isValidHostname(hostname);\n }\n\n // Basic validation checks that apply even with wildcards\n // - Hostname cannot start or end with a dot\n if (hostname.startsWith('.') || hostname.endsWith('.')) {\n return false;\n }\n\n // - No consecutive dots\n if (hostname.includes('..')) {\n return false;\n }\n\n // For wildcard validation, check that non-wildcard parts contain valid characters\n // Replace wildcards with a valid placeholder to check the rest\n const testHostname = hostname.replace(/\\*+/g, 'wildcard');\n\n // Check if the resulting string would be a valid hostname\n if (!/^[a-zA-Z0-9.-]+$/.test(testHostname)) {\n return false;\n }\n\n // Additional check: ensure the pattern makes sense\n // Check each segment between wildcards\n const segments = hostname.split(/\\*+/);\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === '') continue; // Empty segments are OK (consecutive wildcards)\n\n // First segment can't start with dot\n if (i === 0 && segment.startsWith('.')) {\n return false;\n }\n\n // Last segment can't end with dot\n if (i === segments.length - 1 && segment.endsWith('.')) {\n return false;\n }\n\n // No segment should have consecutive dots\n if (segment.includes('..')) {\n return false;\n }\n }\n\n return true;\n}\nundefined?.test(\"isValidHostnameWithWildcards\", ({ expect }) => {\n // Test with valid regular hostnames\n expect(isValidHostnameWithWildcards(\"example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"localhost\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"sub.domain.example.com\")).toBe(true);\n\n // Test with valid wildcard hostnames\n expect(isValidHostnameWithWildcards(\"*.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"a-*.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"*.*.org\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"**.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"sub.**.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"*-api.*.com\")).toBe(true);\n\n // Test with invalid hostnames\n expect(isValidHostnameWithWildcards(\"\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example.com/path\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"https://example.com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\".example.com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example.com.\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example..com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"*.example..com\")).toBe(false);\n});\n\nexport function matchHostnamePattern(pattern: string, hostname: string): boolean {\n // If no wildcards, it's an exact match\n if (!pattern.includes('*')) {\n return pattern === hostname;\n }\n\n // Convert the pattern to a regex\n // First, escape all regex special characters except *\n let regexPattern = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Use a placeholder for ** to handle it separately from single *\n const doubleWildcardPlaceholder = '\\x00DOUBLE_WILDCARD\\x00';\n regexPattern = regexPattern.replace(/\\*\\*/g, doubleWildcardPlaceholder);\n\n // Replace single * with a pattern that matches anything except dots\n regexPattern = regexPattern.replace(/\\*/g, '[^.]*');\n\n // Replace the double wildcard placeholder with a pattern that matches anything including dots\n regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');\n\n // Anchor the pattern to match the entire hostname\n regexPattern = '^' + regexPattern + '$';\n\n try {\n const regex = new RegExp(regexPattern);\n return regex.test(hostname);\n } catch {\n return false;\n }\n}\nundefined?.test(\"matchHostnamePattern\", ({ expect }) => {\n // Test exact matches\n expect(matchHostnamePattern(\"example.com\", \"example.com\")).toBe(true);\n expect(matchHostnamePattern(\"example.com\", \"other.com\")).toBe(false);\n\n // Test single wildcard matches\n expect(matchHostnamePattern(\"*.example.com\", \"api.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"*.example.com\", \"www.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"*.example.com\", \"example.com\")).toBe(false);\n expect(matchHostnamePattern(\"*.example.com\", \"api.v2.example.com\")).toBe(false);\n\n // Test double wildcard matches\n expect(matchHostnamePattern(\"**.example.com\", \"api.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"api.v2.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"a.b.c.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"example.com\")).toBe(false);\n\n // Test complex patterns\n expect(matchHostnamePattern(\"api-*.example.com\", \"api-v1.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"api-*.example.com\", \"api-v2.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"api-*.example.com\", \"api.example.com\")).toBe(false);\n expect(matchHostnamePattern(\"*.*.org\", \"mail.example.org\")).toBe(true);\n expect(matchHostnamePattern(\"*.*.org\", \"example.org\")).toBe(false);\n});\n\nexport function isLocalhost(urlOrString: string | URL) {\n const url = createUrlIfValid(urlOrString);\n if (!url) return false;\n if (url.hostname === \"localhost\" || url.hostname.endsWith(\".localhost\")) return true;\n if (url.hostname.match(/^127\\.\\d+\\.\\d+\\.\\d+$/)) return true;\n return false;\n}\nundefined?.test(\"isLocalhost\", ({ expect }) => {\n // Test with localhost URLs\n expect(isLocalhost(\"http://localhost\")).toBe(true);\n expect(isLocalhost(\"https://localhost:8080\")).toBe(true);\n expect(isLocalhost(\"http://sub.localhost\")).toBe(true);\n expect(isLocalhost(\"http://127.0.0.1\")).toBe(true);\n expect(isLocalhost(\"http://127.1.2.3\")).toBe(true);\n\n // Test with non-localhost URLs\n expect(isLocalhost(\"https://example.com\")).toBe(false);\n expect(isLocalhost(\"http://192.168.1.1\")).toBe(false);\n expect(isLocalhost(\"http://10.0.0.1\")).toBe(false);\n\n // Test with URL objects\n expect(isLocalhost(new URL(\"http://localhost\"))).toBe(true);\n expect(isLocalhost(new URL(\"https://example.com\"))).toBe(false);\n\n // Test with invalid URLs\n expect(isLocalhost(\"not a url\")).toBe(false);\n expect(isLocalhost(\"\")).toBe(false);\n});\n\nexport function isRelative(url: string) {\n const randomDomain = `${generateSecureRandomString()}.stack-auth.example.com`;\n const u = createUrlIfValid(url, `https://${randomDomain}`);\n if (!u) return false;\n if (u.host !== randomDomain) return false;\n if (u.protocol !== \"https:\") return false;\n return true;\n}\nundefined?.test(\"isRelative\", ({ expect }) => {\n // We can't easily mock generateSecureRandomString in this context\n // but we can still test the function's behavior\n\n // Test with relative URLs\n expect(isRelative(\"/\")).toBe(true);\n expect(isRelative(\"/path\")).toBe(true);\n expect(isRelative(\"/path?query=value#hash\")).toBe(true);\n\n // Test with absolute URLs\n expect(isRelative(\"https://example.com\")).toBe(false);\n expect(isRelative(\"http://example.com\")).toBe(false);\n expect(isRelative(\"//example.com\")).toBe(false);\n\n // Note: The implementation treats empty strings and invalid URLs as relative\n // This is because they can be resolved against a base URL\n expect(isRelative(\"\")).toBe(true);\n expect(isRelative(\"not a url\")).toBe(true);\n});\n\nexport function getRelativePart(url: URL) {\n return url.pathname + url.search + url.hash;\n}\nundefined?.test(\"getRelativePart\", ({ expect }) => {\n // Test with various URLs\n expect(getRelativePart(new URL(\"https://example.com\"))).toBe(\"/\");\n expect(getRelativePart(new URL(\"https://example.com/path\"))).toBe(\"/path\");\n expect(getRelativePart(new URL(\"https://example.com/path?query=value\"))).toBe(\"/path?query=value\");\n expect(getRelativePart(new URL(\"https://example.com/path#hash\"))).toBe(\"/path#hash\");\n expect(getRelativePart(new URL(\"https://example.com/path?query=value#hash\"))).toBe(\"/path?query=value#hash\");\n\n // Test with different domains but same paths\n const url1 = new URL(\"https://example.com/path?query=value#hash\");\n const url2 = new URL(\"https://different.com/path?query=value#hash\");\n expect(getRelativePart(url1)).toBe(getRelativePart(url2));\n});\n\n/**\n * A template literal tag that returns a URL.\n *\n * Any values passed are encoded.\n */\nexport function url(strings: TemplateStringsArray | readonly string[], ...values: (string|number|boolean)[]): URL {\n return new URL(urlString(strings, ...values));\n}\nundefined?.test(\"url\", ({ expect }) => {\n // Test with no interpolation\n expect(url`https://example.com`).toBeInstanceOf(URL);\n expect(url`https://example.com`.href).toBe(\"https://example.com/\");\n\n // Test with string interpolation\n expect(url`https://example.com/${\"path\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path\"}`.pathname).toBe(\"/path\");\n\n // Test with number interpolation\n expect(url`https://example.com/${42}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${42}`.pathname).toBe(\"/42\");\n\n // Test with boolean interpolation\n expect(url`https://example.com/${true}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${true}`.pathname).toBe(\"/true\");\n\n // Test with special characters in interpolation\n expect(url`https://example.com/${\"path with spaces\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path with spaces\"}`.pathname).toBe(\"/path%20with%20spaces\");\n\n // Test with multiple interpolations\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`.pathname).toBe(\"/path\");\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`.search).toBe(\"?query=value\");\n});\n\n\n/**\n * A template literal tag that returns a URL string.\n *\n * Any values passed are encoded.\n */\nexport function urlString(strings: TemplateStringsArray | readonly string[], ...values: (string|number|boolean)[]): string {\n return templateIdentity(strings, ...values.map(encodeURIComponent));\n}\nundefined?.test(\"urlString\", ({ expect }) => {\n // Test with no interpolation\n expect(urlString`https://example.com`).toBe(\"https://example.com\");\n\n // Test with string interpolation\n expect(urlString`https://example.com/${\"path\"}`).toBe(\"https://example.com/path\");\n\n // Test with number interpolation\n expect(urlString`https://example.com/${42}`).toBe(\"https://example.com/42\");\n\n // Test with boolean interpolation\n expect(urlString`https://example.com/${true}`).toBe(\"https://example.com/true\");\n\n // Test with special characters in interpolation\n expect(urlString`https://example.com/${\"path with spaces\"}`).toBe(\"https://example.com/path%20with%20spaces\");\n expect(urlString`https://example.com/${\"?&=\"}`).toBe(\"https://example.com/%3F%26%3D\");\n\n // Test with multiple interpolations\n expect(urlString`https://example.com/${\"path\"}?query=${\"value\"}`).toBe(\"https://example.com/path?query=value\");\n expect(urlString`https://example.com/${\"path\"}?query=${\"value with spaces\"}`).toBe(\"https://example.com/path?query=value%20with%20spaces\");\n});\n\nexport function isChildUrl(parentUrl: URL, maybeChildUrl: URL) {\n return parentUrl.origin === maybeChildUrl.origin\n && isChildPath(parentUrl.pathname, maybeChildUrl.pathname)\n && [...parentUrl.searchParams].every(([key, value]) => maybeChildUrl.searchParams.get(key) === value)\n && (!parentUrl.hash || parentUrl.hash === maybeChildUrl.hash);\n}\nundefined?.test(\"isChildUrl\", ({ expect }) => {\n // true\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path?query=value\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path?query=value#hash\"))).toBe(true);\n\n // false\n expect(isChildUrl(new URL(\"https://abc.com\"), new URL(\"https://example.com\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path?query=value\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path?query=value#hash\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://example.com\"), new URL(\"https://abc.com/path?query=value#hash\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://example.com?query=value123\"), new URL(\"https://example.com/path?query=value#hash\"))).toBe(false);\n});\n\nexport function isChildPath(parentPath: string, maybeChildPath: string) {\n parentPath = parentPath.endsWith(\"/\") ? parentPath : parentPath + \"/\";\n maybeChildPath = maybeChildPath.endsWith(\"/\") ? maybeChildPath : maybeChildPath + \"/\";\n return maybeChildPath.startsWith(parentPath);\n}\nundefined?.test(\"isSubPath\", ({ expect }) => {\n expect(isChildPath(\"/\", \"/\")).toBe(true);\n expect(isChildPath(\"/\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path\", \"/\")).toBe(false);\n expect(isChildPath(\"/path\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path/\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path/\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path/abc\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path/abc\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path-abc\")).toBe(false);\n expect(isChildPath(\"/path\", \"/path-abc/\")).toBe(false);\n expect(isChildPath(\"/path/\", \"/path-abc\")).toBe(false);\n expect(isChildPath(\"/path/\", \"/path-abc/\")).toBe(false);\n});\n\n"],"mappings":"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","names":["url"]}
1
+ {"version":3,"sources":["../../../src/utils/urls.tsx"],"sourcesContent":["import { generateSecureRandomString } from \"./crypto\";\nimport { templateIdentity } from \"./strings\";\n\nexport function createUrlIfValid(...args: ConstructorParameters<typeof URL>) {\n try {\n return new URL(...args);\n } catch (e) {\n return null;\n }\n}\nundefined?.test(\"createUrlIfValid\", ({ expect }) => {\n // Test with valid URLs\n expect(createUrlIfValid(\"https://example.com\")).toBeInstanceOf(URL);\n expect(createUrlIfValid(\"https://example.com/path?query=value#hash\")).toBeInstanceOf(URL);\n expect(createUrlIfValid(\"/path\", \"https://example.com\")).toBeInstanceOf(URL);\n\n // Test with invalid URLs\n expect(createUrlIfValid(\"\")).toBeNull();\n expect(createUrlIfValid(\"not a url\")).toBeNull();\n expect(createUrlIfValid(\"http://\")).toBeNull();\n});\n\nexport function isValidUrl(url: string) {\n return !!createUrlIfValid(url);\n}\nundefined?.test(\"isValidUrl\", ({ expect }) => {\n // Test with valid URLs\n expect(isValidUrl(\"https://example.com\")).toBe(true);\n expect(isValidUrl(\"http://localhost:3000\")).toBe(true);\n expect(isValidUrl(\"ftp://example.com\")).toBe(true);\n\n // Test with invalid URLs\n expect(isValidUrl(\"\")).toBe(false);\n expect(isValidUrl(\"not a url\")).toBe(false);\n expect(isValidUrl(\"http://\")).toBe(false);\n});\n\nexport function isValidHostname(hostname: string) {\n // Basic validation\n if (!hostname || hostname.startsWith('.') || hostname.endsWith('.') || hostname.includes('..')) {\n return false;\n }\n\n const url = createUrlIfValid(`https://${hostname}`);\n if (!url) return false;\n return url.hostname === hostname;\n}\nundefined?.test(\"isValidHostname\", ({ expect }) => {\n // Test with valid hostnames\n expect(isValidHostname(\"example.com\")).toBe(true);\n expect(isValidHostname(\"localhost\")).toBe(true);\n expect(isValidHostname(\"sub.domain.example.com\")).toBe(true);\n expect(isValidHostname(\"127.0.0.1\")).toBe(true);\n\n // Test with invalid hostnames\n expect(isValidHostname(\"\")).toBe(false);\n expect(isValidHostname(\"example.com/path\")).toBe(false);\n expect(isValidHostname(\"https://example.com\")).toBe(false);\n expect(isValidHostname(\"example com\")).toBe(false);\n});\n\nexport function isValidHostnameWithWildcards(hostname: string) {\n // Empty hostnames are invalid\n if (!hostname) return false;\n\n // Check if it contains wildcards\n const hasWildcard = hostname.includes('*');\n\n if (!hasWildcard) {\n // If no wildcards, validate as a normal hostname\n return isValidHostname(hostname);\n }\n\n // Basic validation checks that apply even with wildcards\n // - Hostname cannot start or end with a dot\n if (hostname.startsWith('.') || hostname.endsWith('.')) {\n return false;\n }\n\n // - No consecutive dots\n if (hostname.includes('..')) {\n return false;\n }\n\n // For wildcard validation, check that non-wildcard parts contain valid characters\n // Replace wildcards with a valid placeholder to check the rest\n const testHostname = hostname.replace(/\\*+/g, 'wildcard');\n\n // Check if the resulting string would be a valid hostname\n if (!/^[a-zA-Z0-9.-]+$/.test(testHostname)) {\n return false;\n }\n\n // Additional check: ensure the pattern makes sense\n // Check each segment between wildcards\n const segments = hostname.split(/\\*+/);\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === '') continue; // Empty segments are OK (consecutive wildcards)\n\n // First segment can't start with dot\n if (i === 0 && segment.startsWith('.')) {\n return false;\n }\n\n // Last segment can't end with dot\n if (i === segments.length - 1 && segment.endsWith('.')) {\n return false;\n }\n\n // No segment should have consecutive dots\n if (segment.includes('..')) {\n return false;\n }\n }\n\n return true;\n}\nundefined?.test(\"isValidHostnameWithWildcards\", ({ expect }) => {\n // Test with valid regular hostnames\n expect(isValidHostnameWithWildcards(\"example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"localhost\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"sub.domain.example.com\")).toBe(true);\n\n // Test with valid wildcard hostnames\n expect(isValidHostnameWithWildcards(\"*.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"a-*.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"*.*.org\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"**.example.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"sub.**.com\")).toBe(true);\n expect(isValidHostnameWithWildcards(\"*-api.*.com\")).toBe(true);\n\n // Test with invalid hostnames\n expect(isValidHostnameWithWildcards(\"\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example.com/path\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"https://example.com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\".example.com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example.com.\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"example..com\")).toBe(false);\n expect(isValidHostnameWithWildcards(\"*.example..com\")).toBe(false);\n});\n\nexport function matchHostnamePattern(pattern: string, hostname: string): boolean {\n // If no wildcards, it's an exact match\n if (!pattern.includes('*')) {\n return pattern === hostname;\n }\n\n // Convert the pattern to a regex\n // First, escape all regex special characters except *\n let regexPattern = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Use a placeholder for ** to handle it separately from single *\n const doubleWildcardPlaceholder = '\\x00DOUBLE_WILDCARD\\x00';\n regexPattern = regexPattern.replace(/\\*\\*/g, doubleWildcardPlaceholder);\n\n // Replace single * with a pattern that matches anything except dots\n regexPattern = regexPattern.replace(/\\*/g, '[^.]*');\n\n // Replace the double wildcard placeholder with a pattern that matches anything including dots\n regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');\n\n // Anchor the pattern to match the entire hostname\n regexPattern = '^' + regexPattern + '$';\n\n try {\n const regex = new RegExp(regexPattern);\n return regex.test(hostname);\n } catch {\n return false;\n }\n}\nundefined?.test(\"matchHostnamePattern\", ({ expect }) => {\n // Test exact matches\n expect(matchHostnamePattern(\"example.com\", \"example.com\")).toBe(true);\n expect(matchHostnamePattern(\"example.com\", \"other.com\")).toBe(false);\n\n // Test single wildcard matches\n expect(matchHostnamePattern(\"*.example.com\", \"api.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"*.example.com\", \"www.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"*.example.com\", \"example.com\")).toBe(false);\n expect(matchHostnamePattern(\"*.example.com\", \"api.v2.example.com\")).toBe(false);\n\n // Test double wildcard matches\n expect(matchHostnamePattern(\"**.example.com\", \"api.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"api.v2.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"a.b.c.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"**.example.com\", \"example.com\")).toBe(false);\n\n // Test complex patterns\n expect(matchHostnamePattern(\"api-*.example.com\", \"api-v1.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"api-*.example.com\", \"api-v2.example.com\")).toBe(true);\n expect(matchHostnamePattern(\"api-*.example.com\", \"api.example.com\")).toBe(false);\n expect(matchHostnamePattern(\"*.*.org\", \"mail.example.org\")).toBe(true);\n expect(matchHostnamePattern(\"*.*.org\", \"example.org\")).toBe(false);\n});\n\nexport function isLocalhost(urlOrString: string | URL) {\n const url = createUrlIfValid(urlOrString);\n if (!url) return false;\n if (url.hostname === \"localhost\" || url.hostname.endsWith(\".localhost\")) return true;\n if (url.hostname.match(/^127\\.\\d+\\.\\d+\\.\\d+$/)) return true;\n return false;\n}\nundefined?.test(\"isLocalhost\", ({ expect }) => {\n // Test with localhost URLs\n expect(isLocalhost(\"http://localhost\")).toBe(true);\n expect(isLocalhost(\"https://localhost:8080\")).toBe(true);\n expect(isLocalhost(\"http://sub.localhost\")).toBe(true);\n expect(isLocalhost(\"http://127.0.0.1\")).toBe(true);\n expect(isLocalhost(\"http://127.1.2.3\")).toBe(true);\n\n // Test with non-localhost URLs\n expect(isLocalhost(\"https://example.com\")).toBe(false);\n expect(isLocalhost(\"http://192.168.1.1\")).toBe(false);\n expect(isLocalhost(\"http://10.0.0.1\")).toBe(false);\n\n // Test with URL objects\n expect(isLocalhost(new URL(\"http://localhost\"))).toBe(true);\n expect(isLocalhost(new URL(\"https://example.com\"))).toBe(false);\n\n // Test with invalid URLs\n expect(isLocalhost(\"not a url\")).toBe(false);\n expect(isLocalhost(\"\")).toBe(false);\n});\n\nexport function isRelative(url: string) {\n const randomDomain = `${generateSecureRandomString()}.stack-auth.example.com`;\n const u = createUrlIfValid(url, `https://${randomDomain}`);\n if (!u) return false;\n if (u.host !== randomDomain) return false;\n if (u.protocol !== \"https:\") return false;\n return true;\n}\nundefined?.test(\"isRelative\", ({ expect }) => {\n // We can't easily mock generateSecureRandomString in this context\n // but we can still test the function's behavior\n\n // Test with relative URLs\n expect(isRelative(\"/\")).toBe(true);\n expect(isRelative(\"/path\")).toBe(true);\n expect(isRelative(\"/path?query=value#hash\")).toBe(true);\n\n // Test with absolute URLs\n expect(isRelative(\"https://example.com\")).toBe(false);\n expect(isRelative(\"http://example.com\")).toBe(false);\n expect(isRelative(\"//example.com\")).toBe(false);\n\n // Note: The implementation treats empty strings and invalid URLs as relative\n // This is because they can be resolved against a base URL\n expect(isRelative(\"\")).toBe(true);\n expect(isRelative(\"not a url\")).toBe(true);\n});\n\nexport function getRelativePart(url: URL) {\n return url.pathname + url.search + url.hash;\n}\nundefined?.test(\"getRelativePart\", ({ expect }) => {\n // Test with various URLs\n expect(getRelativePart(new URL(\"https://example.com\"))).toBe(\"/\");\n expect(getRelativePart(new URL(\"https://example.com/path\"))).toBe(\"/path\");\n expect(getRelativePart(new URL(\"https://example.com/path?query=value\"))).toBe(\"/path?query=value\");\n expect(getRelativePart(new URL(\"https://example.com/path#hash\"))).toBe(\"/path#hash\");\n expect(getRelativePart(new URL(\"https://example.com/path?query=value#hash\"))).toBe(\"/path?query=value#hash\");\n\n // Test with different domains but same paths\n const url1 = new URL(\"https://example.com/path?query=value#hash\");\n const url2 = new URL(\"https://different.com/path?query=value#hash\");\n expect(getRelativePart(url1)).toBe(getRelativePart(url2));\n});\n\n/**\n * A template literal tag that returns a URL.\n *\n * Any values passed are encoded.\n */\nexport function url(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): URL {\n return new URL(urlString(strings, ...values));\n}\nundefined?.test(\"url\", ({ expect }) => {\n // Test with no interpolation\n expect(url`https://example.com`).toBeInstanceOf(URL);\n expect(url`https://example.com`.href).toBe(\"https://example.com/\");\n\n // Test with string interpolation\n expect(url`https://example.com/${\"path\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path\"}`.pathname).toBe(\"/path\");\n\n // Test with number interpolation\n expect(url`https://example.com/${42}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${42}`.pathname).toBe(\"/42\");\n\n // Test with boolean interpolation\n expect(url`https://example.com/${true}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${true}`.pathname).toBe(\"/true\");\n\n // Test with special characters in interpolation\n expect(url`https://example.com/${\"path with spaces\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path with spaces\"}`.pathname).toBe(\"/path%20with%20spaces\");\n\n // Test with multiple interpolations\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`).toBeInstanceOf(URL);\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`.pathname).toBe(\"/path\");\n expect(url`https://example.com/${\"path\"}?query=${\"value\"}`.search).toBe(\"?query=value\");\n});\n\n\n/**\n * A template literal tag that returns a URL string.\n *\n * Any values passed are encoded.\n */\nexport function urlString(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): string {\n return templateIdentity(strings, ...values.map(encodeURIComponent));\n}\nundefined?.test(\"urlString\", ({ expect }) => {\n // Test with no interpolation\n expect(urlString`https://example.com`).toBe(\"https://example.com\");\n\n // Test with string interpolation\n expect(urlString`https://example.com/${\"path\"}`).toBe(\"https://example.com/path\");\n\n // Test with number interpolation\n expect(urlString`https://example.com/${42}`).toBe(\"https://example.com/42\");\n\n // Test with boolean interpolation\n expect(urlString`https://example.com/${true}`).toBe(\"https://example.com/true\");\n\n // Test with special characters in interpolation\n expect(urlString`https://example.com/${\"path with spaces\"}`).toBe(\"https://example.com/path%20with%20spaces\");\n expect(urlString`https://example.com/${\"?&=\"}`).toBe(\"https://example.com/%3F%26%3D\");\n\n // Test with multiple interpolations\n expect(urlString`https://example.com/${\"path\"}?query=${\"value\"}`).toBe(\"https://example.com/path?query=value\");\n expect(urlString`https://example.com/${\"path\"}?query=${\"value with spaces\"}`).toBe(\"https://example.com/path?query=value%20with%20spaces\");\n});\n\nexport function isChildUrl(parentUrl: URL, maybeChildUrl: URL) {\n return parentUrl.origin === maybeChildUrl.origin\n && isChildPath(parentUrl.pathname, maybeChildUrl.pathname)\n && [...parentUrl.searchParams].every(([key, value]) => maybeChildUrl.searchParams.get(key) === value)\n && (!parentUrl.hash || parentUrl.hash === maybeChildUrl.hash);\n}\nundefined?.test(\"isChildUrl\", ({ expect }) => {\n // true\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path?query=value\"))).toBe(true);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://abc.com/path?query=value#hash\"))).toBe(true);\n\n // false\n expect(isChildUrl(new URL(\"https://abc.com\"), new URL(\"https://example.com\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path?query=value\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://abc.com/\"), new URL(\"https://example.com/path?query=value#hash\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://example.com\"), new URL(\"https://abc.com/path?query=value#hash\"))).toBe(false);\n expect(isChildUrl(new URL(\"https://example.com?query=value123\"), new URL(\"https://example.com/path?query=value#hash\"))).toBe(false);\n});\n\nexport function isChildPath(parentPath: string, maybeChildPath: string) {\n parentPath = parentPath.endsWith(\"/\") ? parentPath : parentPath + \"/\";\n maybeChildPath = maybeChildPath.endsWith(\"/\") ? maybeChildPath : maybeChildPath + \"/\";\n return maybeChildPath.startsWith(parentPath);\n}\nundefined?.test(\"isSubPath\", ({ expect }) => {\n expect(isChildPath(\"/\", \"/\")).toBe(true);\n expect(isChildPath(\"/\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path\", \"/\")).toBe(false);\n expect(isChildPath(\"/path\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path/\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path/\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path/abc\")).toBe(true);\n expect(isChildPath(\"/path/\", \"/path/abc\")).toBe(true);\n expect(isChildPath(\"/path\", \"/path-abc\")).toBe(false);\n expect(isChildPath(\"/path\", \"/path-abc/\")).toBe(false);\n expect(isChildPath(\"/path/\", \"/path-abc\")).toBe(false);\n expect(isChildPath(\"/path/\", \"/path-abc/\")).toBe(false);\n});\n"],"mappings":"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","names":["url"]}
@@ -140,6 +140,12 @@ declare class StackAdminInterface extends StackServerInterface {
140
140
  success: boolean;
141
141
  error_message?: string;
142
142
  }>;
143
+ sendTestWebhook(data: {
144
+ endpoint_id: string;
145
+ }): Promise<{
146
+ success: boolean;
147
+ error_message?: string;
148
+ }>;
143
149
  listSentEmails(): Promise<InternalEmailsCrud["Admin"]["List"]>;
144
150
  sendSignInInvitationEmail(email: string, callbackUrl: string): Promise<void>;
145
151
  sendChatMessage(threadId: string, contextType: "email-theme" | "email-template" | "email-draft", messages: Array<{
@@ -140,6 +140,12 @@ declare class StackAdminInterface extends StackServerInterface {
140
140
  success: boolean;
141
141
  error_message?: string;
142
142
  }>;
143
+ sendTestWebhook(data: {
144
+ endpoint_id: string;
145
+ }): Promise<{
146
+ success: boolean;
147
+ error_message?: string;
148
+ }>;
143
149
  listSentEmails(): Promise<InternalEmailsCrud["Admin"]["List"]>;
144
150
  sendSignInInvitationEmail(email: string, callbackUrl: string): Promise<void>;
145
151
  sendChatMessage(threadId: string, contextType: "email-theme" | "email-template" | "email-draft", messages: Array<{
@@ -290,6 +290,16 @@ var StackAdminInterface = class extends import_server_interface.StackServerInter
290
290
  }, null);
291
291
  return await response.json();
292
292
  }
293
+ async sendTestWebhook(data) {
294
+ const response = await this.sendAdminRequest(`/internal/send-test-webhook`, {
295
+ method: "POST",
296
+ headers: {
297
+ "content-type": "application/json"
298
+ },
299
+ body: JSON.stringify(data)
300
+ }, null);
301
+ return await response.json();
302
+ }
293
303
  async listSentEmails() {
294
304
  const response = await this.sendAdminRequest("/internal/emails", {
295
305
  method: "GET"