Skip to content

Commit cd558da

Browse files
committed
feat: add service worker store and script
1 parent 4e19429 commit cd558da

File tree

12 files changed

+889
-5
lines changed

12 files changed

+889
-5
lines changed

apps/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"prettier": "@sanity/prettier-config",
1515
"dependencies": {
16+
"@sanity/sdk": "workspace:*",
1617
"@sanity/sdk-react": "workspace:*",
1718
"@sanity/ui": "^2.15.13",
1819
"react": "^18.3.1",

apps/dashboard/src/App.tsx

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import {createSubscriptionRequest, registerSubscription, unregisterSubscription} from '@sanity/sdk'
12
import {SanityApp, SanityConfig} from '@sanity/sdk-react'
23
import {Spinner, ThemeProvider} from '@sanity/ui'
34
import {buildTheme} from '@sanity/ui/theme'
4-
import {type JSX, Suspense} from 'react'
5+
import {type JSX, Suspense, useState} from 'react'
56

67
const theme = buildTheme({})
78

@@ -20,6 +21,78 @@ const devConfigs: SanityConfig[] = [
2021
},
2122
]
2223

24+
// SharedWorker test component
25+
function SharedWorkerTest() {
26+
const [subscriptionId, setSubscriptionId] = useState<string | null>(null)
27+
const [status, setStatus] = useState<string>('Ready to test')
28+
29+
const testSubscription = async () => {
30+
// eslint-disable-next-line no-console
31+
console.log('testSubscription')
32+
try {
33+
setStatus('Testing subscription...')
34+
35+
const subscription = createSubscriptionRequest({
36+
storeName: 'query',
37+
projectId: 'ppsg7ml5',
38+
dataset: 'test',
39+
params: {
40+
query: '*[_type == "movie"]',
41+
options: {},
42+
},
43+
appId: 'dashboard-app',
44+
})
45+
46+
const id = await registerSubscription(subscription)
47+
setSubscriptionId(id)
48+
setStatus(`Subscription registered: ${id}`)
49+
} catch (error) {
50+
setStatus(`Error: ${error instanceof Error ? error.message : String(error)}`)
51+
}
52+
}
53+
54+
const testUnsubscription = async () => {
55+
if (!subscriptionId) return
56+
57+
try {
58+
setStatus('Testing unsubscription...')
59+
60+
await unregisterSubscription(subscriptionId)
61+
setSubscriptionId(null)
62+
setStatus('Subscription unregistered successfully')
63+
} catch (error) {
64+
setStatus(`Error: ${error instanceof Error ? error.message : String(error)}`)
65+
}
66+
}
67+
68+
return (
69+
<div style={{padding: 12, borderBottom: '1px solid #eee'}}>
70+
<div>Dashboard (iframes sdk-app below)</div>
71+
<div style={{marginTop: 8, fontSize: '14px'}}>
72+
<div>SharedWorker Test:</div>
73+
<div style={{marginTop: 4}}>
74+
<button onClick={testSubscription} disabled={!!subscriptionId}>
75+
Test Subscription
76+
</button>
77+
{subscriptionId && (
78+
<button onClick={testUnsubscription} style={{marginLeft: 8}}>
79+
Test Unsubscription
80+
</button>
81+
)}
82+
</div>
83+
<div style={{marginTop: 4, fontFamily: 'monospace', fontSize: '12px'}}>
84+
Status: {status}
85+
</div>
86+
{subscriptionId && (
87+
<div style={{marginTop: 4, fontFamily: 'monospace', fontSize: '12px'}}>
88+
Active Subscription: {subscriptionId}
89+
</div>
90+
)}
91+
</div>
92+
</div>
93+
)
94+
}
95+
2396
export default function App(): JSX.Element {
2497
return (
2598
<ThemeProvider theme={theme}>
@@ -33,9 +106,7 @@ export default function App(): JSX.Element {
33106
flexDirection: 'column',
34107
}}
35108
>
36-
<div style={{padding: 12, borderBottom: '1px solid #eee'}}>
37-
Dashboard (iframes sdk-app below)
38-
</div>
109+
<SharedWorkerTest />
39110
<iframe
40111
title="sdk-app"
41112
src="http://localhost:3341/"

apps/dashboard/src/main.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
1+
import {addStatusListener, getSdkWorker, type WorkerStatus} from '@sanity/sdk'
2+
import sdkWorker from '@sanity/sdk/worker?worker&url'
13
import {StrictMode} from 'react'
24
import {createRoot} from 'react-dom/client'
35

46
import App from './App'
57

8+
// Initialize SharedWorker for subscription management
9+
async function initializeSharedWorker() {
10+
try {
11+
// Get the SDK worker instance - use direct URL
12+
const workerUrl = new URL(sdkWorker, import.meta.url).href
13+
14+
getSdkWorker(workerUrl)
15+
16+
// Add status listener for debugging
17+
addStatusListener((status: WorkerStatus) => {
18+
// eslint-disable-next-line no-console
19+
console.log('[Dashboard] Worker status changed:', status)
20+
})
21+
} catch (error) {
22+
// eslint-disable-next-line no-console
23+
console.warn('Failed to initialize SharedWorker:', error)
24+
// Fallback to local subscription management
25+
}
26+
}
27+
28+
// Initialize SharedWorker when the app starts
29+
initializeSharedWorker()
30+
631
createRoot(document.getElementById('root')!).render(
732
<StrictMode>
833
<App />

knip.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const baseConfig = {
8787
},
8888
project,
8989
entry: ['package.bundle.ts'],
90-
ignore: ['src/presence/bifurTransport.ts', 'src/presence/types.ts'],
90+
ignore: ['src/presence/bifurTransport.ts', 'src/presence/types.ts', 'src/_exports/worker.ts'],
9191
ignoreDependencies: ['@sanity/bifur-client', '@sanity/browserslist-config'],
9292
},
9393
},

