@sanity/sdk 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +27 -19
- package/dist/index.js +68 -51
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/_exports/index.ts +1 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +42 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +23 -14
- package/src/comlink/node/actions/releaseNode.test.ts +8 -73
- package/src/comlink/node/actions/releaseNode.ts +5 -18
- package/src/comlink/node/comlinkNodeStore.ts +7 -3
- package/src/comlink/node/getNodeState.test.ts +90 -0
- package/src/comlink/node/getNodeState.ts +73 -0
- package/src/document/documentConstants.ts +1 -1
- package/src/document/sharedListener.ts +1 -1
- package/src/favorites/favorites.test.ts +50 -96
- package/src/favorites/favorites.ts +22 -42
- package/src/query/queryStoreConstants.ts +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"lodash-es": "^4.17.21",
|
|
54
54
|
"reselect": "^5.1.1",
|
|
55
55
|
"rxjs": "^7.8.2",
|
|
56
|
-
"zustand": "^5.0.
|
|
56
|
+
"zustand": "^5.0.4"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"vitest": "^3.1.2",
|
|
70
70
|
"@repo/config-eslint": "0.0.0",
|
|
71
71
|
"@repo/config-test": "0.0.1",
|
|
72
|
-
"@repo/package.config": "0.0.1",
|
|
73
72
|
"@repo/package.bundle": "3.82.0",
|
|
73
|
+
"@repo/package.config": "0.0.1",
|
|
74
74
|
"@repo/tsconfig": "0.0.1"
|
|
75
75
|
},
|
|
76
76
|
"engines": {
|
package/src/_exports/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
releaseChannel,
|
|
33
33
|
} from '../comlink/controller/comlinkControllerStore'
|
|
34
34
|
export type {ComlinkNodeState} from '../comlink/node/comlinkNodeStore'
|
|
35
|
-
export {
|
|
35
|
+
export {getNodeState, type NodeState} from '../comlink/node/getNodeState'
|
|
36
36
|
export {type FrameMessage, type WindowMessage} from '../comlink/types'
|
|
37
37
|
export {type AuthConfig, type AuthProvider} from '../config/authConfig'
|
|
38
38
|
export {
|
|
@@ -12,6 +12,7 @@ vi.mock('@sanity/comlink', () => ({
|
|
|
12
12
|
createNode: vi.fn(() => ({
|
|
13
13
|
start: vi.fn(),
|
|
14
14
|
stop: vi.fn(),
|
|
15
|
+
onStatus: vi.fn(),
|
|
15
16
|
})),
|
|
16
17
|
}))
|
|
17
18
|
|
|
@@ -32,9 +33,9 @@ describe('getOrCreateNode', () => {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
beforeEach(() => {
|
|
35
|
-
mockNode = {start: vi.fn(), stop: vi.fn()}
|
|
36
|
+
mockNode = {start: vi.fn(), stop: vi.fn(), onStatus: vi.fn()}
|
|
36
37
|
vi.mocked(comlink.createNode).mockReturnValue(mockNode as Node<WindowMessage, FrameMessage>)
|
|
37
|
-
state = createStoreState<ComlinkNodeState>({nodes: new Map()})
|
|
38
|
+
state = createStoreState<ComlinkNodeState>({nodes: new Map(), subscriptions: new Map()})
|
|
38
39
|
vi.clearAllMocks()
|
|
39
40
|
})
|
|
40
41
|
|
|
@@ -45,7 +46,7 @@ describe('getOrCreateNode', () => {
|
|
|
45
46
|
expect(node.start).toHaveBeenCalled()
|
|
46
47
|
})
|
|
47
48
|
|
|
48
|
-
it('
|
|
49
|
+
it('should store the node in nodeStore', () => {
|
|
49
50
|
const node = getOrCreateNode({state, instance}, nodeConfig)
|
|
50
51
|
|
|
51
52
|
expect(getOrCreateNode({state, instance}, nodeConfig)).toBe(node)
|
|
@@ -64,4 +65,42 @@ describe('getOrCreateNode', () => {
|
|
|
64
65
|
),
|
|
65
66
|
).toThrow('Node "test-node" already exists with different options')
|
|
66
67
|
})
|
|
68
|
+
|
|
69
|
+
it('should subscribe to status changes and update state', () => {
|
|
70
|
+
let statusCallback: ((status: string) => void) | undefined
|
|
71
|
+
const statusUnsubMock = vi.fn()
|
|
72
|
+
mockNode.onStatus = vi.fn((cb) => {
|
|
73
|
+
statusCallback = cb
|
|
74
|
+
return statusUnsubMock
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
getOrCreateNode({state, instance}, nodeConfig)
|
|
78
|
+
|
|
79
|
+
expect(mockNode.onStatus).toHaveBeenCalled()
|
|
80
|
+
expect(state.get().nodes.get(nodeConfig.name)?.statusUnsub).toBe(statusUnsubMock)
|
|
81
|
+
|
|
82
|
+
statusCallback?.('connected')
|
|
83
|
+
|
|
84
|
+
expect(state.get().nodes.get(nodeConfig.name)?.status).toBe('connected')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should not update state if node entry is missing when status changes', () => {
|
|
88
|
+
let statusCallback: ((status: string) => void) | undefined
|
|
89
|
+
const statusUnsubMock = vi.fn()
|
|
90
|
+
mockNode.onStatus = vi.fn((cb) => {
|
|
91
|
+
statusCallback = cb
|
|
92
|
+
return statusUnsubMock
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
getOrCreateNode({state, instance}, nodeConfig)
|
|
96
|
+
|
|
97
|
+
// Remove the node entry before triggering the status callback
|
|
98
|
+
state.get().nodes.delete(nodeConfig.name)
|
|
99
|
+
|
|
100
|
+
// Simulate a status change
|
|
101
|
+
statusCallback?.('connected')
|
|
102
|
+
|
|
103
|
+
// Assert: node entry is still missing, so no update occurred
|
|
104
|
+
expect(state.get().nodes.has(nodeConfig.name)).toBe(false)
|
|
105
|
+
})
|
|
67
106
|
})
|
|
@@ -5,12 +5,6 @@ import {type StoreContext} from '../../../store/defineStore'
|
|
|
5
5
|
import {type FrameMessage, type WindowMessage} from '../../types'
|
|
6
6
|
import {type ComlinkNodeState} from '../comlinkNodeStore'
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Retrieve or create a node to be used for communication between
|
|
10
|
-
* an application and the controller -- specifically, a node should
|
|
11
|
-
* be created within a frame / window to communicate with the controller.
|
|
12
|
-
* @public
|
|
13
|
-
*/
|
|
14
8
|
export const getOrCreateNode = (
|
|
15
9
|
{state}: StoreContext<ComlinkNodeState>,
|
|
16
10
|
options: NodeInput,
|
|
@@ -24,13 +18,6 @@ export const getOrCreateNode = (
|
|
|
24
18
|
throw new Error(`Node "${options.name}" already exists with different options`)
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
state.set('incrementNodeRefCount', {
|
|
28
|
-
nodes: new Map(nodes).set(options.name, {
|
|
29
|
-
...existing,
|
|
30
|
-
refCount: existing.refCount + 1,
|
|
31
|
-
}),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
21
|
existing.node.start()
|
|
35
22
|
return existing.node
|
|
36
23
|
}
|
|
@@ -38,7 +25,29 @@ export const getOrCreateNode = (
|
|
|
38
25
|
const node: Node<WindowMessage, FrameMessage> = createNode(options)
|
|
39
26
|
node.start()
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
// Subscribe to status changes
|
|
29
|
+
const statusUnsub = node.onStatus((status) => {
|
|
30
|
+
const currentNodes = state.get().nodes
|
|
31
|
+
const currentEntry = currentNodes.get(options.name)
|
|
32
|
+
if (!currentEntry) return
|
|
33
|
+
const updatedEntry = {
|
|
34
|
+
...currentEntry,
|
|
35
|
+
status,
|
|
36
|
+
}
|
|
37
|
+
state.set('updateNodeStatus', {
|
|
38
|
+
nodes: new Map(currentNodes).set(options.name, updatedEntry),
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Set up initial entry with status, error, and statusUnsub
|
|
43
|
+
const entry = {
|
|
44
|
+
node,
|
|
45
|
+
options,
|
|
46
|
+
status: 'idle' as const,
|
|
47
|
+
statusUnsub,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
nodes.set(options.name, entry)
|
|
42
51
|
|
|
43
52
|
state.set('createNode', {nodes})
|
|
44
53
|
|
|
@@ -4,7 +4,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
4
4
|
import {createSanityInstance, type SanityInstance} from '../../../store/createSanityInstance'
|
|
5
5
|
import {createStoreState} from '../../../store/createStoreState'
|
|
6
6
|
import {type FrameMessage, type WindowMessage} from '../../types'
|
|
7
|
-
import {type ComlinkNodeState
|
|
7
|
+
import {type ComlinkNodeState} from '../comlinkNodeStore'
|
|
8
8
|
import {releaseNode} from './releaseNode'
|
|
9
9
|
|
|
10
10
|
const nodeConfig = {
|
|
@@ -18,6 +18,7 @@ describe('releaseNode', () => {
|
|
|
18
18
|
let mockNode: Partial<Node<WindowMessage, FrameMessage>> & {
|
|
19
19
|
start: ReturnType<typeof vi.fn>
|
|
20
20
|
stop: ReturnType<typeof vi.fn>
|
|
21
|
+
onStatus: ReturnType<typeof vi.fn>
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
beforeEach(() => {
|
|
@@ -25,8 +26,8 @@ describe('releaseNode', () => {
|
|
|
25
26
|
projectId: 'test-project-id',
|
|
26
27
|
dataset: 'test-dataset',
|
|
27
28
|
})
|
|
28
|
-
mockNode = {start: vi.fn(), stop: vi.fn()}
|
|
29
|
-
state = createStoreState<ComlinkNodeState>({nodes: new Map()})
|
|
29
|
+
mockNode = {start: vi.fn(), stop: vi.fn(), onStatus: vi.fn()}
|
|
30
|
+
state = createStoreState<ComlinkNodeState>({nodes: new Map(), subscriptions: new Map()})
|
|
30
31
|
vi.clearAllMocks()
|
|
31
32
|
})
|
|
32
33
|
|
|
@@ -40,7 +41,6 @@ describe('releaseNode', () => {
|
|
|
40
41
|
nodes.set('test-node', {
|
|
41
42
|
node: mockNode as Node<WindowMessage, FrameMessage>,
|
|
42
43
|
options: nodeConfig,
|
|
43
|
-
refCount: 1,
|
|
44
44
|
})
|
|
45
45
|
state.set('setup', {nodes})
|
|
46
46
|
|
|
@@ -54,83 +54,18 @@ describe('releaseNode', () => {
|
|
|
54
54
|
expect(state.get().nodes.has('test-node')).toBe(false)
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
it('should
|
|
58
|
-
|
|
57
|
+
it('should call statusUnsub if present when releasing node', () => {
|
|
58
|
+
const statusUnsub = vi.fn()
|
|
59
59
|
const nodes = new Map()
|
|
60
60
|
nodes.set('test-node', {
|
|
61
61
|
node: mockNode as Node<WindowMessage, FrameMessage>,
|
|
62
62
|
options: nodeConfig,
|
|
63
|
-
|
|
63
|
+
statusUnsub,
|
|
64
64
|
})
|
|
65
65
|
state.set('setup', {nodes})
|
|
66
66
|
|
|
67
|
-
// Release once
|
|
68
67
|
releaseNode({state, instance}, 'test-node')
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
expect(mockNode.stop).not.toHaveBeenCalled()
|
|
72
|
-
|
|
73
|
-
// Verify refCount is 1
|
|
74
|
-
const nodeEntry = state.get().nodes.get('test-node') as NodeEntry
|
|
75
|
-
expect(nodeEntry?.refCount).toBe(1)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should handle multiple releases gracefully', () => {
|
|
79
|
-
// Set up a node in the state
|
|
80
|
-
const nodes = new Map()
|
|
81
|
-
nodes.set('test-node', {
|
|
82
|
-
node: mockNode as Node<WindowMessage, FrameMessage>,
|
|
83
|
-
options: nodeConfig,
|
|
84
|
-
refCount: 1,
|
|
85
|
-
})
|
|
86
|
-
state.set('setup', {nodes})
|
|
87
|
-
|
|
88
|
-
// Release multiple times
|
|
89
|
-
releaseNode({state, instance}, 'test-node')
|
|
90
|
-
releaseNode({state, instance}, 'test-node')
|
|
91
|
-
releaseNode({state, instance}, 'test-node')
|
|
92
|
-
|
|
93
|
-
// Verify node is removed after first release
|
|
94
|
-
expect(state.get().nodes.has('test-node')).toBe(false)
|
|
95
|
-
// Stop should be called exactly once
|
|
96
|
-
expect(mockNode.stop).toHaveBeenCalledTimes(1)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('should handle releasing non-existent nodes', () => {
|
|
100
|
-
// Should not throw when releasing non-existent node
|
|
101
|
-
expect(() => releaseNode({state, instance}, 'non-existent')).not.toThrow()
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('should maintain correct state after complex operations', () => {
|
|
105
|
-
// Set up a node with refCount = 3
|
|
106
|
-
const nodes = new Map()
|
|
107
|
-
nodes.set('test-node', {
|
|
108
|
-
node: mockNode as Node<WindowMessage, FrameMessage>,
|
|
109
|
-
options: nodeConfig,
|
|
110
|
-
refCount: 3,
|
|
111
|
-
})
|
|
112
|
-
state.set('setup', {nodes})
|
|
113
|
-
|
|
114
|
-
// Initial refCount should be 3
|
|
115
|
-
let nodeEntry = state.get().nodes.get('test-node') as NodeEntry
|
|
116
|
-
expect(nodeEntry?.refCount).toBe(3)
|
|
117
|
-
|
|
118
|
-
// Release twice
|
|
119
|
-
releaseNode({state, instance}, 'test-node')
|
|
120
|
-
releaseNode({state, instance}, 'test-node')
|
|
121
|
-
|
|
122
|
-
nodeEntry = state.get().nodes.get('test-node') as NodeEntry
|
|
123
|
-
expect(nodeEntry?.refCount).toBe(1)
|
|
124
|
-
|
|
125
|
-
// Verify node hasn't been stopped yet
|
|
126
|
-
expect(mockNode.stop).not.toHaveBeenCalled()
|
|
127
|
-
|
|
128
|
-
// Release final reference
|
|
129
|
-
releaseNode({state, instance}, 'test-node')
|
|
130
|
-
|
|
131
|
-
// Verify node was stopped
|
|
132
|
-
expect(mockNode.stop).toHaveBeenCalled()
|
|
133
|
-
|
|
134
|
-
expect(state.get().nodes.has('test-node')).toBe(false)
|
|
69
|
+
expect(statusUnsub).toHaveBeenCalled()
|
|
135
70
|
})
|
|
136
71
|
})
|
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
import {type StoreContext} from '../../../store/defineStore'
|
|
2
2
|
import {type ComlinkNodeState} from '../comlinkNodeStore'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Release a node that was previously created with getOrCreateNode.
|
|
6
|
-
* @public
|
|
7
|
-
*/
|
|
8
4
|
export const releaseNode = ({state}: StoreContext<ComlinkNodeState>, name: string): void => {
|
|
9
5
|
const nodes = state.get().nodes
|
|
10
6
|
const existing = nodes.get(name)
|
|
11
7
|
|
|
12
8
|
if (existing) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (newRefCount <= 0) {
|
|
16
|
-
existing.node.stop()
|
|
17
|
-
nodes.delete(name)
|
|
18
|
-
state.set('removeNode', {nodes})
|
|
19
|
-
return
|
|
9
|
+
if (existing.statusUnsub) {
|
|
10
|
+
existing.statusUnsub()
|
|
20
11
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...existing,
|
|
25
|
-
refCount: newRefCount,
|
|
26
|
-
}),
|
|
27
|
-
})
|
|
12
|
+
existing.node.stop()
|
|
13
|
+
nodes.delete(name)
|
|
14
|
+
state.set('removeNode', {nodes})
|
|
28
15
|
return
|
|
29
16
|
}
|
|
30
17
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type Node, type NodeInput} from '@sanity/comlink'
|
|
1
|
+
import {type Node, type NodeInput, type Status} from '@sanity/comlink'
|
|
2
2
|
|
|
3
3
|
import {bindActionGlobally} from '../../store/createActionBinder'
|
|
4
4
|
import {defineStore} from '../../store/defineStore'
|
|
@@ -14,8 +14,9 @@ export interface NodeEntry {
|
|
|
14
14
|
node: Node<WindowMessage, FrameMessage>
|
|
15
15
|
// we store options to ensure that channels remain as unique / consistent as possible
|
|
16
16
|
options: NodeInput
|
|
17
|
-
//
|
|
18
|
-
|
|
17
|
+
// status of the node connection
|
|
18
|
+
status: Status
|
|
19
|
+
statusUnsub?: () => void
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -24,12 +25,15 @@ export interface NodeEntry {
|
|
|
24
25
|
*/
|
|
25
26
|
export interface ComlinkNodeState {
|
|
26
27
|
nodes: Map<string, NodeEntry>
|
|
28
|
+
// Map of node name to set of active subscriber symbols
|
|
29
|
+
subscriptions: Map<string, Set<symbol>>
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export const comlinkNodeStore = defineStore<ComlinkNodeState>({
|
|
30
33
|
name: 'nodeStore',
|
|
31
34
|
getInitialState: () => ({
|
|
32
35
|
nodes: new Map(),
|
|
36
|
+
subscriptions: new Map(),
|
|
33
37
|
}),
|
|
34
38
|
|
|
35
39
|
initialize({state}) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {type Node} from '@sanity/comlink'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createSanityInstance} from '../../store/createSanityInstance'
|
|
5
|
+
import {type FrameMessage, type WindowMessage} from '../types'
|
|
6
|
+
import * as comlinkNodeStoreModule from './comlinkNodeStore'
|
|
7
|
+
import {getOrCreateNode} from './comlinkNodeStore'
|
|
8
|
+
import {getNodeState} from './getNodeState'
|
|
9
|
+
|
|
10
|
+
const mockNode: Node<WindowMessage, FrameMessage> = {
|
|
11
|
+
start: vi.fn(),
|
|
12
|
+
stop: vi.fn(),
|
|
13
|
+
onStatus: vi.fn(),
|
|
14
|
+
} as unknown as Node<WindowMessage, FrameMessage>
|
|
15
|
+
|
|
16
|
+
vi.mock('@sanity/comlink', () => ({
|
|
17
|
+
createNode: vi.fn(() => mockNode),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const nodeConfig = {name: 'test-node', connectTo: 'parent'}
|
|
21
|
+
|
|
22
|
+
describe('getNodeState', () => {
|
|
23
|
+
let instance: ReturnType<typeof createSanityInstance>
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.clearAllMocks()
|
|
31
|
+
instance.dispose()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns undefined if node is not present', () => {
|
|
35
|
+
const source = getNodeState(instance, nodeConfig)
|
|
36
|
+
expect(source.getCurrent()).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns node and status if node is present and connected', async () => {
|
|
40
|
+
let statusCallback: ((status: 'idle' | 'handshaking' | 'connected') => void) | undefined
|
|
41
|
+
|
|
42
|
+
mockNode.onStatus = (cb: (status: 'idle' | 'handshaking' | 'connected') => void) => {
|
|
43
|
+
statusCallback = cb
|
|
44
|
+
return () => {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Subscribe to the state source first
|
|
48
|
+
const source = getNodeState(instance, nodeConfig)
|
|
49
|
+
source.subscribe(() => {})
|
|
50
|
+
|
|
51
|
+
// Actually create the node
|
|
52
|
+
getOrCreateNode(instance, nodeConfig)
|
|
53
|
+
|
|
54
|
+
// Simulate the node becoming connected
|
|
55
|
+
statusCallback?.('connected')
|
|
56
|
+
|
|
57
|
+
// Await a tick for the selector to pick up the change
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
59
|
+
|
|
60
|
+
expect(source.getCurrent()).toEqual({node: mockNode, status: 'connected'})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('onSubscribe calls getOrCreateNode', () => {
|
|
64
|
+
const spy = vi.spyOn(comlinkNodeStoreModule, 'getOrCreateNode')
|
|
65
|
+
const source = getNodeState(instance, nodeConfig)
|
|
66
|
+
const unsubscribe = source.subscribe()
|
|
67
|
+
expect(spy).toHaveBeenCalledWith(instance, nodeConfig)
|
|
68
|
+
unsubscribe()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('unsubscribe calls releaseNode', async () => {
|
|
72
|
+
vi.useFakeTimers()
|
|
73
|
+
const spy = vi.spyOn(comlinkNodeStoreModule, 'releaseNode')
|
|
74
|
+
let statusCallback: ((status: 'idle' | 'handshaking' | 'connected') => void) | undefined
|
|
75
|
+
mockNode.onStatus = (cb: (status: 'idle' | 'handshaking' | 'connected') => void) => {
|
|
76
|
+
statusCallback = cb
|
|
77
|
+
return () => {}
|
|
78
|
+
}
|
|
79
|
+
const source = getNodeState(instance, nodeConfig)
|
|
80
|
+
const unsubscribe = source.subscribe(() => {})
|
|
81
|
+
|
|
82
|
+
getOrCreateNode(instance, nodeConfig)
|
|
83
|
+
statusCallback?.('connected')
|
|
84
|
+
|
|
85
|
+
unsubscribe()
|
|
86
|
+
vi.advanceTimersByTime(5000)
|
|
87
|
+
expect(spy).toHaveBeenCalledWith(instance, 'test-node')
|
|
88
|
+
vi.useRealTimers()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {type Node, type NodeInput, type Status} from '@sanity/comlink'
|
|
2
|
+
import {createSelector} from 'reselect'
|
|
3
|
+
|
|
4
|
+
import {bindActionGlobally} from '../../store/createActionBinder'
|
|
5
|
+
import {createStateSourceAction, type SelectorContext} from '../../store/createStateSourceAction'
|
|
6
|
+
import {type FrameMessage, type WindowMessage} from '../types'
|
|
7
|
+
import {
|
|
8
|
+
type ComlinkNodeState,
|
|
9
|
+
comlinkNodeStore,
|
|
10
|
+
getOrCreateNode,
|
|
11
|
+
releaseNode,
|
|
12
|
+
} from './comlinkNodeStore'
|
|
13
|
+
|
|
14
|
+
const NODE_RELEASE_TIME = 5000
|
|
15
|
+
|
|
16
|
+
// Public shape for node state
|
|
17
|
+
/**
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export interface NodeState {
|
|
21
|
+
node: Node<WindowMessage, FrameMessage>
|
|
22
|
+
status: Status | undefined
|
|
23
|
+
}
|
|
24
|
+
const selectNode = (context: SelectorContext<ComlinkNodeState>, nodeInput: NodeInput) =>
|
|
25
|
+
context.state.nodes.get(nodeInput.name)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Provides a subscribable state source for a node by name
|
|
29
|
+
* @param instance - The Sanity instance to get the node state for
|
|
30
|
+
* @param nodeInput - The configuration for the node to get the state for
|
|
31
|
+
|
|
32
|
+
* @returns A subscribable state source for the node
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export const getNodeState = bindActionGlobally(
|
|
36
|
+
comlinkNodeStore,
|
|
37
|
+
createStateSourceAction<ComlinkNodeState, [NodeInput], NodeState | undefined>({
|
|
38
|
+
selector: createSelector([selectNode], (nodeEntry) => {
|
|
39
|
+
return nodeEntry?.status === 'connected'
|
|
40
|
+
? {
|
|
41
|
+
node: nodeEntry.node,
|
|
42
|
+
status: nodeEntry.status,
|
|
43
|
+
}
|
|
44
|
+
: undefined
|
|
45
|
+
}),
|
|
46
|
+
onSubscribe: ({state, instance}, nodeInput) => {
|
|
47
|
+
const nodeName = nodeInput.name
|
|
48
|
+
const subscriberId = Symbol('comlink-node-subscriber')
|
|
49
|
+
getOrCreateNode(instance, nodeInput)
|
|
50
|
+
|
|
51
|
+
// Add subscriber to the set for this node
|
|
52
|
+
let subs = state.get().subscriptions.get(nodeName)
|
|
53
|
+
if (!subs) {
|
|
54
|
+
subs = new Set()
|
|
55
|
+
state.get().subscriptions.set(nodeName, subs)
|
|
56
|
+
}
|
|
57
|
+
subs.add(subscriberId)
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
const activeSubs = state.get().subscriptions.get(nodeName)
|
|
62
|
+
if (activeSubs) {
|
|
63
|
+
activeSubs.delete(subscriberId)
|
|
64
|
+
if (activeSubs.size === 0) {
|
|
65
|
+
state.get().subscriptions.delete(nodeName)
|
|
66
|
+
releaseNode(instance, nodeName)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, NODE_RELEASE_TIME)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import {getClientState} from '../client/clientStore'
|
|
17
17
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
18
18
|
|
|
19
|
-
const API_VERSION = '
|
|
19
|
+
const API_VERSION = 'v2025-05-06'
|
|
20
20
|
|
|
21
21
|
export interface SharedListener {
|
|
22
22
|
events: Observable<ListenEvent<SanityDocument>>
|