From 134fa4c18341a0c263210b33e54c7ff9fa34b7a3 Mon Sep 17 00:00:00 2001
From: Carolina Gonzalez
Date: Wed, 13 Aug 2025 13:49:16 -0400
Subject: [PATCH 01/10] feat(apps): scaffold experimentation apps
---
apps/dashboard/src/App.tsx | 13 +++++--------
apps/sdk-app/src/App.tsx | 19 +++++++++++++------
2 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 4c8ac0dbc..6c20eb758 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -1,8 +1,8 @@
+import {createSubscriptionRequest, registerSubscription, unregisterSubscription} from '@sanity/sdk'
import {SanityApp, SanityConfig, useFrameConnection} from '@sanity/sdk-react'
import {Spinner, ThemeProvider} from '@sanity/ui'
import {buildTheme} from '@sanity/ui/theme'
-import {type JSX, Suspense, useState, useEffect, useRef, useCallback} from 'react'
-import {registerSubscription, unregisterSubscription, createSubscriptionRequest} from '@sanity/sdk'
+import {type JSX, Suspense, useCallback, useEffect, useRef, useState} from 'react'
const theme = buildTheme({})
@@ -57,7 +57,7 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject {
console.log('[Dashboard] Received query request:', data)
-
+
try {
// Create a subscription request from the incoming query data
const subscription = createSubscriptionRequest({
@@ -93,10 +93,7 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject({
+ const {connect} = useFrameConnection({
name: 'dashboard',
connectTo: 'sdk-app',
targetOrigin: '*',
@@ -132,7 +129,7 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject({
name: 'sdk-app',
@@ -66,7 +65,15 @@ function QueryTest() {
Data:
-
+
{/* {JSON.stringify(data, null, 2)} */}
@@ -81,9 +88,9 @@ export default function App(): JSX.Element {
return (
} config={devConfigs}>
-
-
-
+
+
+
)
From 3a3ec7ac60c19002c39437c41fdf6a13f5d97485 Mon Sep 17 00:00:00 2001
From: Carolina Gonzalez
Date: Thu, 14 Aug 2025 17:41:59 -0400
Subject: [PATCH 02/10] feat: add service worker store and script
---
.../core/src/sharedWorkerStore/sharedWorkerStore.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/sharedWorkerStore/sharedWorkerStore.ts b/packages/core/src/sharedWorkerStore/sharedWorkerStore.ts
index 449a3023c..084326747 100644
--- a/packages/core/src/sharedWorkerStore/sharedWorkerStore.ts
+++ b/packages/core/src/sharedWorkerStore/sharedWorkerStore.ts
@@ -23,16 +23,19 @@ export const sharedWorkerStore = createStore {
const state = get()
-
+
// Check if we already have an equivalent subscription
const existingSubscriptions = Array.from(state.subscriptions.values())
- const equivalentSubscription = existingSubscriptions.find(existing =>
- areSubscriptionsEquivalent(existing, subscription)
+ const equivalentSubscription = existingSubscriptions.find((existing) =>
+ areSubscriptionsEquivalent(existing, subscription),
)
if (equivalentSubscription) {
// Return the existing subscription ID instead of creating a new one
- console.log('[SharedWorkerStore] Found equivalent subscription, reusing:', equivalentSubscription.subscriptionId)
+ console.log(
+ '[SharedWorkerStore] Found equivalent subscription, reusing:',
+ equivalentSubscription.subscriptionId,
+ )
return equivalentSubscription.subscriptionId
}
From 0e1f9c1c5e38fe048149f6c00a27914cd083805f Mon Sep 17 00:00:00 2001
From: Carolina Gonzalez
Date: Tue, 19 Aug 2025 08:34:06 +0300
Subject: [PATCH 03/10] feat(core): add connection to query store
---
apps/dashboard/src/App.tsx | 16 +++-
packages/core/src/query/queryStore.ts | 105 +++++++++++++-------------
2 files changed, 65 insertions(+), 56 deletions(-)
diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 6c20eb758..fef183c64 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -57,7 +57,6 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject {
console.log('[Dashboard] Received query request:', data)
-
try {
// Create a subscription request from the incoming query data
const subscription = createSubscriptionRequest({
@@ -129,7 +128,6 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject(null)
+
return (
} config={devConfigs}>
@@ -224,16 +224,28 @@ export default function App(): JSX.Element {
inset: 0,
display: 'flex',
flexDirection: 'column',
+ height: '100vh',
+ width: '100vw',
}}
>
}
>
diff --git a/apps/kitchensink-react/src/sanityConfigs.ts b/apps/kitchensink-react/src/sanityConfigs.ts
index 998eaa007..d617833cc 100644
--- a/apps/kitchensink-react/src/sanityConfigs.ts
+++ b/apps/kitchensink-react/src/sanityConfigs.ts
@@ -6,11 +6,7 @@ export const devConfigs: SanityConfig[] = [
dataset: 'test',
},
{
- projectId: 'd45jg133',
- dataset: 'production',
- },
- {
- projectId: 'v28v5k8m',
+ projectId: 'vo1ysemo',
dataset: 'production',
},
]
From 40d965bfdc38a3443042fa4fcecb69b050e3a360 Mon Sep 17 00:00:00 2001
From: Ryan Bonial
Date: Thu, 11 Sep 2025 20:46:46 -0600
Subject: [PATCH 09/10] chore: fix QUERY_STORE_DEFAULT_PERSPECTIVE
---
packages/core/src/query/queryStore.ts | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/packages/core/src/query/queryStore.ts b/packages/core/src/query/queryStore.ts
index d4f9712b7..b1bb5a2d2 100644
--- a/packages/core/src/query/queryStore.ts
+++ b/packages/core/src/query/queryStore.ts
@@ -37,7 +37,11 @@ import {
import {type StoreState} from '../store/createStoreState'
import {defineStore, type StoreContext} from '../store/defineStore'
import {insecureRandomId} from '../utils/ids'
-import {QUERY_STATE_CLEAR_DELAY, QUERY_STORE_API_VERSION} from './queryStoreConstants'
+import {
+ QUERY_STATE_CLEAR_DELAY,
+ QUERY_STORE_API_VERSION,
+ QUERY_STORE_DEFAULT_PERSPECTIVE,
+} from './queryStoreConstants'
import {
addSubscriber,
cancelQuery,
@@ -96,7 +100,8 @@ function normalizeOptionsWithPerspective(
const instancePerspective = instance.config.perspective
return {
...options,
- perspective: instancePerspective !== undefined ? instancePerspective : 'published',
+ perspective:
+ instancePerspective !== undefined ? instancePerspective : QUERY_STORE_DEFAULT_PERSPECTIVE,
}
}
From 59970d59b05f87e2f758bcae4c1fd5702edc4b73 Mon Sep 17 00:00:00 2001
From: Ryan Bonial
Date: Fri, 12 Sep 2025 09:23:24 -0600
Subject: [PATCH 10/10] chore: debugging
---
apps/dashboard/src/App.tsx | 1 +
apps/kitchensink-react/package.json | 2 +-
apps/kitchensink-react/src/main.tsx | 42 ++++++++++++
packages/core/src/_exports/worker.ts | 64 +++++++++++++------
.../core/src/comlink/node/getNodeState.ts | 8 ++-
packages/core/src/query/queryStore.ts | 13 +++-
.../sharedWorkerStore/sharedWorkerClient.ts | 31 ++++++---
packages/react/package.json | 5 ++
packages/react/src/_exports/worker.ts | 3 +
9 files changed, 135 insertions(+), 34 deletions(-)
create mode 100644 packages/react/src/_exports/worker.ts
diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 3c1f852e2..aac6fa8d5 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -76,6 +76,7 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject {
+ // eslint-disable-next-line no-console
+ console.log('[Kitchensink] Worker status changed:', status)
+ })
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn('Failed to initialize SharedWorker:', error)
+ // Fallback to local subscription management
+ }
+}
+
+// Initialize SharedWorker when the app starts
+// eslint-disable-next-line no-console
+console.log('[Kitchensink] Initializing SharedWorker...')
+initializeSharedWorker()
+
createRoot(document.getElementById('root')!).render(
diff --git a/packages/core/src/_exports/worker.ts b/packages/core/src/_exports/worker.ts
index f00650f88..d4212a9eb 100644
--- a/packages/core/src/_exports/worker.ts
+++ b/packages/core/src/_exports/worker.ts
@@ -39,36 +39,45 @@ self.onconnect = (event: MessageEvent) => {
// Set up message handling for this port
port.onmessage = async (e: MessageEvent) => {
- const {type, data} = e.data
+ const {type, data, messageId} = e.data as {type: string; data: unknown; messageId?: string}
+ console.log('[SharedWorker] port.onmessage', {type, messageId, data})
console.log('[SharedWorker] Received message:', type, data)
try {
switch (type) {
case 'REGISTER_SUBSCRIPTION':
- handleRegisterSubscription(data, port)
+ console.log('[SharedWorker] Registering subscription:', {messageId, data})
+ handleRegisterSubscription(data as SubscriptionRequest, port, messageId)
break
case 'UNREGISTER_SUBSCRIPTION':
- handleUnregisterSubscription(data.subscriptionId, port)
+ console.log('[SharedWorker] Unregistering subscription:', {messageId, data})
+ handleUnregisterSubscription(
+ (data as {subscriptionId: string}).subscriptionId,
+ port,
+ messageId,
+ )
break
case 'GET_SUBSCRIPTION_COUNT':
- handleGetSubscriptionCount(port)
+ console.log('[SharedWorker] Getting subscription count', {messageId})
+ handleGetSubscriptionCount(port, messageId)
break
case 'GET_ALL_SUBSCRIPTIONS':
- handleGetAllSubscriptions(port)
+ console.log('[SharedWorker] Getting all subscriptions', {messageId})
+ handleGetAllSubscriptions(port, messageId)
break
default:
console.warn('[SharedWorker] Unknown message type:', type)
port.postMessage({
type: 'ERROR',
- data: {error: `Unknown message type: ${type}`},
+ data: {error: `Unknown message type: ${type}`, messageId},
})
}
} catch (error) {
console.error('[SharedWorker] Error handling message:', error)
port.postMessage({
type: 'ERROR',
- data: {error: (error as Error).message},
+ data: {error: (error as Error).message, messageId},
})
}
}
@@ -85,19 +94,23 @@ self.onconnect = (event: MessageEvent) => {
* @param subscription - The subscription to register
* @param port - The port to send the response to
*/
-function handleRegisterSubscription(subscription: SubscriptionRequest, port: MessagePort): void {
+function handleRegisterSubscription(
+ subscription: SubscriptionRequest,
+ port: MessagePort,
+ messageId?: string,
+): void {
try {
// Register the subscription in the store
sharedWorkerStore.getState().registerSubscription(subscription)
// Check if we need to execute a query for this subscription
if (subscription.storeName === 'query' && subscription.params?.['query']) {
- handleQuerySubscription(subscription, port)
+ handleQuerySubscription(subscription, port, messageId)
} else {
// For non-query subscriptions, just confirm registration
port.postMessage({
type: 'SUBSCRIPTION_REGISTERED',
- data: {subscriptionId: subscription.subscriptionId},
+ data: {subscriptionId: subscription.subscriptionId, messageId},
})
}
@@ -108,7 +121,11 @@ function handleRegisterSubscription(subscription: SubscriptionRequest, port: Mes
// Send error back to the client
port.postMessage({
type: 'SUBSCRIPTION_ERROR',
- data: {error: (error as Error).message, subscriptionId: subscription.subscriptionId},
+ data: {
+ error: (error as Error).message,
+ subscriptionId: subscription.subscriptionId,
+ messageId,
+ },
})
}
}
@@ -120,6 +137,7 @@ function handleRegisterSubscription(subscription: SubscriptionRequest, port: Mes
async function handleQuerySubscription(
subscription: SubscriptionRequest,
port: MessagePort,
+ messageId?: string,
): Promise {
const cacheKey = getCacheKey(subscription)
@@ -159,6 +177,7 @@ async function handleQuerySubscription(
data: {
error: `Query execution failed: ${(error as Error).message}`,
subscriptionId: subscription.subscriptionId,
+ messageId,
},
})
return
@@ -176,6 +195,7 @@ async function handleQuerySubscription(
result: cacheEntry.result,
cached: cacheEntry.timestamp !== Date.now(),
cacheKey,
+ messageId,
},
})
@@ -188,14 +208,18 @@ async function handleQuerySubscription(
* @param subscriptionId - The ID of the subscription to unregister
* @param port - The port to send the response to
*/
-function handleUnregisterSubscription(subscriptionId: string, port: MessagePort): void {
+function handleUnregisterSubscription(
+ subscriptionId: string,
+ port: MessagePort,
+ messageId?: string,
+): void {
try {
sharedWorkerStore.getState().unregisterSubscription(subscriptionId)
// Send confirmation back to the client
port.postMessage({
type: 'SUBSCRIPTION_UNREGISTERED',
- data: {subscriptionId},
+ data: {subscriptionId, messageId},
})
console.log('[SharedWorker] Unregistered subscription:', subscriptionId)
@@ -205,43 +229,43 @@ function handleUnregisterSubscription(subscriptionId: string, port: MessagePort)
// Send error back to the client
port.postMessage({
type: 'SUBSCRIPTION_ERROR',
- data: {error: (error as Error).message, subscriptionId},
+ data: {error: (error as Error).message, subscriptionId, messageId},
})
}
}
-function handleGetSubscriptionCount(port: MessagePort): void {
+function handleGetSubscriptionCount(port: MessagePort, messageId?: string): void {
try {
const count = sharedWorkerStore.getState().getSubscriptionCount()
port.postMessage({
type: 'SUBSCRIPTION_COUNT',
- data: {count},
+ data: {count, messageId},
})
} catch (error) {
console.error('[SharedWorker] Failed to get subscription count:', error)
port.postMessage({
type: 'SUBSCRIPTION_ERROR',
- data: {error: (error as Error).message},
+ data: {error: (error as Error).message, messageId},
})
}
}
-function handleGetAllSubscriptions(port: MessagePort): void {
+function handleGetAllSubscriptions(port: MessagePort, messageId?: string): void {
try {
const subscriptions = sharedWorkerStore.getState().getAllSubscriptions()
port.postMessage({
type: 'ALL_SUBSCRIPTIONS',
- data: {subscriptions},
+ data: {subscriptions, messageId},
})
} catch (error) {
console.error('[SharedWorker] Failed to get all subscriptions:', error)
port.postMessage({
type: 'SUBSCRIPTION_ERROR',
- data: {error: (error as Error).message},
+ data: {error: (error as Error).message, messageId},
})
}
}
diff --git a/packages/core/src/comlink/node/getNodeState.ts b/packages/core/src/comlink/node/getNodeState.ts
index 56df1c4d9..3b5e5f35f 100644
--- a/packages/core/src/comlink/node/getNodeState.ts
+++ b/packages/core/src/comlink/node/getNodeState.ts
@@ -36,6 +36,8 @@ export const getNodeState = bindActionGlobally(
comlinkNodeStore,
createStateSourceAction({
selector: createSelector([selectNode], (nodeEntry) => {
+ // eslint-disable-next-line no-console
+ console.log('[Comlink] selectNode entry:', nodeEntry)
return nodeEntry?.status === 'connected'
? {
node: nodeEntry.node,
@@ -46,7 +48,11 @@ export const getNodeState = bindActionGlobally(
onSubscribe: ({state, instance}, nodeInput) => {
const nodeName = nodeInput.name
const subscriberId = Symbol('comlink-node-subscriber')
- getOrCreateNode(instance, nodeInput)
+ // eslint-disable-next-line no-console
+ console.log('[Comlink] onSubscribe getOrCreateNode:', nodeInput)
+ const node = getOrCreateNode(instance, nodeInput)
+ // eslint-disable-next-line no-console
+ console.log('[Comlink] node.start invoked; node:', node)
// Add subscriber to the set for this node
let subs = state.get().subscriptions.get(nodeName)
diff --git a/packages/core/src/query/queryStore.ts b/packages/core/src/query/queryStore.ts
index b1bb5a2d2..6e2ee802e 100644
--- a/packages/core/src/query/queryStore.ts
+++ b/packages/core/src/query/queryStore.ts
@@ -129,8 +129,9 @@ function forwardQueryToDashboard(
// Get the node state for communicating with Dashboard
const nodeStateSource = getNodeState(instance, {
- name: 'sdk-app',
- connectTo: 'dashboard',
+ // Align with the active comlink channel naming observed in logs
+ name: 'dashboard/nodes/sdk',
+ connectTo: 'dashboard/channels/sdk',
})
// Create a stable query key for this request
@@ -149,7 +150,9 @@ function forwardQueryToDashboard(
const subscription = nodeStateSource.observable
.pipe(
+ tap((nodeState) => console.log('[QueryStore] nodeState update:', nodeState)),
filter((nodeState) => !!nodeState && nodeState.status === 'connected'),
+ tap((nodeState) => console.log('[QueryStore] nodeState passed filter:', nodeState)),
switchMap((nodeState) => {
const node = nodeState!.node
@@ -166,6 +169,7 @@ function forwardQueryToDashboard(
},
) as Promise,
).pipe(
+ tap(() => console.log('[QueryStore] node.fetch promise resolved')),
map((response) => {
const typedResponse = response as {data?: unknown; error?: string}
console.log('[QueryStore] Received response from Dashboard:', typedResponse)
@@ -201,7 +205,9 @@ function forwardQueryToDashboard(
}
},
observable: nodeStateSource.observable.pipe(
+ tap((nodeState) => console.log('[QueryStore] (obs) nodeState update:', nodeState)),
filter((nodeState) => !!nodeState && nodeState.status === 'connected'),
+ tap((nodeState) => console.log('[QueryStore] (obs) nodeState passed filter:', nodeState)),
switchMap((nodeState) => {
const node = nodeState!.node
@@ -218,6 +224,7 @@ function forwardQueryToDashboard(
},
) as Promise,
).pipe(
+ tap(() => console.log('[QueryStore] (obs) node.fetch promise resolved')),
map((response) => {
const typedResponse = response as {data?: unknown; error?: string}
console.log('[QueryStore] Observable received response:', typedResponse)
@@ -490,6 +497,8 @@ const _resolveQuery = bindActionByDataset(
queryStore,
({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
+ // eslint-disable-next-line no-console
+ console.log('[QueryStore] isInDashboardContext:', isInDashboardContext())
// Check if we should forward this query to Dashboard
if (isInDashboardContext()) {
// eslint-disable-next-line no-console
diff --git a/packages/core/src/sharedWorkerStore/sharedWorkerClient.ts b/packages/core/src/sharedWorkerStore/sharedWorkerClient.ts
index d3d3f56ed..d193ef9ef 100644
--- a/packages/core/src/sharedWorkerStore/sharedWorkerClient.ts
+++ b/packages/core/src/sharedWorkerStore/sharedWorkerClient.ts
@@ -59,6 +59,7 @@ export function getSdkWorker(workerUrl: string): {status: WorkerStatus; sendMess
try {
const worker = new SharedWorker(workerUrl, {
type: 'module',
+ name: 'sanity-sdk-shared-worker',
})
console.log('[SharedWorkerClient] SharedWorker created successfully')
@@ -67,6 +68,11 @@ export function getSdkWorker(workerUrl: string): {status: WorkerStatus; sendMess
// Set up port message handling
worker.port.onmessage = (event) => {
console.log('[SharedWorkerClient] Received message from worker:', event.data)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const debugData = (event.data?.data ?? {}) as any
+ if (debugData?.messageId) {
+ console.log('[SharedWorkerClient] Response messageId:', debugData.messageId)
+ }
const response = event.data
// Handle connection status message
@@ -117,22 +123,23 @@ export function getSdkWorker(workerUrl: string): {status: WorkerStatus; sendMess
// Helper function to determine if a response matches a message
function isResponseForMessage(response: {type: string; data: unknown}, messageId: string): boolean {
- // Extract the message type from the messageId
- // messageId format: "REGISTER_SUBSCRIPTION_timestamp_random"
+ // Prefer strict correlation via echoed messageId when available
+ const responseData = response.data as {messageId?: string} | undefined
+ if (responseData && responseData.messageId) {
+ return responseData.messageId === messageId
+ }
+
+ // Fallback: match by message type if worker hasn't been updated to echo messageId
const parts = messageId.split('_')
const messageType = parts.slice(0, -2).join('_') // Remove timestamp and random parts
-
- // A bit hard-coded. We can pass in configuration later
const responseMap: Record = {
REGISTER_SUBSCRIPTION: ['SUBSCRIPTION_REGISTERED', 'SUBSCRIPTION_ERROR'],
UNREGISTER_SUBSCRIPTION: ['SUBSCRIPTION_UNREGISTERED', 'SUBSCRIPTION_ERROR'],
GET_SUBSCRIPTION_COUNT: ['SUBSCRIPTION_COUNT', 'SUBSCRIPTION_ERROR'],
GET_ALL_SUBSCRIPTIONS: ['ALL_SUBSCRIPTIONS', 'SUBSCRIPTION_ERROR'],
}
-
const expectedResponses = responseMap[messageType] || []
- const result = expectedResponses.includes(response.type)
- return result
+ return expectedResponses.includes(response.type)
}
// Process any buffered messages once connected
@@ -153,9 +160,8 @@ function sendMessageInternal(message: {type: string; data: unknown; messageId?:
return false
}
- // Remove messageId from the message sent to worker
- const {messageId: _, ...workerMessage} = message
- workerInstance.port.postMessage(workerMessage)
+ // Keep messageId so the worker can echo it back for correlation
+ workerInstance.port.postMessage(message)
return true
}
@@ -178,6 +184,7 @@ function handleMessage(message: {
setTimeout(() => {
if (messageHandlers.has(messageId)) {
messageHandlers.delete(messageId)
+ console.error('[SharedWorkerClient] Message timed out:', {type: message.type, messageId})
reject(new Error('Message timeout'))
}
}, 30000) // 30 second timeout
@@ -195,6 +202,10 @@ function handleMessage(message: {
// If worker is not yet connected, buffer the message
if (workerStatus !== 'connected') {
+ console.log('[SharedWorkerClient] Buffering message (worker not connected):', {
+ type: message.type,
+ messageId,
+ })
messageBuffer.push({...message, messageId})
return promise
}
diff --git a/packages/react/package.json b/packages/react/package.json
index 7965d0d27..42799e77a 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -30,6 +30,11 @@
"import": "./dist/index.js",
"default": "./dist/index.js"
},
+ "./worker": {
+ "source": "./src/_exports/worker.ts",
+ "import": "./dist/worker.js",
+ "default": "./dist/worker.js"
+ },
"./package.json": "./package.json"
},
"main": "./dist/index.js",
diff --git a/packages/react/src/_exports/worker.ts b/packages/react/src/_exports/worker.ts
new file mode 100644
index 000000000..2e08acfbd
--- /dev/null
+++ b/packages/react/src/_exports/worker.ts
@@ -0,0 +1,3 @@
+import workerUrl from '@sanity/sdk/worker?worker&url'
+
+export default workerUrl