packages/core/src/_exports/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,28 @@ export {
132132
export {getPerspectiveState} from '../releases/getPerspectiveState'
133133
export type {ReleaseDocument} from '../releases/releasesStore'
134134
export {getActiveReleasesState} from '../releases/releasesStore'
135+
export {
136+
addStatusListener,
137+
disconnectWorker,
138+
getSdkWorker,
139+
registerSubscription,
140+
sendMessage,
141+
unregisterSubscription,
142+
type WorkerStatus,
143+
} from '../sharedWorkerStore/sharedWorkerClient'
144+
export {type SharedWorkerStore, sharedWorkerStore} from '../sharedWorkerStore/sharedWorkerStore'
145+
export {
146+
type ActiveSubscription,
147+
type SharedWorkerStoreActions,
148+
type SharedWorkerStoreState,
149+
type SubscriptionRequest,
150+
} from '../sharedWorkerStore/types'
151+
export {
152+
areSubscriptionsEquivalent,
153+
createSubscriptionId,
154+
createSubscriptionRequest,
155+
groupSubscriptionsByParams,
156+
} from '../sharedWorkerStore/utils/subscriptionManager'
135157
export {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
136158
export {type Selector, type StateSource} from '../store/createStateSourceAction'
137159
export {getUsersKey, parseUsersKey} from '../users/reducers'
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/// <reference lib="webworker" />
2+
/* eslint-disable no-console */
3+
4+
/**
5+
* @internal
6+
* SharedWorker for managing subscriptions across SDK apps
7+
*/
8+
9+
import {sharedWorkerStore} from '../sharedWorkerStore/sharedWorkerStore'
10+
import {type SubscriptionRequest} from '../sharedWorkerStore/types'
11+
12+
declare const self: SharedWorkerGlobalScope
13+
14+
console.log('[SharedWorker] Worker script loaded')
15+
16+
// Handle new connections
17+
self.onconnect = (event: MessageEvent) => {
18+
const port = event.ports[0]
19+
20+
console.log('[SharedWorker] New connection established')
21+
22+
// Set up message handling for this port
23+
port.onmessage = async (e: MessageEvent) => {
24+
const {type, data} = e.data
25+
26+
console.log('[SharedWorker] Received message:', type, data)
27+
28+
try {
29+
switch (type) {
30+
case 'REGISTER_SUBSCRIPTION':
31+
handleRegisterSubscription(data, port)
32+
break
33+
case 'UNREGISTER_SUBSCRIPTION':
34+
handleUnregisterSubscription(data.subscriptionId, port)
35+
break
36+
case 'GET_SUBSCRIPTION_COUNT':
37+
handleGetSubscriptionCount(port)
38+
break
39+
case 'GET_ALL_SUBSCRIPTIONS':
40+
handleGetAllSubscriptions(port)
41+
break
42+
default:
43+
console.warn('[SharedWorker] Unknown message type:', type)
44+
port.postMessage({
45+
type: 'ERROR',
46+
data: {error: `Unknown message type: ${type}`},
47+
})
48+
}
49+
} catch (error) {
50+
console.error('[SharedWorker] Error handling message:', error)
51+
port.postMessage({
52+
type: 'ERROR',
53+
data: {error: (error as Error).message},
54+
})
55+
}
56+
}
57+
58+
// Start the port
59+
port.start()
60+
console.log('[SharedWorker] Port started, sending welcome message')
61+
port.postMessage({type: 'welcome'})
62+
}
63+
64+
/**
65+
* @internal
66+
* Handle the registration of a subscription
67+
* @param subscription - The subscription to register
68+
* @param port - The port to send the response to
69+
*/
70+
function handleRegisterSubscription(subscription: SubscriptionRequest, port: MessagePort): void {
71+
try {
72+
sharedWorkerStore.getState().registerSubscription(subscription)
73+
74+
// Send confirmation back to the client
75+
port.postMessage({
76+
type: 'SUBSCRIPTION_REGISTERED',
77+
data: {subscriptionId: subscription.subscriptionId},
78+
})
79+
80+
console.log('[SharedWorker] Registered subscription:', subscription.subscriptionId)
81+
} catch (error) {
82+
console.error('[SharedWorker] Failed to register subscription:', error)
83+
84+
// Send error back to the client
85+
port.postMessage({
86+
type: 'SUBSCRIPTION_ERROR',
87+
data: {error: (error as Error).message, subscriptionId: subscription.subscriptionId},
88+
})
89+
}
90+
}
91+
92+
/**
93+
* @internal
94+
* Handle the unregistration of a subscription
95+
* @param subscriptionId - The ID of the subscription to unregister
96+
* @param port - The port to send the response to
97+
*/
98+
function handleUnregisterSubscription(subscriptionId: string, port: MessagePort): void {
99+
try {
100+
sharedWorkerStore.getState().unregisterSubscription(subscriptionId)
101+
102+
// Send confirmation back to the client
103+
port.postMessage({
104+
type: 'SUBSCRIPTION_UNREGISTERED',
105+
data: {subscriptionId},
106+
})
107+
108+
console.log('[SharedWorker] Unregistered subscription:', subscriptionId)
109+
} catch (error) {
110+
console.error('[SharedWorker] Failed to unregister subscription:', error)
111+
112+
// Send error back to the client
113+
port.postMessage({
114+
type: 'SUBSCRIPTION_ERROR',
115+
data: {error: (error as Error).message, subscriptionId},
116+
})
117+
}
118+
}
119+
120+
function handleGetSubscriptionCount(port: MessagePort): void {
121+
try {
122+
const count = sharedWorkerStore.getState().getSubscriptionCount()
123+
124+
port.postMessage({
125+
type: 'SUBSCRIPTION_COUNT',
126+
data: {count},
127+
})
128+
} catch (error) {
129+
console.error('[SharedWorker] Failed to get subscription count:', error)
130+
131+
port.postMessage({
132+
type: 'SUBSCRIPTION_ERROR',
133+
data: {error: (error as Error).message},
134+
})
135+
}
136+
}
137+
138+
function handleGetAllSubscriptions(port: MessagePort): void {
139+
try {
140+
const subscriptions = sharedWorkerStore.getState().getAllSubscriptions()
141+
142+
port.postMessage({
143+
type: 'ALL_SUBSCRIPTIONS',
144+
data: {subscriptions},
145+
})
146+
} catch (error) {
147+
console.error('[SharedWorker] Failed to get all subscriptions:', error)
148+
149+
port.postMessage({
150+
type: 'SUBSCRIPTION_ERROR',
151+
data: {error: (error as Error).message},
152+
})
153+
}
154+
}
155+
156+
// Export for testing/development (this won't be used in the actual SharedWorker)
157+
/** @internal */
158+
export {handleRegisterSubscription, handleUnregisterSubscription}

0 commit comments

Comments
 (0)