Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions apps/kitchensink-react/public/sanity-sdk-shared-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* SharedWorker script for kitchensink app testing.
* Performs fetch based on incoming request payloads and posts result back.
*/

// eslint-disable-next-line no-restricted-globals
onconnect = function (event) {
var port = event.ports[0]

port.onmessage = async function (e) {
var data = e.data || {}
var id = data.id
var req = data.request || {}
var context = req.context || {}
var url = context.options.url
var method = context.options.method || 'GET'
var headers = context.options.headers || {}
var body = context.options.body
console.log('context', context, url, method, headers, body)

try {
// eslint-disable-next-line no-console
console.log('[sanity] shared worker received', {
id: id,
url: url,
method: method,
hasContext: !!req.context,
})
} catch (err) {}

// If no url provided, just log the context for debugging
if (!url) {
try {
// eslint-disable-next-line no-console
console.log('[sanity] No url provided. Shared worker context', req.context)
} catch (err) {
// eslint-disable-next-line no-console
console.warn('[sanity] shared worker log failed', err)
}
return
}

try {
var fetchOptions = {method: method, headers: headers, mode: 'cors', redirect: 'follow'}
if (req && typeof req.credentials === 'string') {
fetchOptions.credentials = req.credentials
}
if (body != null) {
var lowerHeaderKeys = {}
try {
for (var k in headers) {
if (Object.prototype.hasOwnProperty.call(headers, k)) {
lowerHeaderKeys[String(k).toLowerCase()] = headers[k]
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.log('error parsing headers', err)
}

var isPlainObject =
typeof body === 'object' &&
body !== null &&
!(body instanceof ArrayBuffer) &&
!(body instanceof Blob) &&
!(body instanceof FormData) &&
!(body instanceof URLSearchParams)

if (isPlainObject) {
if (!('content-type' in lowerHeaderKeys)) {
fetchOptions.headers = Object.assign({}, headers, {'content-type': 'application/json'})
}
fetchOptions.body = JSON.stringify(body)
} else {
fetchOptions.body = body
}
}

// eslint-disable-next-line no-console
console.log('fetching', url, fetchOptions)

var res = await fetch(url, fetchOptions)
console.log('res', res)
var resHeadersObj = {}
try {
res.headers.forEach(function (value, key) {
resHeadersObj[key] = value
})
} catch (err) {}

var contentType = (res.headers && res.headers.get && res.headers.get('content-type')) || ''
var responseBody = null
try {
if (typeof contentType === 'string' && contentType.indexOf('application/json') !== -1) {
try {
responseBody = await res.json()
} catch (err) {
responseBody = await res.text()
}
} else {
responseBody = await res.text()
}
} catch (err) {
responseBody = null
}

// Mark responses as coming from the shared worker
try {
resHeadersObj['x-from-shared-worker'] = '1'
} catch (err) {}

var response = {
url: url,
method: method,
headers: resHeadersObj || {},
body: responseBody,
statusCode: res.status,
statusMessage: res.statusText,
}

if (id) {
try {
// eslint-disable-next-line no-console
console.log('[sanity] shared worker responding', {id: id, status: response.statusCode})
} catch (err) {}
port.postMessage({type: 'interceptResponse', id: id, response: response})
} else {
// eslint-disable-next-line no-console
console.log('[sanity] shared worker fetch response', response)
port.postMessage({type: 'interceptResponse', response: response})
}
} catch (err) {
// eslint-disable-next-line no-console
console.log('error', err)
if (id) {
port.postMessage({
type: 'interceptResponse',
id: id,
response: null,
error: (err && err.message) || String(err),
})
} else {
// eslint-disable-next-line no-console
console.warn('[sanity] shared worker fetch failed', err)
}
}
}

port.start()
}
2 changes: 2 additions & 0 deletions packages/core/src/auth/handleAuthCallback.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {sharedWorkerInterceptor} from '../client/sharedWorkerInterceptor'
import {bindActionGlobally} from '../store/createActionBinder'
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
import {AuthStateType} from './authStateType'
Expand Down Expand Up @@ -67,6 +68,7 @@ export const handleAuthCallback = bindActionGlobally(
useProjectHostname: false,
useCdn: false,
...(apiHost && {apiHost}),
requester: sharedWorkerInterceptor,
})

const {token} = await client.request<{token: string; label: string}>({
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/auth/refreshStampedToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
timer,
} from 'rxjs'

import {sharedWorkerInterceptor} from '../client/sharedWorkerInterceptor'
import {type StoreContext} from '../store/defineStore'
import {DEFAULT_API_VERSION} from './authConstants'
import {AuthStateType} from './authStateType'
Expand Down Expand Up @@ -64,6 +65,7 @@ function createTokenRefreshStream(
token,
ignoreBrowserTokenWarning: true,
...(apiHost && {apiHost}),
requester: sharedWorkerInterceptor,
})

const subscription = client.observable
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/auth/studioModeAuth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {type ClientConfig, type SanityClient} from '@sanity/client'

import {sharedWorkerInterceptor} from '../client/sharedWorkerInterceptor'
import {getTokenFromStorage} from './utils'

/**
Expand All @@ -18,6 +19,7 @@ export async function checkForCookieAuth(
const client = clientFactory({
projectId,
useCdn: false,
requester: sharedWorkerInterceptor,
})
const user = await client.request({
uri: '/users/me',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type CurrentUser} from '@sanity/types'
import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'rxjs'

import {sharedWorkerInterceptor} from '../client/sharedWorkerInterceptor'
import {type StoreContext} from '../store/defineStore'
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
import {AuthStateType} from './authStateType'
Expand Down Expand Up @@ -31,6 +32,7 @@ export const subscribeToStateAndFetchCurrentUser = ({
useProjectHostname: false,
useCdn: false,
...(apiHost && {apiHost}),
requester: sharedWorkerInterceptor,
}),
),
switchMap((client) =>
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/client/clientStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {getAuthMethodState, getTokenState} from '../auth/authStore'
import {bindActionGlobally} from '../store/createActionBinder'
import {createStateSourceAction} from '../store/createStateSourceAction'
import {defineStore, type StoreContext} from '../store/defineStore'
import {sharedWorkerInterceptor} from './sharedWorkerInterceptor'

const DEFAULT_API_VERSION = '2024-11-12'
const DEFAULT_REQUEST_TAG_PREFIX = 'sanity.sdk'
Expand Down Expand Up @@ -160,14 +161,15 @@ export const getClient = bindActionGlobally(
const dataset = options.dataset ?? instance.config.dataset
const apiHost = options.apiHost ?? instance.config.auth?.apiHost

const effectiveOptions: ClientOptions = {
const effectiveOptions: ClientConfig = {
...DEFAULT_CLIENT_CONFIG,
...((options.scope === 'global' || !projectId) && {useProjectHostname: false}),
token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
...options,
...(projectId && {projectId}),
...(dataset && {dataset}),
...(apiHost && {apiHost}),
requester: sharedWorkerInterceptor,
}

if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {
Expand All @@ -179,11 +181,11 @@ export const getClient = bindActionGlobally(
delete effectiveOptions.withCredentials
}

const key = getClientConfigKey(effectiveOptions)
const key = getClientConfigKey(effectiveOptions as ClientOptions)

if (clients[key]) return clients[key]

const client = createClient(effectiveOptions)
const client = createClient({...effectiveOptions})
state.set('addClient', (prev) => ({clients: {...prev.clients, [key]: client}}))

return client
Expand Down
48 changes: 48 additions & 0 deletions packages/core/src/client/networkTimingInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {requester as baseSanityRequester} from '@sanity/client'

// Track request timings per middleware context
const requestTimings = new WeakMap<object, number>()

// Clone the base requester and attach middleware using hook event signatures
export const networkTimingInterceptor = baseSanityRequester.clone().use({
interceptRequest: (prev, {context}) => {
requestTimings.set(context, performance.now())
// eslint-disable-next-line no-console
console.log('[sanity] interceptRequest context', context)

// return {
// url,
// method,
// body: [{ displayName: "intercepted", id: "123" }],
// headers: {},
// statusCode: 200,
// statusMessage: "OK",
// };
return prev
},
onError: (error, context) => {
const start = requestTimings.get(context)
if (typeof start === 'number') {
const ms = performance.now() - start
// eslint-disable-next-line no-console
console.log(`[sanity] timing error: ${ms.toFixed(1)}ms`)
requestTimings.delete(context)
}
return error
},
onResponse: (response, context) => {
const start = requestTimings.get(context)
let ms = 0
if (typeof start === 'number') {
ms = performance.now() - start
// eslint-disable-next-line no-console
console.log(`[sanity] timing: ${ms.toFixed(1)}ms ${response.url} ${response.method}`)
requestTimings.delete(context)
}
response.headers = {
...response.headers,
'x-middleware-timing': `${ms.toFixed(1)}ms`,
}
return response
},
})
Loading
Loading